Skip to content

Commit c7d4cec

Browse files
feat(biome-js-analyze): add is_meaningful_read to semantic class reads
1 parent c14b12d commit c7d4cec

File tree

1 file changed

+79
-68
lines changed

1 file changed

+79
-68
lines changed

crates/biome_js_analyze/src/services/semantic_class.rs

Lines changed: 79 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,36 @@ where
126126
}
127127
}
128128

129+
/// Represents how a class member is accessed within the code.
130+
/// Variants:
131+
///
132+
/// - `Write`:
133+
/// The member is being assigned to or mutated.
134+
/// Example: `this.count = 10;`
135+
/// This indicates the member’s value/state changes at this point.
136+
///
137+
/// - `MeaningfulRead`:
138+
/// The member’s value is retrieved and used in a way that affects program logic.
139+
/// Example: `if (this.enabled) { ... }` or `let x = this.value + 1;`
140+
/// These reads influence control flow or computation.
141+
///
142+
/// - `TrivialRead`:
143+
/// The member is accessed, but its value is not used in a way that
144+
/// meaningfully affects logic.
145+
/// Example: `this.value;` as a standalone expression, or a read that is optimized away.
146+
/// This is mostly for distinguishing "dead reads" from truly meaningful ones.
147+
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
148+
enum AccessKind {
149+
Write,
150+
MeaningfulRead,
151+
TrivialRead,
152+
}
153+
129154
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
130155
pub struct ClassMemberReference {
131156
pub name: Text,
132157
pub range: TextRange,
133-
/// Indicates if the read is meaningful (e.g. used in an expression) or not (e.g. part of a destructuring assignment).
134-
/// `None` if not applicable (e.g. for write references).
135-
pub is_meaningful_read: Option<bool>,
158+
pub access_kind: AccessKind,
136159
}
137160

