Commit Graph
10 Commits
Author SHA1 Message Date
Artem BambalovandGitHub 34240eb297 feat(jvm): resolve Java/Kotlin imports by fully-qualified name (#412)
Wrap top-level declarations of `.kt` / `.java` files in an implicit `namespace` node carrying the file's `package`, then resolve `import com.example.foo.Bar` through that qualifiedName index — so a Bar in Models.kt resolves correctly regardless of filename, a top-level function import binds to its declaration, Java↔Kotlin interop crosses cleanly, and same-name classes across packages no longer collide. Wildcard imports still go through name-matcher.

Also extracts Java/C# anonymous-class overrides (`new T() { ... }`) as first-class class nodes with their override methods. Phase 5.5 interface-impl then bridges T's abstract methods to the anonymous overrides automatically — including the lambda-returned `new T() { ... }` pattern common in guava (Splitter, CacheBuilder).

Concrete impact on macrozheng/mall (524 .java files, multi-module Spring + MyBatis): 524 namespace nodes, 862 imports edges newly resolve to Java symbols, 76 distinct `Criteria` classes preserved across packages with no merge. On google/guava (3,227 .java): 3,608 anonymous classes extracted, +2,534 interface-impl edges reach overrides hidden in `new T() { ... }` blocks.

Agent A/B playbook on small (spring-petclinic-kotlin, 38 .kt), medium (mall, 524 .java), large (guava, 3,227 .java) — 3 flow prompts × 2 runs/arm × 2 arms = 36 runs, claude-opus, headless. Spring repos: 0/0 Read/Grep with-arm, −27% wall-clock vs no-codegraph. Guava: 1.8 Read avg with-arm (vs 2.0 without) — improved by the anon-class extraction; residual is a lambda→SAM coverage gap orthogonal to FQN imports (filing follow-up).
2026-05-26 22:06:53 -05:00
48eebe1e3e feat(resolution): add C/C++ include path resolution (#453)
* feat(resolution): add C/C++ include path resolution

Add full import resolution pipeline for C and C++ #include directives,
connecting extracted import nodes to actual header files in the project.

- Add C/C++ extension resolution (.h, .hpp, .hxx, .cpp, .cc, .cxx)
- Add system header filtering with ~80 C and ~80 C++ stdlib headers
- Add extractCppImports() for #include import mapping extraction
- Add compile_commands.json parsing for -I/-isystem include directories
- Add heuristic include dir discovery (include/, src/, lib/, api/)
- Add resolveCppIncludePath() for include directory search
- Add C/C++ built-in symbol filtering (printf, malloc, std::*, etc.)
- Wire getCppIncludeDirs into ResolutionContext
- Add 13 new tests for C/C++ import resolution and extraction

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* review: wire #include resolution into pipeline + fix builtin filter

The PR landed the include-dir scan logic (loadCppIncludeDirs +
resolveCppIncludePath) but the indexer never reached it: imports
references with referenceName='X.h' fell into resolveViaImport's
symbol-lookup branch (matched extractCppImports' basename-without-ext
localName via .startsWith, then tried to find a symbol named like the
extension and failed). End result on bitcoin-core: 0 new file→file
imports vs main, despite the include-dir scan resolving paths correctly
when probed directly. resolveViaImport now has a C/C++ imports branch
that resolves the include path to the actual file node and returns
that — skipping the irrelevant symbol scan. Measured on bitcoin-core:
+2,059 newly resolved file→file imports (6,027 → 8,086, +34%).

The unconditional CPP_BUILT_INS / C_BUILT_INS filter also misfired:
C/C++ codebases routinely shadow stdlib names (bitcoin's mp::move,
custom allocators with free/malloc, stream classes with read/write/
close/open, logging libs wrapping printf). Filtering those names
killed legitimate edges — 1,179 → 0 for move(), 33 → 0 for free(),
149 → 7 for write() on bitcoin. The filter now defers to
hasAnyPossibleMatch: only filter when no user-defined symbol with the
name exists. std:: prefix stays unconditional (never user-shadowed in
practice). After: printf/free/open/close/read/write/swap all preserved
at main's counts; the std::move-binds-to-mp::move false-positives still
drop (correctly: −2,154 C/C++ calls).

Also: drop the duplicate 'FILE' in C_BUILT_INS; add an end-to-end test
that asserts `#include "X.h"` produces a file→file imports edge in the
real indexing pipeline (not just direct resolver probes); add a test
documenting the cross-language `.h` heuristic claim (Obj-C dirs are
intentionally allowed as C/C++ include dirs); add CHANGELOG entry
under [Unreleased] with measured numbers.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Colby McHenry <me@colbymchenry.com>
2026-05-26 19:56:25 -05:00
Colby MchenryandGitHub 8c69001289 fix(resolution): Java/Kotlin imports disambiguate same-name classes (#314) (#472)
A Maven multi-module project where `dao/converter/FooConverter` and
`service/converter/FooConverter` both expose a `convert` method used to
resolve by file-path proximity — picking whichever class was closer to
the caller, which is wrong any time the caller lives in an equidistant
cross-cutting module. `extractImportMappings` had no Java branch at all,
so the FQN signal Java imports carry — `import
com.example.dao.converter.FooConverter;` — was thrown away.

- `extractJavaImports` parses regular and `import static` directives;
  wildcard imports (`*`) are intentionally skipped.
- `resolveViaImport` has a new Java/Kotlin cross-file branch that
  converts the imported FQN to a file-path suffix
  (`com/example/dao/converter/FooConverter.java`, or `.kt`) and
  resolves the symbol against the file whose path matches by suffix.
- For the field-receiver pattern (`@Autowired private FooConverter
  fooConverter; fooConverter.convert(...)`), `matchMethodCall` now
  looks up the receiver's inferred type in the caller file's imports
  and threads the resulting FQN through to `resolveMethodOnType`.
  When two `FooConverter::convert` candidates exist, the import — not
  iteration order — picks the right one.

Validated with a synthetic 3-module repro: swapping only the import
line on the caller swaps the resolved target between dao and service.

spring-petclinic (47 .java files): +15 newly import-resolved edges,
+2 references, no regression elsewhere.

Closes #314.
2026-05-26 17:42:14 -05:00
Colby MchenryandGitHub 186632fa88 fix(extraction): TS type-alias object members are first-class nodes (#359) (#471)
A call site `recorder.stop()` where `recorder: RecorderHandle` and
`type RecorderHandle = { stop: () => Promise<void> }` used to attach
its edge to an unrelated `class Foo { stop() {} }` in a sibling
directory — there was no `RecorderHandle::stop` node, so the existing
camelCase/path-proximity scoring picked the only `stop` method in the
graph (which happened to be wrong). False-positive `calls` edges
silently widened `codegraph_impact` blast radius.

`extractTypeAlias` now surfaces object-shape (and intersection-type)
members as first-class graph nodes:

  type X = { foo: T; bar(): T };
  ->  X        (type_alias)
      X::foo   (property)
      X::bar   (method)

Function-typed properties (`stop: () => Promise<void>`) emit as `method`
kind so `obj.stop()` resolves to them at the call site — same node
kind the existing receiver-name/word-overlap heuristic in
`matchMethodCall` already prefers. No new resolver logic needed.

Walk only immediate `object_type` / `intersection_type` operands of the
alias value. Anonymous nested object types inside generic arguments
(`Promise<{ ok: true }>`) intentionally don't produce phantom members.

Validation on excalidraw/excalidraw (314 .ts files):
  +776 new property nodes (alias non-function members)
  +1,008 new method nodes (alias function-typed properties + method_signatures)
  +226 calls edges newly accurate against alias members

User's exact 3-file repro:
  before: finaliseRecording -> StdioMcpClient::stop (wrong, sibling dir)
  after:  finaliseRecording -> RecorderHandle::stop (correct)
  StdioMcpClient::stop callers: voice/ false-positives gone

Closes #359.
2026-05-26 17:35:26 -05:00
Colby MchenryandGitHub 046e03a05f fix(extraction): C# produces references edges for type annotations (#381) (#470)
Indexing any C# project produced zero `references` edges, so
`codegraph_callers SomeDto` returned no hits even when the DTO was used
as a param/return type across the codebase, and `codegraph_callees` on
a service class only saw its `using` imports — the headline structural
query silently degraded to text-search on half of every typical backend
stack.

Two root causes:

1. `csharp.ts` was missing `returnField` (default `'return_type'` doesn't
   exist on C# AST; the field is `'type'`) AND had
   `paramsField:'parameter_list'` (the node TYPE, not the field NAME
   `'parameters'`) — so parameter type extraction silently no-op'd.
2. `extractTypeRefsFromSubtree` only emitted refs for `type_identifier`
   leaves. C# tree-sitter doesn't produce `type_identifier` — it uses
   `identifier`, `predefined_type`, `qualified_name`, `generic_name`,
   `array_type`, `nullable_type`, `tuple_type`, etc.

Fix:

- `csharp.ts`: `paramsField:'parameters'`, `returnField:'type'`.
- Route C# through a dedicated `extractCsharpTypeRefs` +
  `walkCsharpTypePosition`. Descends ONLY into known type fields
  (`parameter.type`, `method.type`, `property.type`,
  `variable_declaration.type`, `tuple_element.type`), so parameter
  NAMES like `request` in `Build(UserDto request)` never leak as type
  refs.
- Hook `extractField` and `extractProperty` to call
  `extractTypeAnnotations` so property/field type refs land in the graph.

Validation on dotnet/eShop (527 .cs files):
  C# `references` edges: 35 -> 925 (+26x)
  No regression in calls/imports/instantiates/extends/implements.

Closes #381.
2026-05-26 17:23:17 -05:00
Colby MchenryandGitHub f1b79eeae1 fix(resolution): Go cross-package qualified calls resolve via go.mod (#388) (#469)
`pkga.FuncX(...)` cross-package calls in Go monorepos were dropping
through the import resolver — `isExternalImport(go)` flagged any
non-`/internal/` import as third-party because the resolver had no idea
what the project's own module path was. Resolution fell back to name
matching with path-proximity scoring, which on a layered codebase picks
one accidental candidate per call site (~<1% recall per #388's
5,303-vs-1 figure).

- `src/resolution/go-module.ts` (new) parses the `module ...` directive
  from project-root `go.mod`, exposed via `getGoModule()` on
  `ResolutionContext`.
- `isExternalImport(go)` treats `<module-path>/...` imports as in-module;
  the existing `/internal/` escape hatch is preserved for repos without
  a parsed go.mod.
- `resolveViaImport` gets a Go cross-package branch that strips the
  module prefix to a project-relative directory, then resolves the
  qualified member via `getNodesByName(member)` filtered to that exact
  directory and `isExported=true`. Sub-packages don't collide with their
  parents; same-name funcs in different packages don't false-merge.
- Go extractor sets `isExported` from the identifier's first character
  (Go's universal uppercase=exported convention). The resolver depends
  on this to filter candidates.

Validation on gRPC-Go (1,031 .go files, layered package tree):
  total `calls` edges:    23,803 -> 34,105 (+43%)
  cross-pkg `calls`:      10,880 -> 19,929 (+83%)
  fmt/strconv/etc. stdlib calls: stay external (no false positives)

Tests cover in-module disambiguation with same-name funcs in two
packages, aliased imports, and stdlib calls not being false-resolved to
in-project nodes.

Closes #388.
2026-05-26 17:14:35 -05:00
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