Skip to content

feat(resolution): tsconfig path aliases + re-export chain following#7

Open
mschreib28 wants to merge 2 commits into
mainfrom
upstream/feat/resolution-aliases-and-reexports
Open

feat(resolution): tsconfig path aliases + re-export chain following#7
mschreib28 wants to merge 2 commits into
mainfrom
upstream/feat/resolution-aliases-and-reexports

Conversation

@mschreib28
Copy link
Copy Markdown
Owner

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-defined compilerOptions.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\nAdds src/resolution/path-aliases.ts:\n- Reads tsconfig.json, falls back to jsconfig.json\n- Honours compilerOptions.baseUrl and compilerOptions.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, which JSON.parse rejects)\n\nResolutionContext.getProjectAliases() lazily loads + caches the result. resolveAliasedImport consults it before the legacy fallback list, so old projects keep working unchanged.\n\n### 2. Re-export chain following\n\nimport { 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. The barrel pattern is universal in modern JS/TS — every index.ts aggregator 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 recursive findExportedSymbol(...) helper with depth cap (8) and visited-set cycle protection\n\nresolveViaImport now 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 @lib aliases:\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 chain main.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 run380 passed, no regressions\n- [x] npx tsc --noEmit clean\n- [x] npm run build succeeds\n\n🤖 Generated with Claude Code\n


Copied from colbymchenry/codegraph#130

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).
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.

2 participants