fix(resolution): a binding in a module that exports nothing is not a cross-file candidate - #1720
fix(resolution): a binding in a module that exports nothing is not a cross-file candidate#1720bompus wants to merge 6 commits into
Conversation
…cross-file candidate
On vitejs/vite, 157 cross-file `imports` refs — every `import { defineConfig }
from 'vite'` in the playground and the create-vite templates — resolved onto
`playground/ssr-html/test-stacktrace.js::vite`, which is `const vite = await
createServer(...)` at module scope in a file with zero exports.
Neither existing guard can see it. `isLexicallyReachable` returns early for any
candidate that is not a `function`, and the bare-import guard correctly declines
because `vite` IS a workspace member, so the specifier really is project-local.
What is wrong is only which node the name lands on.
A JS/TS file that contains an `import` statement and no export of any form
offers nothing to any other file, so none of its bindings is a candidate for a
cross-file name match. Applied in both name-based strategies: declining in
matchByExactName alone just hands the same target to matchFuzzy, which resolves
a unique candidate on its own.
Narrow on three axes, each a class this would otherwise get wrong in the
opposite direction: a classic script is exempt (a top-level binding really is a
reachable global), CommonJS is exempt (`module.exports` and `exports.x` count as
exports), and every non-JS/TS language is exempt. The export test reads source
rather than the node's `isExported` flag, because that flag is set only from an
`export_statement` ancestor and so reads false for `const x = ...; export { x }`.
…of the fork's six changes The section named colbymchenry#1719 as the defect and stopped there. colbymchenry#1720 measures the guard for it on the same merge base and corpus: 320 rows removed, every one verified against source to land on a file that exports nothing, and 18 added by the dilution mechanism running in reverse. Both halves belong here — a removal count stated without the additions is the projection error this README's own method footnote warns about.
|
Reproduced on the same checkout (vite Composition with #1718. The two auto-merge — adjacent lines in One shape note, not a blocker. In if (finalCandidates.length === 1 && isLexicallyReachable(finalCandidates[0]!, ref, context) && isCrossFileReachable(finalCandidates[0]!, ref, context)) {That has no more order-sensitivity than the Two limits of the source test, both rare, both worth a line in the doc comment rather than a change: a module with imports, no exports and a The removals themselves I did not re-audit file by file; your 320/320 claim is about the target files' source, and the 14-file table reads as the expected shape (entry scripts and specs). |
…as exports A file writing `exports["x"] = …` exports x, and a file with a `declare global` block contributes every name in it to every other file whether or not it exports anything of its own — the extractor emits nodes for the ambient `var` and `interface` members, so sealing such a file would hide names that really are reachable everywhere. Neither shape occurs on the vite corpus, so this changes no measured count; both are now covered by the test.
|
Thanks for reproducing it independently — and the #1718 stack result is more than I had; I had only measured against #1715. First, a correction you should not have to discover: the #874 claim in my description was wrong, and you repeated it back to me. I withdraw it.
A different test each time, and once with no patch at all. The variant could not have caused it in any case: The variant is still rejected, on the real reason. It removes 57 rows this branch keeps, and 41 of them are same-file — Your two limits: both fixed in code rather than documented, and the second is bigger than you framed it. (the bare /^[ \t]*export[\s{*]|^[ \t]*declare\s+global\b|\bmodule\.exports\b|\bexports\s*[.[]/mwhich covers the bracket form too. You were right that neither occurs on vite — I re-ran the arm rather than take it on trust, and the widened predicate gives an edge set identical to the previous revision's, On the Head is now |
…mport The consumer bound every name from 'some-external-pkg'. A bare specifier names a package that is not in the graph, so no project node is the right target for such a reference and colbymchenry#1715 declines it -- which made four of the five positive assertions depend on a resolution that should not happen, and they failed the moment this branch was stacked on colbymchenry#1715. Free references reach the same exact-match path without asserting that. `strayVar` was not testable at all: a bare identifier read emits no edge, so that assertion only ever passed through the bare-import binding. The `declare global` coverage moves to an interface reached through a type annotation, paired with an identical file whose interface is not in a `declare global` -- so the assertion turns on that clause rather than passing whichever way the guard goes.
|
All three open name-matcher PRs stacked on their shared base and measured on the same corpus, plus a fixture defect in this PR that the stack exposed. Composition
The arms interact; the deltas are not additive
So #1720 overlaps the other two by only ~10 of its 320 removals and 5 of its 18 additions — they are very largely complementary. #1718 shows the same effect from the other side: 12 standalone, but 8 on top of #1715. The stack found a defect in this PR's test, not in the othersThe three-way suite ran 4,205 passing / 24 failing. Twenty-three of those fail on unpatched The defect was mine. The fixture bound every name through A fifth assertion was worse than wrong. Pushed as On the
|
|
Restating the suite line from the matrix above, now that the fixture is fixed. Same three-way tree, only the test file changed, so the edge counts are unaffected — 24,986, and the 310 removed / 13 added that #1720 contributes on top of #1715 + #1718 both stand. The three-way suite is 4,205 passing / 24 failing, and the #1719 test is no longer among them. The count is unchanged but the set is not: exactly one name swapped.
The daemon test passes 10/10 on three consecutive isolated runs of Worth flagging for anyone reproducing the matrix: an equal failure count across two arms is not the same set, and here it hid both a real fix and a new flake. The arms are also worth committing before building from them — two of mine were resolved merges left in a working tree that a later |
|
Rebuilt the three-way arm here to settle the Matrix reproduces. Same merge, same two The shape. With the three merged, I moved the sealed test off the fuzzy candidate set and onto the survivor, beside the reachability one: const callableCandidates = applyLanguageGate(candidates.filter((n) => callableKinds.has(n.kind)), ref);
…
if (
finalCandidates.length === 1 &&
isLexicallyReachable(finalCandidates[0]!, ref, context) &&
isCrossFileReachable(finalCandidates[0]!, ref, context)
) {Re-indexed vite: row-identical to the filter form — 24,995 both ways, LOST 0 / GAINED 0 between them, fuzzy 1 in both — and the three PRs' tests still pass (206 across the three fuzzy/resolution files). So on this corpus there is no instance either way; what the survivor form buys is closing the manufacturing path for the case that has one. On your 57 / 41 same-file number: that was the exact-match decline-variant, where "the guard created the uniqueness" is a set-wide property. The survivor form in fuzzy is not set-wide — It is a three-line change. Happy to carry it in #1718 if that merges first, or you can take it here if this one does — either way the diff is the block above. Thanks for tracing the fixture failure to #1715 rather than to the stack; declining |
…ers its set matchFuzzy declines an ambiguous name outright, so filtering sealed candidates out of its set can leave a lone survivor and manufacture a 0.5 edge from an ambiguity that would have been declined. Testing the single survivor instead closes that path; matchByExactName keeps the filter, because it ranks a crowd rather than declining one. No instance on vitejs/vite either way (row-identical, LOST 0 / GAINED 0 per colbymchenry#1720 review). It also declines one shape the filter form resolved: a sealed same-language survivor no longer yields to a cross-language candidate at 0.3.
|
Taken here, as Thank you for reproducing it on your base rather than arguing the shape — and for isolating the 29 One shape it declines that the filter form resolved, which your corpus has no instance of and which is a second effect, not the manufactured-uniqueness one. For a candidate set Local checks: If #1718 merges first I will rebase onto it; the survivor block then just gains your |
|
@colbymchenry, could you review the updated head It rejects invalid call targets without promoting unrelated replacements, handles export examples in strings/comments, and prevents JSON data and Markdown headings from becoming inappropriate targets. Four negative fixtures reproduce on the previous head and pass with the update. The build passes, and 32 focused tests pass with native extraction and again with WASM forced. The full Windows run is 4,181 passed / 24 cleanup failures / 44 skipped: 23 failures match the recorded upstream baseline; the additional daemon cleanup failure passes its isolated 10-test rerun. Details are in the updated PR description. |
|
Measured Standalone: 27,462 edges, LOST 333 / GAINED 8 against base (was 320 / 18 at Stacked on #1715 + #1718 (merge: the import line and the fuzzy survivor block, resolved as you predicted — your block plus the One thing this head does that the previous one did not, and it is the imports branch. Against The mechanism is the one the description names for calls: Two ways to close it, either fine: run the CJS test on the masked Nothing else moved between the two heads that I can see: the 27 extra removals are |
Bare-name fallback can connect imports and calls to private JS/TS bindings or documentation headings. Removing an unreachable candidate before ranking can also promote a different unrelated target.
This extends the module-boundary fix for #1719:
Markdown fixtures inject indexed heading nodes, so this update can be reviewed before #1699's extractor lands. The broader export-status redesign in #1721 remains separate.
Validation on Windows x64, Node 24.16.0:
247413c) and pass with this update.The update changes six code/test files and contains no fork-specific README or benchmark-data changes.