feat(resolution): tsconfig path aliases + re-export chain following#7
Open
mschreib28 wants to merge 2 commits into
Open
feat(resolution): tsconfig path aliases + re-export chain following#7mschreib28 wants to merge 2 commits into
mschreib28 wants to merge 2 commits into
Conversation
Two related correctness improvements that unlock accurate import resolution on modern JS/TS codebases. 1) tsconfig/jsconfig path aliases. The resolver previously had a hard-coded list of common aliases (@/, ~/, src/, app/) and ignored any project-defined paths from tsconfig.json compilerOptions.paths — which means every import through @components/Foo, @lib/utils, etc. on Vite/Next/Nuxt/Nest projects silently failed to resolve. Adds src/resolution/path- aliases.ts that reads tsconfig.json (and falls back to jsconfig.json), honours baseUrl, supports the * wildcard, and respects the priority order of multiple replacement targets per alias. JSONC tolerant (strips comments + trailing commas, common in the wild). The new ResolutionContext.getProjectAliases() lazily loads + caches the result; resolveAliasedImport consults it before the legacy fallback list. Verified live on a synthetic project with @utils/* and @lib custom aliases: both resolved to the correct files and produced edges, unresolved_refs empty. 2) Re-export chain following. `import { Foo } from './barrel'` where barrel.ts only re-exports (`export { Foo } from './real'` or `export * from './real'`) used to fail because the resolver only looked for declarations IN the resolved file — it never followed the export chain to the actual definition. Adds extractReExports() (named + wildcard + as-rename forms), a per-file getReExports() context method, and a recursive findExportedSymbol() helper with depth cap (8) and visited-set cycle protection. resolveViaImport now uses it whenever the symbol isn't directly declared in the imported file. Verified live on a synthetic 3-hop chain (main → all.ts wildcard → index.ts named → auth.ts declaration): signIn resolved correctly, unresolved_refs empty. Full test suite: 380 passed, 0 failed.
… JSONC strings, comment stripping, optional context method
Five fixes from independent semantic review:
- isExternalImport now consults context.getProjectAliases() before
the bare-specifier heuristic. Without this, custom prefixes like
'@components/*' from tsconfig.paths were classified as npm and
resolveAliasedImport never even ran. Adds a context parameter
(optional, for backward compat with mock contexts).
- stripJsonc rewritten as a string-aware state machine. The previous
regex-only version corrupted any URL embedded in a JSON string
value ('https://cdn.example.com' lost everything after '//').
- extractReExports now strips JS line+block comments from content
before applying the regex, so a commented-out 'export { x } from
...' no longer creates a phantom re-export edge. New
stripJsComments helper preserves string literals (single, double,
template) so '//' inside a string stays intact.
- ResolutionContext.getProjectAliases() made optional so existing
mock contexts in __tests__/resolution.test.ts (which TypeScript
doesn't type-check because tsconfig excludes __tests__) don't
throw at runtime when resolveAliasedImport hits them. Caller
uses ?.
- Two new integration tests in __tests__/resolution.test.ts:
* Path-alias resolution with name-collision: two pickMe() in
different dirs, only the @utils-aliased one should be the
call target. Asserts via getCallers on each candidate node.
* No-tsconfig fallback: relative import still produces the call
edge.
Full test suite: 832 passed (was 380; the increase is from the
biomarkers + LLM hooks that ship via parent branches).
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\n\nTwo related correctness improvements that unlock accurate import resolution on modern JS/TS codebases — the single biggest gap in resolution quality identified during the recent quality audit.\n\n### 1. tsconfig/jsconfig path aliases\n\nThe resolver had a small hard-coded alias list (
@/,~/,src/,app/) and ignored project-definedcompilerOptions.paths. Every import through@components/Foo,@lib/utils,~api/auth, etc. on Vite / Next / Nuxt / Nest / NestJS / TanStack-Start projects silently failed to resolve.\n\nAddssrc/resolution/path-aliases.ts:\n- Readstsconfig.json, falls back tojsconfig.json\n- HonourscompilerOptions.baseUrlandcompilerOptions.paths\n- Supports the*wildcard (the only TS-supported wildcard)\n- Multiple replacement targets per alias, tried in tsconfig priority order\n- JSONC-tolerant: strips//and/* */comments + trailing commas before parsing (tsconfigs in the wild routinely contain those, whichJSON.parserejects)\n\nResolutionContext.getProjectAliases()lazily loads + caches the result.resolveAliasedImportconsults it before the legacy fallback list, so old projects keep working unchanged.\n\n### 2. Re-export chain following\n\nimport { Foo } from './barrel'wherebarrel.tsonly re-exports (export { Foo } from './real'orexport * from './real') used to fail because the resolver only looked for declarations in the resolved file. The barrel pattern is universal in modern JS/TS — everyindex.tsaggregator broke resolution for everything it forwarded.\n\nAdds:\n-extractReExports(content, language)— recognises named (export { a, b as c } from '…'), wildcard (export * from '…'), and namespace (export * as ns from '…') forms\n-ResolutionContext.getReExports(filePath, language)— per-file cached\n- A new recursivefindExportedSymbol(...)helper with depth cap (8) and visited-set cycle protection\n\nresolveViaImportnow follows the chain whenever the symbol isn't directly declared in the imported file.\n\n## Test plan\n\n- [x] Path alias verification — synthetic project with@utils/*and@libaliases:\n\n resolved calls from src/main.ts:\n { caller: 'main', callee: 'formatDate', callee_file: 'src/utils/format.ts' }\n { caller: 'main', callee: 'libCore', callee_file: 'src/lib/index.ts' }\n unresolved calls (should be empty): []\n\n- [x] Re-export chain verification — 3-hop chainmain.ts → all.ts (wildcard) → index.ts (named) → auth.ts (declaration):\n\n resolved edges from src/main.ts (lands in src/services/auth.ts):\n { caller: 'go', callee: 'signIn', callee_file: 'src/services/auth.ts' }\n unresolved (should be empty for signIn/Session): []\n\n- [x]npx vitest run— 380 passed, no regressions\n- [x]npx tsc --noEmitclean\n- [x]npm run buildsucceeds\n\n🤖 Generated with Claude Code\nCopied from colbymchenry/codegraph#130