138161
#[derive(Debug, Clone, Eq, PartialEq, Default)]
@@ -359,9 +382,9 @@ impl ThisScopeReferences {
359382
ClassMemberReference {
360383
name: id.to_trimmed_text().clone(),
361384
range: id.syntax().text_trimmed_range(),
362-
is_meaningful_read: Some(is_used_in_expression_context(
385+
access_kind: get_read_access_kind(
363386
&AnyCandidateForUsedInExpressionNode::from(id),
364-
)),
387+
),
365388
}
366389
})
367390
})
@@ -439,8 +462,7 @@ impl ThisPatternResolver {
439462
Self::extract_this_member_reference(
440463
assignment.as_js_static_member_assignment(),
441464
scoped_this_references,
442-
// It is a write reference, so not applicable for meaningful read
443-
None,
465+
AccessKind::Write,
444466
)
445467
})
446468
}
@@ -456,8 +478,7 @@ impl ThisPatternResolver {
456478
Self::extract_this_member_reference(
457479
assignment.as_js_static_member_assignment(),
458480
scoped_this_references,
459-
// It is a write reference, so not applicable for meaningful read
460-
None,
481+
AccessKind::Write,
461482
)
462483
})
463484
} else {
@@ -486,8 +507,7 @@ impl ThisPatternResolver {
486507
return Self::extract_this_member_reference(
487508
rest_params.target().ok()?.as_js_static_member_assignment(),
488509
scoped_this_references,
489-
// It is a write reference, so not applicable for meaningful read
490-
None,
510+
AccessKind::Write,
491511
);
492512
}
493513
if let Some(property) = prop
@@ -503,8 +523,7 @@ impl ThisPatternResolver {
503523
.as_any_js_assignment()?
504524
.as_js_static_member_assignment(),
505525
scoped_this_references,
506-
// It is a write reference, so not applicable for meaningful read
507-
None,
526+
AccessKind::Write,
508527
);
509528
}
510529
None
@@ -523,7 +542,7 @@ impl ThisPatternResolver {
523542
fn extract_this_member_reference(
524543
operand: Option<&JsStaticMemberAssignment>,
525544
scoped_this_references: &[FunctionThisReferences],
526-
is_meaningful_read: Option<bool>,
545+
access_kind: AccessKind,
527546
) -> Option<ClassMemberReference> {
528547
operand.and_then(|assignment| {
529548
if let Ok(object) = assignment.object()
@@ -535,15 +554,15 @@ impl ThisPatternResolver {
535554
.map(|name| ClassMemberReference {
536555
name: name.to_trimmed_text(),
537556
range: name.syntax().text_trimmed_range(),
538-
is_meaningful_read,
557+
access_kind: access_kind.clone(),
539558
})
540559
.or_else(|| {
541560
member
542561
.as_js_private_name()
543562
.map(|private_name| ClassMemberReference {
544563
name: private_name.to_trimmed_text(),
545564
range: private_name.syntax().text_trimmed_range(),
546-
is_meaningful_read,
565+
access_kind,
547566
})
548567
})
549568
})
@@ -637,9 +656,7 @@ fn handle_object_binding_pattern(
637656
reads.insert(ClassMemberReference {
638657
name: declarator.clone().to_trimmed_text(),
639658
range: declarator.clone().syntax().text_trimmed_range(),
640-
is_meaningful_read: Some(is_used_in_expression_context(
641-
&declarator.clone().into(),
642-
)),
659+
access_kind: get_read_access_kind(&declarator.clone().into()),
643660
});
644661
}
645662
}
@@ -675,7 +692,7 @@ fn handle_static_member_expression(
675692
reads.insert(ClassMemberReference {
676693
name: member.to_trimmed_text(),
677694
range: member.syntax().text_trimmed_range(),
678-
is_meaningful_read: Some(is_used_in_expression_context(&static_member.into())),
695+
access_kind: get_read_access_kind(&static_member.into()),
679696
});
680697
}
681698
}
@@ -721,8 +738,7 @@ fn handle_assignment_expression(
721738
&& let Some(name) = ThisPatternResolver::extract_this_member_reference(
722739
operand.as_js_static_member_assignment(),
723740
scoped_this_references,
724-
// Nodes inside assignment expressions are considered meaningful reads e.g. this.x += 1;
725-
Some(true),
741+
AccessKind::MeaningfulRead,
726742
)
727743
{
728744
reads.insert(name.clone());
@@ -749,8 +765,7 @@ fn handle_assignment_expression(
749765
&& let Some(name) = ThisPatternResolver::extract_this_member_reference(
750766
assignment.as_js_static_member_assignment(),
751767
scoped_this_references,
752-
// It is a write reference, so not applicable for meaningful read
753-
None,
768+
AccessKind::Write,
754769
)
755770
{
756771
writes.insert(name.clone());
@@ -787,19 +802,15 @@ fn handle_pre_or_post_update_expression(
787802
&& let Some(name) = ThisPatternResolver::extract_this_member_reference(
788803
operand.as_js_static_member_assignment(),
789804
scoped_this_references,
790-
None,
805+
AccessKind::Write,
791806
)
792807
{
793-
writes.insert(ClassMemberReference {
794-
name: name.name.clone(),
795-
range: name.range,
796-
is_meaningful_read: None,
797-
});
808+
writes.insert(name.clone());
798809
reads.insert(ClassMemberReference {
799810
name: name.name,
800811
range: name.range,
801-
is_meaningful_read: Some(is_used_in_expression_context(
802-
&AnyCandidateForUsedInExpressionNode::from(js_update_expression.clone()),
812+
access_kind: get_read_access_kind(&AnyCandidateForUsedInExpressionNode::from(
813+
js_update_expression.clone(),
803814
)),
804815
});
805816
}
@@ -842,8 +853,8 @@ fn collect_class_property_reads_from_static_member(
842853
reads.insert(ClassMemberReference {
843854
name,
844855
range: static_member.syntax().text_trimmed_range(),
845-
is_meaningful_read: Some(is_used_in_expression_context(
846-
&AnyCandidateForUsedInExpressionNode::from(static_member.clone()),
856+
access_kind: get_read_access_kind(&AnyCandidateForUsedInExpressionNode::from(
857+
static_member.clone(),
847858
)),
848859
});
849860
}
@@ -872,45 +883,45 @@ fn is_within_scope_without_shadowing(
872883
false
873884
}
874885

886+
/// Determines the kind of read access for a given node.
887+
fn get_read_access_kind(node: &AnyCandidateForUsedInExpressionNode) -> AccessKind {
888+
if is_used_in_expression_context(node) {
889+
AccessKind::MeaningfulRead
890+
} else {
891+
AccessKind::TrivialRead
892+
}
893+
}
894+
875895
/// Checks if the given node is used in an expression context
876896
/// (e.g., return, call arguments, conditionals, binary expressions).
877897
/// Not limited to `this` references. Can be used for any node, but requires more work e.g.
878898
/// Returns `true` if the read is meaningful, `false` otherwise.
879899
fn is_used_in_expression_context(node: &AnyCandidateForUsedInExpressionNode) -> bool {
880-
let mut current: JsSyntaxNode =
881-
if let Some(expression) = AnyJsExpression::cast(node.syntax().clone()) {
882-
expression.syntax().clone() // get JsSyntaxNode
883-
} else {
884-
node.syntax().clone() // fallback to the node itself
885-
};
886-
887-
// Limit the number of parent traversals to avoid deep recursion
888-
for _ in 0..8 {
889-
if let Some(parent) = current.parent() {
890-
match parent.kind() {
891-
JsSyntaxKind::JS_RETURN_STATEMENT
892-
| JsSyntaxKind::JS_CALL_ARGUMENTS
893-
| JsSyntaxKind::JS_CONDITIONAL_EXPRESSION
894-
| JsSyntaxKind::JS_LOGICAL_EXPRESSION
895-
| JsSyntaxKind::JS_THROW_STATEMENT
896-
| JsSyntaxKind::JS_AWAIT_EXPRESSION
897-
| JsSyntaxKind::JS_YIELD_EXPRESSION
898-
| JsSyntaxKind::JS_UNARY_EXPRESSION
899-
| JsSyntaxKind::JS_TEMPLATE_EXPRESSION
900-
| JsSyntaxKind::JS_CALL_EXPRESSION // (callee)
901-
| JsSyntaxKind::JS_NEW_EXPRESSION
902-
| JsSyntaxKind::JS_IF_STATEMENT
903-
| JsSyntaxKind::JS_SWITCH_STATEMENT
904-
| JsSyntaxKind::JS_FOR_STATEMENT
905-
| JsSyntaxKind::JS_FOR_IN_STATEMENT
906-
| JsSyntaxKind::JS_FOR_OF_STATEMENT
907-
| JsSyntaxKind::JS_BINARY_EXPRESSION => return true,
908-
_ => current = parent,
909-
}
910-
} else {
911-
break;
900+
let mut current = node.syntax().clone();
901+
902+
if let Some(parent) = current.parent() {
903+
match parent.kind() {
904+
JsSyntaxKind::JS_RETURN_STATEMENT
905+
| JsSyntaxKind::JS_CALL_ARGUMENTS
906+
| JsSyntaxKind::JS_CONDITIONAL_EXPRESSION
907+
| JsSyntaxKind::JS_LOGICAL_EXPRESSION
908+
| JsSyntaxKind::JS_THROW_STATEMENT
909+
| JsSyntaxKind::JS_AWAIT_EXPRESSION
910+
| JsSyntaxKind::JS_YIELD_EXPRESSION
911+
| JsSyntaxKind::JS_UNARY_EXPRESSION
912+
| JsSyntaxKind::JS_TEMPLATE_EXPRESSION
913+
| JsSyntaxKind::JS_CALL_EXPRESSION // (callee)
914+
| JsSyntaxKind::JS_NEW_EXPRESSION
915+
| JsSyntaxKind::JS_IF_STATEMENT
916+
| JsSyntaxKind::JS_SWITCH_STATEMENT
917+
| JsSyntaxKind::JS_FOR_STATEMENT
918+
| JsSyntaxKind::JS_FOR_IN_STATEMENT
919+
| JsSyntaxKind::JS_FOR_OF_STATEMENT
920+
| JsSyntaxKind::JS_BINARY_EXPRESSION => return true,
921+
_ => current = parent,
912922
}
913923
}
924+
914925
false
915926
}
916927

@@ -945,7 +956,7 @@ mod tests {
945956
});
946957

947958
assert_eq!(
948-
found.is_meaningful_read, *expected_meaningful,
959+
found.access_kind, *expected_meaningful,
949960
"Case '{}' failed: read '{}' meaningful mismatch",
950961
description, expected_name
951962
);
@@ -969,7 +980,7 @@ mod tests {
969980
});
970981

971982
assert_eq!(
972-
found.is_meaningful_read, *expected_meaningful,
983+
found.access_kind, *expected_meaningful,
973984
"Case '{}' failed: write '{}' meaningful mismatch",
974985
description, expected_name
975986
);

0 commit comments

Comments
 (0)