Skip to content

feat(semantic): add MemberWriteTarget flag to ReferenceFlags#20772

Merged
graphite-app[bot] merged 1 commit intomainfrom
feat/semantic-member-write-target-flag
Apr 7, 2026
Merged

feat(semantic): add MemberWriteTarget flag to ReferenceFlags#20772
graphite-app[bot] merged 1 commit intomainfrom
feat/semantic-member-write-target-flag

Conversation

@Dunqing
Copy link
Copy Markdown
Member

@Dunqing Dunqing commented Mar 26, 2026

Summary

Add a new MemberWriteTarget bit to ReferenceFlags Store member write target references in a separate FxHashSet<ReferenceId> on Semantic rather than adding a flag to ReferenceFlags.

This marks identifier references read as the object of a member expression in a simple assignment target position (e.g., A in A.foo = 1).

  • Set during visit_member_expression when flags indicate write-only context (simple =, not compound += or update ++)
  • Stored in Semantic::member_write_references (FxHashSet<ReferenceId>)
  • Queryable via Semantic::is_member_write_reference(reference_id) and Semantic::member_write_references()
  • Does not affect existing ReferenceFlags — no changes to the shared bitflags type

Motivation

Distinguishing "reads that exist only to serve a property write" from "real reads" is broadly useful:

Minifier: dead code elimination of property-write-only symbols

function A() {}
A.from = () => {}  // A's only reference is a member write target
// → both can be eliminated when property_write_side_effects: false

Without this info, the minifier sees A as "referenced" and cannot remove the function declaration. With it, the minifier knows all of A's reads are property-write targets and can safely drop them. (See stacked PR #20773 — will be updated to use the new API in a follow-up)

Linter: no_unused_vars improvement

const config = {};
config.debug = true;
config.verbose = false;
// config is never read — only mutated via property writes

Currently config is considered "used" because config.debug = ... creates a read reference. With member write target tracking, no_unused_vars could detect that config's only reads are property-write targets and report it as unused.

Linter: simplify no_param_reassign property detection

no_param_reassign has a complex is_modifying_property() function that manually walks the ancestor chain to detect property modifications like param.foo = 1. With member write target tracking, this can be checked directly on the reference.

Rolldown: replace MemberExprIsWrite traversal state

Rolldown already maintains its own MemberExprIsWrite flag as local traversal state in the AST scanner to detect member expressions in write context. Having this on Semantic means Rolldown can consume it directly instead of re-detecting it during scanning. There's also a TODO in Rolldown's treeshake options to wire property_write_side_effects to oxc_minifier — our stacked PR #20773 provides that.

General: escape analysis for local objects

Any analysis that needs to know "is this object only mutated locally, never passed elsewhere?" benefits from knowing which reads are purely for property writes vs. reads that pass the value to other functions.

Test plan

  • Semantic snapshot tests updated
  • Conformance snapshots updated
  • Clippy clean

🤖 Generated with Claude Code

Copy link
Copy Markdown
Member Author

Dunqing commented Mar 26, 2026


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent changes, fast-track this PR to the front of the merge queue

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions github-actions Bot added A-semantic Area - Semantic A-transformer Area - Transformer / Transpiler C-enhancement Category - New feature or request labels Mar 26, 2026
@Dunqing
Copy link
Copy Markdown
Member Author

Dunqing commented Mar 26, 2026

As the description mentioned, this ReferenceFlags::MemberWriteTarget could be very helpful in many cases and could definitely speed up performance since we don't need to pre-scan or repeatedly access all its references to find the node to check. However, there is also a con, as reference flags have more than one aspect to consider, so it would take time to sync semantics. You can see numerous snapshots changed due to this; I don't think it is hard to correct them, though.

I know our principle for the flags is to have as few as possible so they can be easily maintained, considering the advantage is greater than the disadvantage, so I propose adding it. I am happy to hear your thoughts on this.

