diff --git a/crates/biome_js_analyze/src/services/semantic_class.rs b/crates/biome_js_analyze/src/services/semantic_class.rs index 09caccad4ffc..295be13d01c2 100644 --- a/crates/biome_js_analyze/src/services/semantic_class.rs +++ b/crates/biome_js_analyze/src/services/semantic_class.rs @@ -1,20 +1,21 @@ -use biome_js_syntax::{ - AnyJsClassMember, AnyJsExpression, AnyJsRoot, JsArrayAssignmentPattern, - JsArrowFunctionExpression, JsAssignmentExpression, JsClassDeclaration, JsClassMemberList, - JsConstructorClassMember, JsFunctionBody, JsLanguage, JsObjectAssignmentPattern, - JsObjectBindingPattern, JsPostUpdateExpression, JsPreUpdateExpression, JsPropertyClassMember, - JsStaticMemberAssignment, JsStaticMemberExpression, JsSyntaxKind, JsSyntaxNode, - JsVariableDeclarator, TextRange, TsPropertyParameter, -}; - use biome_analyze::{ AddVisitor, FromServices, Phase, Phases, QueryKey, QueryMatch, Queryable, RuleKey, RuleMetadata, ServiceBag, ServicesDiagnostic, Visitor, VisitorContext, VisitorFinishContext, }; +use biome_js_syntax::{ + AnyJsBindingPattern, AnyJsClassMember, AnyJsExpression, AnyJsObjectBindingPatternMember, + AnyJsRoot, JsArrayAssignmentPattern, JsArrowFunctionExpression, JsAssignmentExpression, + JsClassDeclaration, JsClassMemberList, JsConstructorClassMember, JsFunctionBody, JsLanguage, + JsObjectAssignmentPattern, JsObjectBindingPattern, JsPostUpdateExpression, + JsPreUpdateExpression, JsPropertyClassMember, JsStaticMemberAssignment, + JsStaticMemberExpression, JsSyntaxKind, JsSyntaxNode, JsVariableDeclarator, TextRange, + TsPropertyParameter, +}; use biome_rowan::{ AstNode, AstNodeList, AstSeparatedList, SyntaxNode, Text, WalkEvent, declare_node_union, }; use rustc_hash::FxHashSet; +use std::option::Option; #[derive(Clone)] pub struct SemanticClassServices { @@ -125,10 +126,36 @@ where } } +/// Represents how a class member is accessed within the code. +/// Variants: +/// +/// - `Write`: +/// The member is being assigned to or mutated. +/// Example: `this.count = 10;` +/// This indicates the member’s value/state changes at this point. +/// +/// - `MeaningfulRead`: +/// The member’s value is retrieved and used in a way that affects program logic. +/// Example: `if (this.enabled) { ... }` or `let x = this.value + 1;` +/// These reads influence control flow or computation. +/// +/// - `TrivialRead`: +/// The member is accessed, but its value is not used in a way that +/// meaningfully affects logic. +/// Example: `this.value;` as a standalone expression, or a read that is optimized away. +/// This is mostly for distinguishing dead reads from truly meaningful ones. +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub enum AccessKind { + Write, + MeaningfulRead, + TrivialRead, +} + #[derive(Debug, Clone, Hash, Eq, PartialEq)] pub struct ClassMemberReference { pub name: Text, pub range: TextRange, + pub access_kind: AccessKind, } #[derive(Debug, Clone, Eq, PartialEq, Default)] @@ -141,6 +168,14 @@ declare_node_union! { pub AnyPropertyMember = JsPropertyClassMember | TsPropertyParameter } +declare_node_union! { + pub AnyCandidateForUsedInExpressionNode = AnyJsUpdateExpression | AnyJsObjectBindingPatternMember | JsStaticMemberExpression | AnyJsBindingPattern +} + +declare_node_union! { + pub AnyJsUpdateExpression = JsPreUpdateExpression | JsPostUpdateExpression +} + /// Collects all `this` property references used within the members of a JavaScript class. /// /// This function traverses a `JsClassMemberList` and extracts property references from method bodies, @@ -220,7 +255,7 @@ struct ThisScopeVisitor<'a> { inherited_this_references: &'a [ClassMemberReference], current_this_scopes: Vec, } -// can not implement `Visitor` directly because it requires a new ctx that can not be created here +// Can not implement `Visitor` directly because it requires a new ctx that can not be created here impl ThisScopeVisitor<'_> { fn visit(&mut self, event: &WalkEvent>) { match event { @@ -284,7 +319,6 @@ impl ThisScopeVisitor<'_> { } WalkEvent::Leave(node) => { - // println!("leave node in ThisScopeVisitor {:?}", node); if let Some(last) = self.skipped_ranges.last() && *last == node.text_range() { @@ -336,7 +370,6 @@ impl ThisScopeReferences { .filter_map(|node| node.as_js_variable_statement().cloned()) .filter_map(|stmt| stmt.declaration().ok().map(|decl| decl.declarators())) .flat_map(|declarators| { - // .into() not working here, JsVariableDeclaratorList is not implmenting it correctly declarators.into_iter().filter_map(|declaration| { declaration.ok().map(|declarator| declarator.as_fields()) }) @@ -345,11 +378,13 @@ impl ThisScopeReferences { let id = fields.id.ok()?; let expr = fields.initializer?.expression().ok()?; let unwrapped = &expr.omit_parentheses(); - (unwrapped.syntax().first_token()?.text_trimmed() == "this").then(|| { ClassMemberReference { name: id.to_trimmed_text().clone(), range: id.syntax().text_trimmed_range(), + access_kind: get_read_access_kind( + &AnyCandidateForUsedInExpressionNode::from(id), + ), } }) }) @@ -406,6 +441,7 @@ struct ThisPatternResolver {} impl ThisPatternResolver { /// Extracts `this` references from array assignments (e.g., `[this.#value]` or `[...this.#value]`). + /// Only applicable to writes. fn collect_array_assignment_names( array_assignment_pattern: &JsArrayAssignmentPattern, scoped_this_references: &[FunctionThisReferences], @@ -426,6 +462,7 @@ impl ThisPatternResolver { Self::extract_this_member_reference( assignment.as_js_static_member_assignment(), scoped_this_references, + AccessKind::Write, ) }) } @@ -441,6 +478,7 @@ impl ThisPatternResolver { Self::extract_this_member_reference( assignment.as_js_static_member_assignment(), scoped_this_references, + AccessKind::Write, ) }) } else { @@ -451,6 +489,7 @@ impl ThisPatternResolver { } /// Collects assignment names from a JavaScript object assignment pattern, e.g. `{...this.#value}`. + /// Only applicable to writes. fn collect_object_assignment_names( assignment: &JsObjectAssignmentPattern, scoped_this_references: &[FunctionThisReferences], @@ -468,6 +507,7 @@ impl ThisPatternResolver { return Self::extract_this_member_reference( rest_params.target().ok()?.as_js_static_member_assignment(), scoped_this_references, + AccessKind::Write, ); } if let Some(property) = prop @@ -483,6 +523,7 @@ impl ThisPatternResolver { .as_any_js_assignment()? .as_js_static_member_assignment(), scoped_this_references, + AccessKind::Write, ); } None @@ -501,6 +542,7 @@ impl ThisPatternResolver { fn extract_this_member_reference( operand: Option<&JsStaticMemberAssignment>, scoped_this_references: &[FunctionThisReferences], + access_kind: AccessKind, ) -> Option { operand.and_then(|assignment| { if let Ok(object) = assignment.object() @@ -512,6 +554,7 @@ impl ThisPatternResolver { .map(|name| ClassMemberReference { name: name.to_trimmed_text(), range: name.syntax().text_trimmed_range(), + access_kind: access_kind.clone(), }) .or_else(|| { member @@ -519,6 +562,7 @@ impl ThisPatternResolver { .map(|private_name| ClassMemberReference { name: private_name.to_trimmed_text(), range: private_name.syntax().text_trimmed_range(), + access_kind, }) }) }) @@ -565,7 +609,14 @@ fn visit_references_in_body( handle_object_binding_pattern(&node, scoped_this_references, reads); handle_static_member_expression(&node, scoped_this_references, reads); handle_assignment_expression(&node, scoped_this_references, reads, writes); - handle_pre_or_post_update_expression(&node, scoped_this_references, reads, writes); + if let Some(js_update_expression) = AnyJsUpdateExpression::cast_ref(&node) { + handle_pre_or_post_update_expression( + &js_update_expression, + scoped_this_references, + reads, + writes, + ); + } } WalkEvent::Leave(_) => {} } @@ -603,8 +654,9 @@ fn handle_object_binding_pattern( && is_this_reference(&expression, scoped_this_references) { reads.insert(ClassMemberReference { - name: declarator.to_trimmed_text(), - range: declarator.syntax().text_trimmed_range(), + name: declarator.clone().to_trimmed_text(), + range: declarator.clone().syntax().text_trimmed_range(), + access_kind: get_read_access_kind(&declarator.clone().into()), }); } } @@ -639,7 +691,8 @@ fn handle_static_member_expression( { reads.insert(ClassMemberReference { name: member.to_trimmed_text(), - range: static_member.syntax().text_trimmed_range(), + range: member.syntax().text_trimmed_range(), + access_kind: get_read_access_kind(&static_member.into()), }); } } @@ -685,6 +738,7 @@ fn handle_assignment_expression( && let Some(name) = ThisPatternResolver::extract_this_member_reference( operand.as_js_static_member_assignment(), scoped_this_references, + AccessKind::MeaningfulRead, ) { reads.insert(name.clone()); @@ -711,6 +765,7 @@ fn handle_assignment_expression( && let Some(name) = ThisPatternResolver::extract_this_member_reference( assignment.as_js_static_member_assignment(), scoped_this_references, + AccessKind::Write, ) { writes.insert(name.clone()); @@ -733,23 +788,31 @@ fn handle_assignment_expression( /// } /// ``` fn handle_pre_or_post_update_expression( - node: &SyntaxNode, + js_update_expression: &AnyJsUpdateExpression, scoped_this_references: &[FunctionThisReferences], reads: &mut FxHashSet, writes: &mut FxHashSet, ) { - let operand = JsPostUpdateExpression::cast_ref(node) - .and_then(|expr| expr.operand().ok()) - .or_else(|| JsPreUpdateExpression::cast_ref(node).and_then(|expr| expr.operand().ok())); + let operand = match js_update_expression { + AnyJsUpdateExpression::JsPreUpdateExpression(expr) => expr.operand().ok(), + AnyJsUpdateExpression::JsPostUpdateExpression(expr) => expr.operand().ok(), + }; if let Some(operand) = operand && let Some(name) = ThisPatternResolver::extract_this_member_reference( operand.as_js_static_member_assignment(), scoped_this_references, + AccessKind::Write, ) { writes.insert(name.clone()); - reads.insert(name.clone()); + reads.insert(ClassMemberReference { + name: name.name, + range: name.range, + access_kind: get_read_access_kind(&AnyCandidateForUsedInExpressionNode::from( + js_update_expression.clone(), + )), + }); } } @@ -790,6 +853,9 @@ fn collect_class_property_reads_from_static_member( reads.insert(ClassMemberReference { name, range: static_member.syntax().text_trimmed_range(), + access_kind: get_read_access_kind(&AnyCandidateForUsedInExpressionNode::from( + static_member.clone(), + )), }); } @@ -817,6 +883,44 @@ fn is_within_scope_without_shadowing( false } +/// Determines the kind of read access for a given node. +fn get_read_access_kind(node: &AnyCandidateForUsedInExpressionNode) -> AccessKind { + if is_used_in_expression_context(node) { + AccessKind::MeaningfulRead + } else { + AccessKind::TrivialRead + } +} + +/// Checks if the given node is used in an expression context +/// (e.g., return, call arguments, conditionals, binary expressions). +/// Not limited to `this` references. +/// It can be used for any node; additional cases may require extending the context checks. +fn is_used_in_expression_context(node: &AnyCandidateForUsedInExpressionNode) -> bool { + node.syntax().ancestors().skip(1).any(|ancestor| { + matches!( + ancestor.kind(), + JsSyntaxKind::JS_RETURN_STATEMENT + | JsSyntaxKind::JS_CALL_ARGUMENTS + | JsSyntaxKind::JS_CONDITIONAL_EXPRESSION + | JsSyntaxKind::JS_LOGICAL_EXPRESSION + | JsSyntaxKind::JS_THROW_STATEMENT + | JsSyntaxKind::JS_AWAIT_EXPRESSION + | JsSyntaxKind::JS_YIELD_EXPRESSION + | JsSyntaxKind::JS_UNARY_EXPRESSION + | JsSyntaxKind::JS_TEMPLATE_EXPRESSION + | JsSyntaxKind::JS_CALL_EXPRESSION + | JsSyntaxKind::JS_NEW_EXPRESSION + | JsSyntaxKind::JS_IF_STATEMENT + | JsSyntaxKind::JS_SWITCH_STATEMENT + | JsSyntaxKind::JS_FOR_STATEMENT + | JsSyntaxKind::JS_FOR_IN_STATEMENT + | JsSyntaxKind::JS_FOR_OF_STATEMENT + | JsSyntaxKind::JS_BINARY_EXPRESSION + ) + }) +} + #[cfg(test)] mod tests { use super::*; @@ -827,8 +931,56 @@ mod tests { struct TestCase<'a> { description: &'a str, code: &'a str, - expected_reads: Vec<&'a str>, - expected_writes: Vec<&'a str>, + expected_reads: Vec<(&'a str, AccessKind)>, // (name, is_meaningful_read) + expected_writes: Vec<(&'a str, AccessKind)>, // (name, is_meaningful_read) + } + + fn assert_reads( + reads: &FxHashSet, + expected: &[(&str, AccessKind)], + description: &str, + ) { + for (expected_name, expected_access_kind) in expected { + let found = reads + .iter() + .find(|r| r.name.clone().text() == *expected_name) + .unwrap_or_else(|| { + panic!( + "Case '{}' failed: expected to find read '{}'", + description, expected_name + ) + }); + + assert_eq!( + found.access_kind, *expected_access_kind, + "Case '{}' failed: read '{}' access_kind mismatch", + description, expected_name + ); + } + } + + fn assert_writes( + writes: &FxHashSet, + expected: &[(&str, AccessKind)], + description: &str, + ) { + for (expected_name, expected_access_kind) in expected { + let found = writes + .iter() + .find(|r| r.name.clone().text() == *expected_name) + .unwrap_or_else(|| { + panic!( + "Case '{}' failed: expected to find write '{}'", + description, expected_name + ) + }); + + assert_eq!( + found.access_kind, *expected_access_kind, + "Case '{}' failed: write '{}' access_kind mismatch", + description, expected_name + ); + } } fn parse_ts(code: &str) -> Parse { @@ -857,26 +1009,32 @@ mod tests { TestCase { description: "reads from this", code: r#" - class Example { - method() { - const { foo, bar } = this; - } + class Example { + method() { + const { foo, bar } = this; } - "#, - expected_reads: vec!["foo", "bar"], + } + "#, + expected_reads: vec![ + ("foo", AccessKind::TrivialRead), + ("bar", AccessKind::TrivialRead), + ], expected_writes: vec![], }, TestCase { description: "reads from aliasForThis", code: r#" - class Example { - method() { - const aliasForThis = this; - const { baz, qux } = aliasForThis; - } + class Example { + method() { + const aliasForThis = this; + const { baz, qux } = aliasForThis; } - "#, - expected_reads: vec!["baz", "qux"], + } + "#, + expected_reads: vec![ + ("baz", AccessKind::TrivialRead), + ("qux", AccessKind::TrivialRead), + ], expected_writes: vec![], }, ]; @@ -896,16 +1054,7 @@ mod tests { handle_object_binding_pattern(&node, &function_this_references, &mut reads); - let names: Vec<_> = reads.into_iter().map(|r| r.name.to_string()).collect(); - - for expected_reads in &case.expected_reads { - assert!( - names.contains(&(*expected_reads).to_string()), - "Case '{}' failed: expected to find '{}'", - case.description, - expected_reads - ); - } + assert_reads(&reads, &case.expected_reads, case.description); } } @@ -915,28 +1064,34 @@ mod tests { TestCase { description: "reads static members from this", code: r#" - class Example { - method() { - console.log(this.foo); - console.log(this.bar); - } + class Example { + method() { + console.log(this.foo); + console.log(this.bar); } - "#, - expected_reads: vec!["foo", "bar"], + } + "#, + expected_reads: vec![ + ("foo", AccessKind::MeaningfulRead), + ("bar", AccessKind::MeaningfulRead), + ], expected_writes: vec![], }, TestCase { description: "reads static members from aliasForThis", code: r#" - class Example { - method() { - const aliasForThis = this; - aliasForThis.baz; - aliasForThis.qux; - } + class Example { + method() { + const aliasForThis = this; + aliasForThis.baz; + aliasForThis.qux; } - "#, - expected_reads: vec!["baz", "qux"], + } + "#, + expected_reads: vec![ + ("baz", AccessKind::TrivialRead), + ("qux", AccessKind::TrivialRead), + ], expected_writes: vec![], }, ]; @@ -952,7 +1107,6 @@ mod tests { let function_this_references = ThisScopeReferences::new(&body).collect_function_this_references(); - // Collect all static member expressions in the syntax let mut reads = FxHashSet::default(); for member_expr in syntax @@ -966,16 +1120,7 @@ mod tests { ); } - let names: Vec<_> = reads.into_iter().map(|r| r.name.to_string()).collect(); - - for expected_reads in &case.expected_reads { - assert!( - names.contains(&(*expected_reads).to_string()), - "Case '{}' failed: expected to find '{}'", - case.description, - expected_reads - ); - } + assert_reads(&reads, &case.expected_reads, case.description); } } @@ -985,32 +1130,40 @@ mod tests { TestCase { description: "assignment reads and writes with this", code: r#" - class Example { - method() { - this.x += 1; - [this.y] = [10]; - ({ a: this.z } = obj); - } + class Example { + method() { + this.x += 1; + [this.y] = [10]; + ({ a: this.z } = obj); } - "#, - expected_reads: vec!["x"], - expected_writes: vec!["x", "y", "z"], + } + "#, + expected_reads: vec![("x", AccessKind::MeaningfulRead)], // x is read due to += + expected_writes: vec![ + ("x", AccessKind::Write), + ("y", AccessKind::Write), + ("z", AccessKind::Write), + ], }, TestCase { description: "assignment reads and writes with aliasForThis", code: r#" - class Example { - method() { - const aliasForThis = this; - [aliasForThis.value] = [42]; - aliasForThis.x += 1; - [aliasForThis.y] = [10]; - ({ a: aliasForThis.z } = obj); - } + class Example { + method() { + const aliasForThis = this; + [aliasForThis.value] = [42]; + aliasForThis.x += 1; + [aliasForThis.y] = [10]; + ({ a: aliasForThis.z } = obj); } - "#, - expected_reads: vec!["x"], - expected_writes: vec!["x", "y", "z"], + } + "#, + expected_reads: vec![("x", AccessKind::MeaningfulRead)], + expected_writes: vec![ + ("x", AccessKind::Write), + ("y", AccessKind::Write), + ("z", AccessKind::Write), + ], }, ]; @@ -1040,26 +1193,8 @@ mod tests { ); } - let read_names: Vec<_> = reads.into_iter().map(|r| r.name.to_string()).collect(); - let write_names: Vec<_> = writes.into_iter().map(|r| r.name.to_string()).collect(); - - for expected_read in &case.expected_reads { - assert!( - read_names.contains(&(*expected_read).to_string()), - "Case '{}' failed: expected to find read '{}'", - case.description, - expected_read - ); - } - - for expected_write in &case.expected_writes { - assert!( - write_names.contains(&(*expected_write).to_string()), - "Case '{}' failed: expected to find write '{}'", - case.description, - expected_write - ); - } + assert_reads(&reads, &case.expected_reads, case.description); + assert_writes(&writes, &case.expected_writes, case.description); } } @@ -1069,15 +1204,30 @@ mod tests { TestCase { description: "pre/post update expressions on this properties", code: r#" - class Example { + class AnyJsUpdateExpression { method() { this.count++; --this.total; + + if (this.inIfCondition++ > 5) { + } + + return this.inReturn++; } } "#, - expected_reads: vec!["count", "total"], - expected_writes: vec!["count", "total"], + expected_reads: vec![ + ("count", AccessKind::TrivialRead), + ("total", AccessKind::TrivialRead), + ("inIfCondition", AccessKind::MeaningfulRead), + ("inReturn", AccessKind::MeaningfulRead), + ], + expected_writes: vec![ + ("count", AccessKind::Write), + ("total", AccessKind::Write), + ("inIfCondition", AccessKind::Write), + ("inReturn", AccessKind::Write), + ], }, TestCase { description: "pre/post update expressions on aliasForThis properties", @@ -1086,13 +1236,23 @@ mod tests { method() { const aliasForThis = this; const anotherAlias = this; - aliasForThis.count++; + aliasForThis.count++; --anotherAlias.total; + + return anotherAlias.inReturnIncrement++; } } "#, - expected_reads: vec!["count", "total"], - expected_writes: vec!["count", "total"], + expected_reads: vec![ + ("count", AccessKind::TrivialRead), + ("total", AccessKind::TrivialRead), + ("inReturnIncrement", AccessKind::MeaningfulRead), + ], + expected_writes: vec![ + ("count", AccessKind::Write), + ("total", AccessKind::Write), + ("inReturnIncrement", AccessKind::Write), + ], }, ]; @@ -1111,34 +1271,211 @@ mod tests { let mut writes = FxHashSet::default(); for node in syntax.descendants() { - handle_pre_or_post_update_expression( - &node, - &function_this_references, - &mut reads, - &mut writes, - ); + if let Some(js_update_expression) = AnyJsUpdateExpression::cast_ref(&node) { + handle_pre_or_post_update_expression( + &js_update_expression, + &function_this_references, + &mut reads, + &mut writes, + ); + } } - let read_names: Vec<_> = reads.into_iter().map(|r| r.name.to_string()).collect(); - let write_names: Vec<_> = writes.into_iter().map(|r| r.name.to_string()).collect(); + assert_reads(&reads, &case.expected_reads, case.description); + assert_writes(&writes, &case.expected_writes, case.description); + } + } - for expected_name in &case.expected_reads { - assert!( - read_names.contains(&(*expected_name).to_string()), - "Case '{}' failed: expected to find read '{}'", - case.description, - expected_name - ); + mod is_used_in_expression_context_tests { + use super::*; + use biome_js_syntax::binding_ext::AnyJsIdentifierBinding; + + fn extract_all_nodes(code: &str) -> Vec { + let parsed = parse_ts(code); + let root = parsed.syntax(); + + let mut nodes = vec![]; + + for descendant in root.descendants() { + // 1) Skip the identifier that is the class name (e.g. `Test` in `class Test {}`) + if AnyJsIdentifierBinding::can_cast(descendant.kind()) + && let Some(parent) = descendant.parent() + && JsClassDeclaration::can_cast(parent.kind()) + { + continue; + } + + // Try to cast the node itself + if let Some(node) = AnyCandidateForUsedInExpressionNode::cast_ref(&descendant) { + nodes.push(node); + } + + // If this is an assignment, also include LHS + if let Some(assign_expr) = JsAssignmentExpression::cast_ref(&descendant) { + if let Ok(lhs) = assign_expr.left() + && let Some(node) = + AnyCandidateForUsedInExpressionNode::cast_ref(lhs.syntax()) + { + nodes.push(node.clone()); + } + + if let Ok(rhs) = assign_expr.right() + && let Some(node) = + AnyCandidateForUsedInExpressionNode::cast_ref(rhs.syntax()) + { + nodes.push(node.clone()); + } + } } - for expected_name in &case.expected_writes { + nodes + } + + struct TestCase<'a> { + description: &'a str, + code: &'a str, + expected: Vec<(&'a str, bool)>, // (member name, is_meaningful_read) + } + + fn run_test_cases(cases: &[TestCase]) { + for case in cases { + let nodes = extract_all_nodes(case.code); assert!( - write_names.contains(&(*expected_name).to_string()), - "Case '{}' failed: expected to find write '{}'", - case.description, - expected_name + !nodes.is_empty(), + "No match found for test case: '{}'", + case.description + ); + + // Ensure the number of nodes matches expected + assert_eq!( + nodes.len(), + case.expected.len(), + "Number of nodes does not match expected for test case: '{}'", + case.description ); + + for (node, (expected_name, expected_access_kind)) in + nodes.iter().zip(&case.expected) + { + let meaningful_node = + AnyCandidateForUsedInExpressionNode::cast_ref(node.syntax()) + .expect("Failed to cast node to AnyMeaningfulReadNode"); + + // Compare node name + let node_name = meaningful_node.to_trimmed_text(); + assert_eq!( + &node_name, expected_name, + "Node name mismatch for test case: '{}'", + case.description + ); + + // Compare is_meaningful_read + let actual_meaningful = is_used_in_expression_context(&meaningful_node); + assert_eq!( + actual_meaningful, *expected_access_kind, + "Meaningful read mismatch for node '{}' in test case: '{}'", + expected_name, case.description + ); + } } } + + #[test] + fn test_is_used_in_expression_contexts() { + let cases = [ + TestCase { + description: "return statement", + code: r#"class Test {method() { return this.x; }}"#, + expected: vec![("this.x", true)], + }, + TestCase { + description: "call arguments", + code: r#"class Test { method() { foo(this.y); } }"#, + expected: vec![("this.y", true)], + }, + TestCase { + description: "conditional expression", + code: r#"class Test { method() { const a = this.z ? 1 : 2; } }"#, + expected: vec![("a", false), ("this.z", true)], + }, + TestCase { + description: "logical expression", + code: r#"class Test { method() { const a = this.a && this.b; } }"#, + expected: vec![("a", false), ("this.a", true), ("this.b", true)], + }, + TestCase { + description: "throw statement", + code: r#"class Test { method() { throw this.err; } }"#, + expected: vec![("this.err", true)], + }, + TestCase { + description: "await expression", + code: r#"class Test { async method() { await this.promise; } }"#, + expected: vec![("this.promise", true)], + }, + TestCase { + description: "yield expression", + code: r#"class Test { *method() { yield this.gen; } }"#, + expected: vec![("this.gen", true)], + }, + TestCase { + description: "unary expression", + code: r#"class Test { method() { -this.num; } }"#, + expected: vec![("this.num", true)], + }, + TestCase { + description: "template expression", + code: r#"class Test { method() { `${this.str}`; } }"#, + expected: vec![("this.str", true)], + }, + TestCase { + description: "call expression callee", + code: r#"class Test { method() { this.func(); } }"#, + expected: vec![("this.func", true)], + }, + TestCase { + description: "new expression", + code: r#"class Test { method() { new this.ClassName(); } }"#, + expected: vec![("this.ClassName", true)], + }, + TestCase { + description: "if statement", + code: r#"class Test { method() { if(this.cond) {} } }"#, + expected: vec![("this.cond", true)], + }, + TestCase { + description: "switch statement", + code: r#"class Test { method() { switch(this.val) {} } }"#, + expected: vec![("this.val", true)], + }, + TestCase { + description: "for statement", + code: r#"class Test { method() { for(this.i = 0; this.i < 10; this.i++) {} } }"#, // First this.i = 0 is a write, so not a match at all + expected: vec![("this.i", true), ("this.i++", true)], + }, + TestCase { + description: "binary expression", + code: r#"class Test { method() { const sum = this.a + this.b; } }"#, + expected: vec![("sum", false), ("this.a", true), ("this.b", true)], + }, + TestCase { + description: "binary expression nested parenthesis", + code: r#"class Test { method() { const sum = (((this.a + ((this.b * 2))))); } }"#, + expected: vec![("sum", false), ("this.a", true), ("this.b", true)], + }, + TestCase { + description: "nested logical and conditional expressions", + code: r#"class Test { method() { const val = foo(this.a && (this.b ? this.c : 7)); } }"#, + expected: vec![ + ("val", false), + ("this.a", true), + ("this.b", true), + ("this.c", true), + ], + }, + ]; + + run_test_cases(&cases); + } } }