fix(parser): resolve dotted-stem relative imports in JS/TS and Dart - #845
Open
lavaxun wants to merge 1 commit into
Open
fix(parser): resolve dotted-stem relative imports in JS/TS and Dart#845lavaxun wants to merge 1 commit into
lavaxun wants to merge 1 commit into
Conversation
Path.with_suffix() replaces the final path suffix rather than appending
one, so the JS/TS/TSX/Vue relative-import resolver mistook a dotted
stem for an existing extension: `./outlet.entity` probed `outlet.ts`
and never found `outlet.entity.ts`. Dotted stems are the dominant
NestJS convention (*.entity.ts, *.service.ts, *.controller.ts,
*.guard.ts, *.module.ts), so relative imports between such files
silently produced no IMPORTS_FROM edge, and importers_of returned a
confident, wrong zero for the real target.
Switch the primary probe to extension-append (Path(str(base) + ext)),
matching the idiom tsconfig_resolver.py already used for alias
imports. Keep a `with_suffix` fallback, but only when the import
already ends in .js/.jsx/.mjs/.cjs — NodeNext/ESM TypeScript writes
`./foo.js` for a file that is `foo.ts` on disk, which is the one case
where replacing the suffix is correct.
The Dart resolver's `.dart`-append fallback had the same bug shape
(`base.with_suffix(".dart")`); fixed the same way. Surveyed the other
relative/dotted-path resolvers in the same function (Python, Rust) —
both build their candidate path from module-name segments that can't
contain a literal dot, so `with_suffix` is safe there and was left
alone.
Adds three tests: the dotted-stem regression (red against the
unpatched resolver), a NodeNext .js->.ts case that must keep passing
so a future simplification that drops the ESM fallback goes red, and
the Dart sibling.
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.
The bug
The JS/TS/TSX/Vue relative-import resolver in
code_review_graph/parser.pyused
Path.with_suffix(ext)to try candidate extensions:with_suffix()replaces the final path suffix, it doesn't append one. Soa relative import like
./outlet.entitygets probed asoutlet.ts(the.entitysegment is mistaken for an existing suffix) and the resolver neverfinds the real file,
outlet.entity.ts.Dotted stems are the dominant NestJS convention —
*.entity.ts,*.service.ts,*.controller.ts,*.guard.ts,*.module.ts— so relativeimports between files that follow this convention silently produce no
IMPORTS_FROMedge. Downstream,importers_ofreturns a confident, wrong0for the real target file: it looks like the file has no importers whenin fact the resolver just never reached it.
The fix
Switch the primary probe to extension-append (
Path(str(base) + ext)),matching the idiom
tsconfig_resolver.py's_probe_pathalready used foralias imports — that path is right, this one was wrong.
This can't be a blind swap though: NodeNext/ESM TypeScript writes
./foo.jsin import statements for a source file that is actually
foo.tson disk.That's the one case where replacing the suffix is correct, so a fallback
is kept for it, gated on the import literally ending in
.js/.jsx/.mjs/.cjs:The Dart sibling
_do_resolve_module's Dart branch had the exact same bug shape:target = base.with_suffix(".dart"). Dart imports normally already carrythe
.dartextension (caught by thebase.is_file()check just above), sothis only bites when an extension is omitted on a dotted stem — e.g.
./thing.modelwherething.model.dartexists on disk. Fixed the same way(append, don't replace).
What I did NOT touch
I surveyed the other relative/dotted-path resolvers in the same function
(Python and Rust) for the same shape. Both also call
with_suffix, buttheir candidate paths are built from module-name segments that can't
contain a literal dot (Python:
module.split("."); Rust: identifier pathparts), so there's no dotted-stem collision possible there and
with_suffixis correct as-is. Left them alone.
Tests
Added three tests to
tests/test_parser.py, callingCodeParser._resolve_module_to_filedirectly against real files in a tempdir:
test_relative_import_dotted_stem_resolves_to_full_filename— the actualbug:
./outlet.entitymust resolve tooutlet.entity.ts, with a decoyoutlet.tspresent to prove a wrong resolution (not just a crash) iswhat happens today. This is RED against
main(fails as.../outlet.tsinstead of.../outlet.entity.ts) and green after thefix.
test_relative_import_nodenext_js_extension_resolves_ts_source— theNodeNext
./foo.js->foo.tscase. This already passes onmain(with_suffix happens to work when the import already ends in
.js), andmust keep passing after the fix — it's a regression guard so a future
"simplify this" pass that drops the ESM fallback goes red instead of
silently reintroducing the class of bug this PR fixes.
test_relative_import_dart_dotted_stem_resolves_to_full_filename— sameshape as (1), for the Dart fallback.