mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(semantic): extend MemberWriteTarget to cover all property modification patterns (#21205)
## Summary - Extend `MemberWriteTarget` flag to cover compound assignments (`+=`), update expressions (`++/--`), and `delete` — previously only simple `=` assignments were covered - Add `visit_unary_expression` override to set `Write` for `delete` on member expressions - Change `is_write_only()` to `is_write()` in `visit_member_expression` so all write contexts trigger the flag - Fix `MemberWriteTarget` leaking into conditional test positions (`(a ? x : y).foo = 1` was incorrectly marking `a`) - Add snapshot test covering compound assignments, delete, and conditional expressions - Update doc comments to reflect broader scope ## Test plan - [x] New snapshot test `member-write-target.js` covering compound assignments, delete, conditional test/consequent/alternate - [x] Existing semantic snapshot tests updated - [x] `just ready` passes 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
1 parent
2338e28f59
commit
6dd061c70e
6 files changed
+158
-17
No files matched your search
@@ -1077,7 +1077,12 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> {
|
||||
|
||||
#[cfg(feature = "cfg")]
|
||||
self.record_ast_nodes();
|
||||
// The test of a conditional is always a pure read — strip MemberWriteTarget
|
||||
// so that `(a ? x : y).foo = 1` doesn't mark `a` as a property-write target.
|
||||
let saved_flags = self.current_reference_flags;
|
||||
self.current_reference_flags -= ReferenceFlags::MemberWriteTarget;
|
||||
self.visit_expression(&expr.test);
|
||||
self.current_reference_flags = saved_flags;
|
||||
#[cfg(feature = "cfg")]
|
||||
let test_node_id = self.retrieve_recorded_ast_node();
|
||||
|
||||
@@ -2027,14 +2032,27 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> {
|
||||
self.leave_node(kind);
|
||||
}
|
||||
|
||||
fn visit_unary_expression(&mut self, it: &UnaryExpression<'a>) {
|
||||
let kind = AstKind::UnaryExpression(self.alloc(it));
|
||||
self.enter_node(kind);
|
||||
// `delete a.foo` — the argument is in a property-write context.
|
||||
// Set Write so `visit_member_expression` can detect it and mark MemberWriteTarget.
|
||||
// Only for member expressions — `delete x` (bare identifier in sloppy mode)
|
||||
// is not a property modification.
|
||||
if it.operator == UnaryOperator::Delete && it.argument.is_member_expression() {
|
||||
self.current_reference_flags = ReferenceFlags::Write;
|
||||
}
|
||||
self.visit_expression(&it.argument);
|
||||
self.leave_node(kind);
|
||||
}
|
||||
|
||||
fn visit_member_expression(&mut self, it: &MemberExpression<'a>) {
|
||||
// A.B = 1;
|
||||
// ^^^ Can't treat A as a Write reference since it's A's property(B) that changes.
|
||||
// For write-only references (simple `=` assignment), mark as MemberWriteTarget
|
||||
// so the minifier can identify property-write-only references.
|
||||
// Compound assignments (`+=`) and update expressions (`++`) have Read|Write flags,
|
||||
// so `is_write_only()` is false and they correctly skip this branch.
|
||||
if self.current_reference_flags.is_write_only() {
|
||||
// When the member expression is in any write context (simple `=`, compound `+=`,
|
||||
// update `++`, `delete`, for-in/of), mark as MemberWriteTarget so downstream
|
||||
// consumers can identify property-modification-only references.
|
||||
if self.current_reference_flags.is_write() {
|
||||
self.current_reference_flags = ReferenceFlags::Read | ReferenceFlags::MemberWriteTarget;
|
||||
} else {
|
||||
self.current_reference_flags -= ReferenceFlags::Write;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// MemberWriteTarget: compound assignment
|
||||
let a = {};
|
||||
a.x += 1;
|
||||
a.x -= 1;
|
||||
a.x &&= 1;
|
||||
|
||||
// MemberWriteTarget: delete
|
||||
let b = {};
|
||||
delete b.x;
|
||||
|
||||
// No MemberWriteTarget: conditional test position
|
||||
let c = {};
|
||||
(c ? 0 : 1).foo = 1;
|
||||
|
||||
// MemberWriteTarget: conditional consequent/alternate
|
||||
let d = {};
|
||||
let e = {};
|
||||
(true ? d : e).foo = 1;
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
---
|
||||
source: crates/oxc_semantic/tests/main.rs
|
||||
input_file: crates/oxc_semantic/tests/fixtures/oxc/js/assignments/member-write-target.js
|
||||
---
|
||||
[
|
||||
{
|
||||
"children": [],
|
||||
"flags": "ScopeFlags(Top)",
|
||||
"id": 0,
|
||||
"node": "Program",
|
||||
"symbols": [
|
||||
{
|
||||
"flags": "SymbolFlags(BlockScopedVariable)",
|
||||
"id": 0,
|
||||
"name": "a",
|
||||
"node": "VariableDeclarator(a)",
|
||||
"references": [
|
||||
{
|
||||
"flags": "ReferenceFlags(Read | MemberWriteTarget)",
|
||||
"id": 0,
|
||||
"name": "a",
|
||||
"node_id": 8,
|
||||
"scope_id": 0
|
||||
},
|
||||
{
|
||||
"flags": "ReferenceFlags(Read | MemberWriteTarget)",
|
||||
"id": 1,
|
||||
"name": "a",
|
||||
"node_id": 14,
|
||||
"scope_id": 0
|
||||
},
|
||||
{
|
||||
"flags": "ReferenceFlags(Read | MemberWriteTarget)",
|
||||
"id": 2,
|
||||
"name": "a",
|
||||
"node_id": 20,
|
||||
"scope_id": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"flags": "SymbolFlags(BlockScopedVariable)",
|
||||
"id": 1,
|
||||
"name": "b",
|
||||
"node": "VariableDeclarator(b)",
|
||||
"references": [
|
||||
{
|
||||
"flags": "ReferenceFlags(Read | MemberWriteTarget)",
|
||||
"id": 3,
|
||||
"name": "b",
|
||||
"node_id": 30,
|
||||
"scope_id": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"flags": "SymbolFlags(BlockScopedVariable)",
|
||||
"id": 2,
|
||||
"name": "c",
|
||||
"node": "VariableDeclarator(c)",
|
||||
"references": [
|
||||
{
|
||||
"flags": "ReferenceFlags(Read)",
|
||||
"id": 4,
|
||||
"name": "c",
|
||||
"node_id": 41,
|
||||
"scope_id": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"flags": "SymbolFlags(BlockScopedVariable)",
|
||||
"id": 3,
|
||||
"name": "d",
|
||||
"node": "VariableDeclarator(d)",
|
||||
"references": [
|
||||
{
|
||||
"flags": "ReferenceFlags(Read | MemberWriteTarget)",
|
||||
"id": 5,
|
||||
"name": "d",
|
||||
"node_id": 60,
|
||||
"scope_id": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"flags": "SymbolFlags(BlockScopedVariable)",
|
||||
"id": 4,
|
||||
"name": "e",
|
||||
"node": "VariableDeclarator(e)",
|
||||
"references": [
|
||||
{
|
||||
"flags": "ReferenceFlags(Read)",
|
||||
"id": 6,
|
||||
"name": "e",
|
||||
"node_id": 61,
|
||||
"scope_id": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
+4
-4
@@ -52,28 +52,28 @@ input_file: crates/oxc_semantic/tests/fixtures/oxc/js/assignments/update-express
|
||||
"node": "VariableDeclarator(obj)",
|
||||
"references": [
|
||||
{
|
||||
"flags": "ReferenceFlags(Read)",
|
||||
"flags": "ReferenceFlags(Read | MemberWriteTarget)",
|
||||
"id": 4,
|
||||
"name": "obj",
|
||||
"node_id": 27,
|
||||
"scope_id": 0
|
||||
},
|
||||
{
|
||||
"flags": "ReferenceFlags(Read)",
|
||||
"flags": "ReferenceFlags(Read | MemberWriteTarget)",
|
||||
"id": 5,
|
||||
"name": "obj",
|
||||
"node_id": 32,
|
||||
"scope_id": 0
|
||||
},
|
||||
{
|
||||
"flags": "ReferenceFlags(Read)",
|
||||
"flags": "ReferenceFlags(Read | MemberWriteTarget)",
|
||||
"id": 6,
|
||||
"name": "obj",
|
||||
"node_id": 37,
|
||||
"scope_id": 0
|
||||
},
|
||||
{
|
||||
"flags": "ReferenceFlags(Read)",
|
||||
"flags": "ReferenceFlags(Read | MemberWriteTarget)",
|
||||
"id": 7,
|
||||
"name": "obj",
|
||||
"node_id": 42,
|
||||
|
||||
@@ -44,7 +44,7 @@ input_file: crates/oxc_semantic/tests/fixtures/oxc/ts/assignments/left-hand.ts
|
||||
"scope_id": 0
|
||||
},
|
||||
{
|
||||
"flags": "ReferenceFlags(Read)",
|
||||
"flags": "ReferenceFlags(Read | MemberWriteTarget)",
|
||||
"id": 4,
|
||||
"name": "Foo",
|
||||
"node_id": 34,
|
||||
|
||||
@@ -117,13 +117,14 @@ bitflags! {
|
||||
/// a type parameter that might shadow it, since type parameters cannot
|
||||
/// have member access.
|
||||
const Namespace = 1 << 4;
|
||||
/// The identifier is read as the object of a member expression in an
|
||||
/// assignment target position. For example, `A` in `A.foo = 1`.
|
||||
/// The identifier is read as the object of a member expression in a
|
||||
/// property modification context. For example, `A` in `A.foo = 1`,
|
||||
/// `A.foo += 1`, `++A.foo`, `delete A.foo`, or `for (A.foo in obj)`.
|
||||
///
|
||||
/// This flag is always combined with [`Read`] (since the identifier is
|
||||
/// read to access the property). It helps the minifier determine if a
|
||||
/// symbol's only reads are property-write targets, enabling dead code
|
||||
/// elimination of patterns like `function A() {} A.foo = 1;`.
|
||||
/// read to access the property). It helps the minifier and linter
|
||||
/// determine if a symbol's only reads are property-modification targets,
|
||||
/// enabling dead code elimination and unused variable detection.
|
||||
///
|
||||
/// [`Read`]: ReferenceFlags::Read
|
||||
const MemberWriteTarget = 1 << 5;
|
||||
@@ -183,8 +184,9 @@ impl ReferenceFlags {
|
||||
self.contains(Self::Read | Self::Write)
|
||||
}
|
||||
|
||||
/// The identifier is read only as the object of a member expression
|
||||
/// in an assignment target position (e.g., `A` in `A.foo = 1`).
|
||||
/// The identifier is read as the object of a member expression
|
||||
/// in a property modification context (e.g., `A` in `A.foo = 1`,
|
||||
/// `A.foo += 1`, `++A.foo`, `delete A.foo`, or `for (A.foo in obj)`).
|
||||
#[inline]
|
||||
pub const fn is_member_write_target(self) -> bool {
|
||||
self.intersects(Self::MemberWriteTarget)
|
||||
|
||||
Reference in new issue
Block a user