Commit Graph
4 Commits
Author SHA1 Message Date
d151c0f922 feat(resolution): tsconfig path aliases + re-export chain following (#130)
* feat(resolution): tsconfig path aliases + re-export chain following

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.

* fix(resolution): address reviewer findings — isExternalImport bypass, 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).

* fix(resolution): allow re-export rename chains past the pre-filter

The fast pre-filter in resolveOne() bails when no symbol with the
reference name exists project-wide, which is incompatible with the
new chain-following code: a renamed re-export (`import { login }
from './barrel'` where the barrel does `export { signIn as login }
from './auth'`) intentionally calls a name that has no project-wide
declaration. The chain finds the renamed upstream symbol — but only
if resolution is allowed to run.

Add an import-mapping escape so the pre-filter only bails when the
ref also doesn't match any local import. Adds two tests covering the
3-hop wildcard chain and the named-rename branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Colby McHenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:53:16 -05:00
8eed24327c feat(extraction): instantiates + decorates graph edges (#134)
* feat(extraction): instantiates + decorates graph edges

Two new structural edges that fill gaps in the call graph for
modern JS/TS / Java / C# / Python / Kotlin codebases.

1) `instantiates` edges from `new Foo(...)`:

The bulk-extraction and visitFunctionBody dispatchers only
recognised `call_expression`; `new_expression` (and the equivalent
`object_creation_expression` / `instance_creation_expression` in
other grammars) was silently ignored. Adds INSTANTIATION_KINDS,
extractInstantiation(), and dispatch from BOTH the top-level
visitNode and the per-function-body walker. Children are still
descended so nested calls inside constructor args (`new Foo(bar())`)
get their own `calls` refs.

Output: a `bootstrap` function that does `new UserService(); new
UserController(svc)` now produces two `instantiates` edges to those
class nodes — previously zero edges.

2) `decorates` edges from `@Decorator` annotations:

Tree-sitter places decorator nodes BEFORE the symbol they apply to
in the AST, so the original walk-time dispatch saw the wrong
nodeStack head (file/class instead of class/method). Replaced with
extractDecoratorsFor(declNode, decoratedId) that runs from inside
extractClass / extractFunction / extractMethod after the symbol's
node id is known.

Looks for decorator nodes in two places:
  - Direct named children of the declaration (method/property style)
  - Preceding siblings in the parent (TypeScript class style:
    @Foo class X {} parses as parent { decorator, class_decl })

Sibling check uses startIndex comparison rather than reference
identity — tree-sitter web bindings return fresh JS wrappers from
parent/namedChild navigation, so `===` is unreliable. Took a debug
session to spot this; flagging in the comment so the next reader
doesn't re-introduce the bug.

Output: a `@Controller` class decorator + `@Get` method decorator
on a NestJS-style controller now produce two `decorates` edges
(class→Controller, method→Get) with the correct source nodes.

Verified live on a synthetic NestJS-shape fixture; all 380
existing tests pass.

* fix(extraction): address reviewer findings — decorator boundary, generic constructors, property/field decorators, marker_annotation, tests

Five fixes from independent semantic review:

- extractDecoratorsFor sibling walk now iterates BACKWARD from the
  declaration and stops at the first non-decorator/annotation
  separator. Previous version walked forward up to declStart and
  consumed every decorator-typed sibling — so two adjacent
  decorated classes (`@A class Foo {} @B class Bar {}`) had `@A`
  spuriously attributed to `Bar`.

- extractInstantiation strips the type-argument suffix from the
  constructor field text. `new Map<K, V>()` was producing
  referenceName 'Map<K, V>' (the constructor field is a generic_type
  node) and resolution always failed.

- extractProperty and extractField now call extractDecoratorsFor
  after their createNode calls. NestJS-style `@Inject() private
  svc: Foo` and Java field annotations were being silently dropped.

- consider() in extractDecoratorsFor recognises 'marker_annotation'
  in addition to 'decorator'/'annotation'. Java's tree-sitter grammar
  emits marker_annotation for arg-less annotations like @Override
  and @Deprecated; without this every Java marker annotation was
  silently skipped.

- 6 new extraction tests covering: instantiates ref for new Foo(),
  generic-type stripping (`new Container<string>()` -> 'Container'),
  qualified-new keeps trailing identifier (`new ns.Foo()` -> 'Foo'),
  decorates ref for @Foo class X {}, regression for adjacent
  decorated classes (each gets its OWN decorator), decorates ref
  for @Foo method().

Full test suite: 386 passed (was 380, +6 new extraction tests).

* feat(resolution): kind-aware scoring + Python instantiation promotion

Two follow-ups to the new instantiates/decorates ref kinds, surfaced
during review:

1) name-matcher previously only had a kind bonus for `calls`
   (preferring function/method). When a class and a function share a
   name across modules, an `instantiates` ref would tie or pick the
   wrong candidate. Adds:
     - `instantiates` → +25 for class/struct/interface
     - `decorates`    → +25 for function/method, +15 for class
       (Python class decorators, Java annotation interfaces)

2) Python (and Ruby) have no `new` keyword — `Foo()` is the standard
   instantiation syntax, indistinguishable from a function call at
   extraction time. Resolution can tell the difference once the
   target is known: when a `calls` ref resolves to a class/struct,
   promote it to `instantiates`. Mirrors the existing extends→
   implements promotion in createEdges.

Verified: 386 → 389 passing (+3 tests covering the kind biases and
the Python promotion).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Colby McHenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:08:55 -05:00
Colby McHenryandClaude Opus 4.6 584bd94ecc fix: Prevent false cross-module edges in name-based resolution
Name matching was creating false `calls` edges between unrelated modules
in monorepos because `findBestMatch()` had no concept of directory
proximity — functions with common names (e.g. `navigate`) in different
apps scored identically and resolved to whichever came first.

Adds path proximity scoring (shared directory segments) so same-module
candidates strongly win over cross-boundary ones, and lowers confidence
for distant matches so import-based resolution takes precedence.

Closes #67

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:23:13 -05:00
Colby McHenry cc6e7a5c89 Init 2026-01-18 16:25:00 -06:00