@Dunqing Dunqing force-pushed the feat/semantic-member-write-target-flag branch from ae2c858 to 075f2cd Compare March 26, 2026 13:59
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Mar 26, 2026

Merging this PR will not alter performance

✅ 48 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing feat/semantic-member-write-target-flag (0f4f445) with main (ca79960)2

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (2ac7527) during the generation of this report, so ca79960 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Dunqing Dunqing marked this pull request as ready for review March 26, 2026 14:19
@Dunqing
Copy link
Copy Markdown
Member Author

Dunqing commented Mar 27, 2026

I just thought another approach is to add a member_write_references: FxHashSet<ReferenceId> in the Semantic and store any MemberWriteTarget references in it, rather than flagging the reference as MemberWriteTarget. This won't affect existing code, but it would introduce an extra FxHashSet overhead. Anyway, I think it's fine, as it could bring more performance.

@Dunqing
Copy link
Copy Markdown
Member Author

Dunqing commented Mar 30, 2026

I just thought another approach is to add a member_write_references: FxHashSet<ReferenceId> in the Semantic and store any MemberWriteTarget references in it, rather than flagging the reference as MemberWriteTarget. This won't affect existing code, but it would introduce an extra FxHashSet overhead. Anyway, I think it's fine, as it could bring more performance.

2ffb46b Switched to this approach, as it won't cause an enormous semantic data mismatch.

@Dunqing Dunqing force-pushed the feat/semantic-member-write-target-flag branch from 2ffb46b to abd48a1 Compare March 30, 2026 09:52
@Dunqing
Copy link
Copy Markdown
Member Author

Dunqing commented Apr 1, 2026

2ffb46b Switched to this approach, as it won't cause an enormous semantic data mismatch.

Reverted

@Dunqing Dunqing marked this pull request as draft April 1, 2026 02:31
@Dunqing Dunqing marked this pull request as ready for review April 2, 2026 08:56
@Boshen Boshen added the 0-merge Merge with Graphite Merge Queue label Apr 7, 2026
Copy link
Copy Markdown
Member

Boshen commented Apr 7, 2026

Merge activity

)

## Summary

Add a new `MemberWriteTarget` bit to `ReferenceFlags` ~~Store member write target references in a separate `FxHashSet<ReferenceId>` on `Semantic` rather than adding a flag to `ReferenceFlags`.~~

This marks identifier references read as the object of a member expression in a simple assignment target position (e.g., `A` in `A.foo = 1`).

- Set during `visit_member_expression` when flags indicate write-only context (simple `=`, not compound `+=` or update `++`)
- Stored in `Semantic::member_write_references` (`FxHashSet<ReferenceId>`)
- Queryable via `Semantic::is_member_write_reference(reference_id)` and `Semantic::member_write_references()`
- Does not affect existing `ReferenceFlags` — no changes to the shared bitflags type

## Motivation

Distinguishing "reads that exist only to serve a property write" from "real reads" is broadly useful:

### Minifier: dead code elimination of property-write-only symbols

```js
function A() {}
A.from = () => {}  // A's only reference is a member write target
// → both can be eliminated when property_write_side_effects: false
```

Without this info, the minifier sees `A` as "referenced" and cannot remove the function declaration. With it, the minifier knows all of `A`'s reads are property-write targets and can safely drop them. (See stacked PR #20773 — will be updated to use the new API in a follow-up)

### Linter: `no_unused_vars` improvement

```js
const config = {};
config.debug = true;
config.verbose = false;
// config is never read — only mutated via property writes
```

Currently `config` is considered "used" because `config.debug = ...` creates a read reference. With member write target tracking, `no_unused_vars` could detect that `config`'s only reads are property-write targets and report it as unused.

### Linter: simplify `no_param_reassign` property detection

`no_param_reassign` has a complex `is_modifying_property()` function that manually walks the ancestor chain to detect property modifications like `param.foo = 1`. With member write target tracking, this can be checked directly on the reference.

