Skip to content

fix(extract): scope this.* binding keys per enclosing class#1833

Merged
BartWaardenburg merged 1 commit into
mainfrom
fix/issue-1821-per-class-this-scoping
Jul 12, 2026
Merged

fix(extract): scope this.* binding keys per enclosing class#1833
BartWaardenburg merged 1 commit into
mainfrom
fix/issue-1821-per-class-this-scoping

Conversation

@BartWaardenburg

Copy link
Copy Markdown
Collaborator

binding_target_names is module-flat with last-write-wins, so two classes in one module with a same-named field collided on the this.<field> key: only the class declared last credited its dependency's members, and reversing declaration order flipped which class's members were falsely reported. This affected public fields, param properties, and (since #1822) #-private fields identically.

this.-rooted binding keys and access/whole-object/iteration-receiver spellings are now qualified with a per-class scope id during the walk and stripped back to plain this. as the last finalize step, so no persisted spelling changes and no downstream consumer (core self-access, heritage filter, Angular output crediting, SFC template filter) sees the qualifier. Bare this and module-level this are never qualified. CACHE_VERSION 233 -> 234.

For the #1821 reporter: with #1822 (private-field receivers) plus this change, every row of the issue's isolation matrix is covered. Two rows were misattributed in the original analysis: the param-property row and the Gap 2 alias-hop rows were failing because of this same-file field-name collision, not because of missing shape support; local and imported alias hops credit correctly in isolation on 3.3.0.

Note the error direction: unlike #1822 this is not purely additive. Correcting last-write-wins also removes accidental credit, so a member that was only credited through a sibling class's colliding field binding can surface as a new, correct finding. Benchmark validation across ten real-world projects showed exactly one such change: NextNodeServer.revalidate in next.js, verified in source as a genuine collision artifact (an unrelated NextCustomServer getter named server was borrowing NextServer's server?: NextNodeServer binding); every other project is byte-identical on member findings and all non-member issue types are byte-identical everywhere.

Fixes #1821

Tests: extract unit tests pinning both colliding classes credited in both declaration orders (public and #-private variants) plus a no-qualifier-in-output assertion, and a cross-module integration fixture mirroring the issue's privParam collision with dead control members asserted to stay flagged.

`binding_target_names` is module-flat, so two classes in one module that
declare a same-named field collided on the `this.<field>` key via
last-write-wins: `constructor(private dep: DepA)` in one class and
`readonly dep = new DepB()` in another both wrote `this.dep`, so every
`this.dep.*` access in the module resolved against the surviving target
only, and the losing class's dep members were falsely reported as
unused-class-member cross-module. This is the collision behind the issue's
privParam row (order-dependent: reversing the class declarations flipped
which class's members were flagged).

Qualify `this.`-rooted binding keys and receiver spellings with an internal
per-class scope id during the walk (`this@<id>.<field>`), pushed and popped
alongside the class-super / class-context stacks in `visit_class`. Both the
insert side and the access / whole-object / iteration-receiver read sides
are qualified consistently, so the longest-prefix resolution and the typed
property-hop expansion keep working within one class scope. The qualifier
is an extraction-only disambiguator: `strip_this_scope_qualifiers` runs last
in `finalize_resolution_phase`, after every resolution pass, and rewrites
every `this@<id>.` spelling back to `this.` across `member_accesses` and
`whole_object_uses` before any spelling reaches `ModuleInfo`, so no
persisted spelling and no downstream consumer (core self-access `== "this"`,
heritage `!= "this"`, unused-component-output `this.<name>`, SFC template
`starts_with("this.")`) ever sees it. Bare `this` (the per-file self-access
key) and module-level `this` are never qualified.

Correcting last-write-wins removes accidental credit, so a member that
exists on both colliding classes but is called on only one can surface as a
new true-positive finding. Validated on ten real-world projects: one such
finding (`NextNodeServer.revalidate` in next.js, where a `this.server`
getter on an unrelated class had been borrowing a sibling class's
`server: NextNodeServer` field binding) and zero non-member drift.

CACHE_VERSION 233 -> 234 (the emitted member_accesses change for modules
with same-named fields across classes; warm 233 caches keep the collision).

Fixes #1821
@BartWaardenburg BartWaardenburg merged commit 2c21d54 into main Jul 12, 2026
19 checks passed
@BartWaardenburg BartWaardenburg deleted the fix/issue-1821-per-class-this-scoping branch July 12, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

unused-class-members: member access through private class fields is not credited cross-module

1 participant