fix(ingestion): credit the receiver of an unresolved method call - #2157
Closed
nickbrus wants to merge 1 commit into
Closed
fix(ingestion): credit the receiver of an unresolved method call#2157nickbrus wants to merge 1 commit into
nickbrus wants to merge 1 commit into
Conversation
`STAGES.forEach(...)` resolves on the member -- `forEach`, an unresolvable builtin -- and so produced no edge at all. `STAGES` was left with zero inbound edges, and the dead-code analyzer, which credits a symbol only on an inbound edge, reported a module-level constant used four times as unused. The same shape covers every module-level table consumed through a builtin: `ROUTES.map(...)`, `DEFAULTS.timeout`, `SUITES.filter(...)`. This is the JS/TS-shaped half of the gap `python_local_refs` closes for Python. It is a partial mitigation, not a fix -- see below. Two limits keep it from guessing: * Only when the member resolved to nothing. `Defaults.timeout()` where `timeout` is a same-file method already puts an edge inside the receiver, so crediting the receiver again would mint an edge for every qualified property read. This is what keeps `test_plain_member_access_is_not_a_reference` green. * Only same-file receivers. An imported name already carries an `imports` edge, and a bare receiver matching an unrelated symbol in another file would be a guess. Emitted as `references`, not `calls`: naming a value is not executing it. `references` is in SYMBOL_USE_EDGE_TYPES so dead code counts it, and outside EXECUTION_EDGE_TYPES so call graphs, flow analysis and the inferred test map are unchanged. Origin `same_file` at 0.95 keeps the one-confidence-per-origin invariant. Measured on 0.48.0 with the tsconfig JSONC fix applied, over a 1,275-file TS/JS repo: unused_internal 462 -> 333, and 9 of 9 hand-verified truly-dead symbols stayed flagged -- 129 rows removed, none of them true. That was the broader rule (credit every same-file receiver); the narrower rule shipped here emits a subset, so read 129 as an upper bound. On the file that motivated it, both rules clear the same five symbols. What this does NOT fix: the category is still mostly wrong. Precision on unused_internal in that repo moves 1.9% -> 2.7%, because arguments and template interpolations, not receivers, are how constants are mostly used. The real fix is an identifier-reference pass -- the JS/TS analogue of python_local_refs, crediting a name in an argument list, a template substitution, a subscript, a JSX expression or a return position -- which is a per-language job in the tree-sitter query layer and much larger than this.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
STAGES.forEach(...)resolves on the member —forEach, an unresolvable builtin — and so produced no edge at all, leavingSTAGESwith zero inbound edges and the dead-code analyzer calling a module-level constant used four times unused.referencesedge only when the member resolved to nothing, and only when the receiver names a same-file symbol.Where it sits relative to what's already here
python_local_refscloses exactly this gap for Python — a top-level symbol referenced within its own module in a non-call position carries no graph edge. There is no JS/TS equivalent, and the shape is not Python-specific: a module-level table consumed through a builtin (ROUTES.map(...),SUITES.filter(...),DEFAULTS.timeout) is invisible in every language.This is one narrow slice of that, at the resolver layer rather than per language.
The two limits, and why
Only when the member resolved to nothing.
Defaults.timeout()wheretimeoutis a same-file method already puts an edge inside the receiver, and the receiver is reachable from it. Crediting it again would mint an edge for every qualified property read — which is precisely whatTestKotlinCallableReferences::test_plain_member_access_is_not_a_referenceguards against. That test stays green, unmodified.Only same-file receivers. An imported name already carries an
importsedge, and a bare receiver matching an unrelated symbol in another file would be a guess, not a resolution.referencesrather thancalls: naming a value is not executing it. It is inSYMBOL_USE_EDGE_TYPESso dead code counts it, and outsideEXECUTION_EDGE_TYPESso call graphs, flow analysis and the inferred test map are unaffected. Originsame_fileat 0.95 keeps the one-confidence-per-origin invariant.Measured
On 0.48.0 with the tsconfig JSONC fix applied (#2156), over a 1,275-file TS/JS + Node repo:
unused_internal, beforeunused_internal, after129 rows removed, none of them true: a control set of 9 hand-verified genuinely-dead symbols (built before the run, not after) stayed flagged 9 of 9.
Two honest caveats on that table:
What the real fix is
An identifier-reference pass: the JS/TS analogue of
python_local_refs, crediting a name appearing in an argument list, a template substitution, a subscript, a property base, a JSX expression and a return position. That is a per-language job in the tree-sitter query layer touchingparser.py,language_configs.pyand the graph builder, across ~20 languages — much larger than this, and guessing at it without a per-language corpus would trade a category that is honestly noisy for one that is quietly wrong. Happy to open an issue for it if that is useful.Test Plan
TestReceiverIsUsedintests/unit/ingestion/test_call_resolver_strategies.py— 8 cases, all through the real parser rather than spies:same_file/references;SYMBOL_USE_EDGE_TYPESand not inEXECUTION_EDGE_TYPES;console— a receiver naming nothing local — mints nothing;Defaults.timeout()) does not credit its receiver again.Five of the eight fail on
mainbefore the change; the three negatives pass in both states, which is what makes them controls.pytest tests/unit/ingestion tests/unit/dead_code tests/unit/analysis) — failure set identical tomainon the same machine, and +8 passingruff checkon both touched files)Checklist