### Rolldown: replace `MemberExprIsWrite` traversal state

Rolldown already maintains its own [`MemberExprIsWrite` flag](https://github.com/rolldown/rolldown/blob/eec7d7320cc991e1efd5ed8264d02c6889a223c3/crates/rolldown/src/ast_scanner/mod.rs#L130-L131) as local traversal state in the AST scanner to [detect member expressions in write context](https://github.com/rolldown/rolldown/blob/eec7d7320cc991e1efd5ed8264d02c6889a223c3/crates/rolldown/src/ast_scanner/impl_visit.rs#L55-L73). Having this on `Semantic` means Rolldown can consume it directly instead of re-detecting it during scanning. There's also a [TODO in Rolldown's treeshake options](https://github.com/rolldown/rolldown/blob/main/crates/rolldown_common/src/inner_bundler_options/types/treeshake.rs#L255-L259) to wire `property_write_side_effects` to oxc_minifier — our stacked PR #20773 provides that.

### General: escape analysis for local objects

Any analysis that needs to know "is this object only mutated locally, never passed elsewhere?" benefits from knowing which reads are purely for property writes vs. reads that pass the value to other functions.

## Test plan

- [x] Semantic snapshot tests updated
- [x] Conformance snapshots updated
- [x] Clippy clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@graphite-app graphite-app Bot force-pushed the feat/semantic-member-write-target-flag branch from 0f4f445 to 3cfe8ed Compare April 7, 2026 06:11
@graphite-app graphite-app Bot merged commit 3cfe8ed into main Apr 7, 2026
25 of 26 checks passed
@graphite-app graphite-app Bot deleted the feat/semantic-member-write-target-flag branch April 7, 2026 06:16
@graphite-app graphite-app Bot removed the 0-merge Merge with Graphite Merge Queue label Apr 7, 2026
Dunqing added a commit that referenced this pull request Apr 10, 2026
…fication patterns

The `MemberWriteTarget` flag (added in #20772) was only set for simple `=`
assignments. This extends it to compound assignments (`+=`), update
expressions (`++/--`), and `delete`, making it useful for any downstream
consumer that needs to detect property-modification-only references.

Changes:
- `visit_member_expression`: change `is_write_only()` to `is_write()` so
  compound and update expressions also trigger the flag
- Add `visit_unary_expression` override: set `Write` for `delete` on member
  expressions (not bare identifiers like `delete x` in sloppy mode)
- Fix `MemberWriteTarget` leaking into conditional test positions
  (`(a ? x : y).foo = 1` was incorrectly marking `a`)
- Update doc comments to reflect broader scope

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
camc314 pushed a commit that referenced this pull request Apr 13, 2026
### 💥 BREAKING CHANGES

- 36cdc31 str: [**BREAKING**] Remove identity `FromIn` impl for `Ident`
(#21251) (overlookmotel)
- 382958a span: [**BREAKING**] Remove re-exports of string types from
`oxc_span` crate (#21246) (overlookmotel)
- c4aedfa str: [**BREAKING**] Add `static_ident!` macro (#21245)
(overlookmotel)

### 🚀 Features

- e7e1aea transformer/typescript: Add `optimize_enums` option for
regular enum inlining (#20539) (Dunqing)
- 679f57f transformer/typescript: Implement const enum inlining and
declaration removal (#20508) (Dunqing)
- 6dd061c semantic: Extend `MemberWriteTarget` to cover all property
modification patterns (#21205) (Dunqing)
- f134e24 minifier: Support `property_write_side_effects` option to drop
unused property assignments (#20773) (Dunqing)
- 75663c0 semantic: Add enum member value evaluation for const enum
support (#20602) (Dunqing)
- 3cfe8ed semantic: Add `MemberWriteTarget` flag to `ReferenceFlags`
(#20772) (Dunqing)

### 🐛 Bug Fixes

- af1a586 transformer/class-properties: Use correct property name when
converting parameter properties (#21268) (Amal Jossy)
- b43250a allocator: Move allocation tracking into `Bump` (#21342)
(overlookmotel)
- 36f505f allocator: `StringBuilder` use `Allocator::alloc_layout`
(#21340) (overlookmotel)
- 7a08a6f allocator: Fix allocation counting in
`Allocator::alloc_concat_strs_array` (#21336) (overlookmotel)
- 2338e28 ecmascript: Treat `this` as potentially having side effects
(#21297) (sapphi-red)
- bd8bd39 allocator: Remove unsafe hacks from `from_raw_parts` methods
(#21283) (overlookmotel)
- 8f4c340 allocator: Remove dangerous pointer const to mut cast (#21279)
(overlookmotel)
- aa9259f parser: Add missing error code for optional param diagnostic
(#21258) (camc314)
- 04b3c2f str: Fix unsound casting const pointers to mut pointers
(#21242) (overlookmotel)
- ceadf6c str: Make `Ident::from_raw` an unsafe function (#21241)
(overlookmotel)
- eab13b3 transformer/decorators: Avoid accessor storage name collisions
(#21106) (Dunqing)
- 07e8a30 transformer/react-refresh: Handle parenthesized variable
initializers (#21047) (camc314)

### ⚡ Performance

- c3ca6f6 allocator: `StringBuilder::from_strs_array_in` check for 0
length earlier (#21338) (overlookmotel)
- c2422bb allocator: `Allocator::alloc_concat_strs_array` check for 0
length earlier (#21337) (overlookmotel)
- 04b0fdc allocator: Mark `Allocator::alloc_layout` as
`#[inline(always)]` (#21335) (overlookmotel)
- 17aee9e allocator: Use `offset_from_unsigned` in
`ChunkFooter::as_raw_parts` (#21280) (overlookmotel)
- 61adedd minifier: Fix O(n²) perf on very many var decls (#21062)
(Gunnlaugur Thor Briem)
- addcd02 napi/parser, linter/plugins: Raw transfer deserializer for
`Vec`s use shift instead of multiply where possible (#21142)
(overlookmotel)
- 3068ded napi/parser, linter/plugins: Shift before add when calculating
positions in raw transfer deserializer (#21141) (overlookmotel)
- eb400b8 napi/parser, linter/plugins: Remove `uint32` buffer view
(#21140) (overlookmotel)
- 2675085 napi/parser: Lazy deserialization use only `Int32Array`
(#21139) (overlookmotel)
- 5b35a53 napi/parser: Deserializing tokens use only `int32` array
(#21138) (overlookmotel)
- f163d10 parser: Tokens raw deserialization use `Int32Array` (#21137)
(overlookmotel)
- 7a86613 linter/plugins: Use `Int32Array`s for tokens and comments
buffers (#21136) (overlookmotel)
- 8c51121 napi/parser, linter/plugins: Raw transfer deserialize `Span`
fields as `i32`s (#21135) (overlookmotel)
- bc1bcdd napi/parser, linter/plugins: Inline trivial raw transfer field
deserializers into node object definitions (#21134) (overlookmotel)
- c0278ab napi/parser, linter/plugins: Use `Int32Array` in raw transfer
deserializer (#21132) (overlookmotel)
- 43482c7 linter/plugins: Use `>>` not `>>>` in binary search loops
(#21129) (overlookmotel)

### 📚 Documentation

- f5e1845 allocator: Upgrade headers in doc comments for `Bump` (#21263)
(overlookmotel)
- 2870174 allocator: Upper case `SAFETY` in comments (#21253)
(overlookmotel)
- 01bc269 str: Reformat `Ident` doc comments (#21240) (overlookmotel)
- dd47359 allocator: Add doc comments for panics and errors (#21230)
(overlookmotel)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-semantic Area - Semantic A-transformer Area - Transformer / Transpiler C-enhancement Category - New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants