Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixes

- Cross-file calls to a **static method** through its class (`Foo.bar(...)` after `import { Foo } from './helpers'`) now resolve to the method. Previously the call was attributed to the class itself and recorded as a construction (`new Foo()`), so the method showed no callers and `codegraph_impact` understated its blast radius, while a bare-name lookup surfaced unrelated same-named symbols — a common pattern with TypeScript static utility classes. Genuine `new Foo()` construction is unchanged. Re-index a project to benefit. (#825) (TypeScript, JavaScript)
- The `codegraph_search` tool's `kind: "type"` filter — a value its own schema advertises — silently matched nothing; it now correctly finds type aliases. The `codegraph_explore` tool's parameter guidance also no longer suggests running `codegraph_search` first, which contradicted explore's call-it-first design and cost agents an extra round-trip.
- Symbols defined in Svelte and Vue `<script>` blocks were reported one line below where they actually are — a function on line 3 was reported at line 4 — which offset every script-block symbol's location in search, `codegraph_node`, and explore output. Line numbers now match the file exactly. Re-index a project to benefit. (Svelte, Vue)
- Doc comments are now captured for exported, `const`-assigned, and decorated declarations, and the documentation a symbol carries is now clean across every supported language. Previously a comment above `export class X`, `export const fn = () => …`, a plain `const fn = () => …`, or a decorated Python `def`/`class` (`@app.route(...)`, `@dataclass`) was dropped entirely — only comments directly above a plain declaration were kept. CodeGraph now finds the comment through the `export` / `const` / decorator wrapper. Comment-marker cleanup was also rounded out for every language CodeGraph supports: Rust/Swift/Kotlin doc lines (`///`, `//!`), Python/Ruby/shell `#`, Lua/Luau (`--` and `--[[ ]]`), and Pascal (`{ }` and `(* *)`) no longer leave stray markers in the stored text — validated end-to-end across all 19 code languages plus Svelte/Vue `<script>` blocks. (#780). Thanks @caleb-kaiser.
Expand Down
62 changes: 62 additions & 0 deletions __tests__/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,68 @@ def bootstrap():
expect(callsToUserService).toHaveLength(0);
});

it('resolves cross-file static method calls ClassName.staticMethod() to the method, not the class (#825)', async () => {
// `Foo.bar()` after `import { Foo } from './helpers'` is a static method
// call. The named-import resolver matched the `Foo.` prefix and resolved
// the receiver to the class `Foo`; createEdges then promoted the `calls`
// edge to `instantiates` on the class and dropped the method — so
// callers/impact for the static method came back empty. The fix resolves
// the trailing member to the method on the imported class.
const srcDir = path.join(tempDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });

fs.writeFileSync(
path.join(srcDir, 'helpers.ts'),
`export class Foo {\n static bar(x: number) { return x + 1; }\n}\n` +
// Sibling whose name CONTAINS the imported class name and has a
// same-named method — the resolver must not soak the call into it.
`export class FooBar {\n bar() { return 0; }\n}\n`
);
fs.writeFileSync(
path.join(srcDir, 'caller.ts'),
`import { Foo } from './helpers';\n` +
`export function run() { return Foo.bar(41); }\n` +
`export function make() { return new Foo(); }\n`
);

cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();

const run = cg.getNodesByKind('function').find((n) => n.name === 'run')!;
const make = cg.getNodesByKind('function').find((n) => n.name === 'make')!;
const foo = cg.getNodesByKind('class').find((n) => n.name === 'Foo')!;
const bar = cg
.getNodesByKind('method')
.find((n) => n.qualifiedName === 'Foo::bar')!;
const fooBarBar = cg
.getNodesByKind('method')
.find((n) => n.qualifiedName === 'FooBar::bar')!;
expect(run).toBeDefined();
expect(bar).toBeDefined();
expect(fooBarBar).toBeDefined();

// run --calls--> Foo::bar (the method), exactly once...
const runOut = cg.getOutgoingEdges(run.id);
const callsBar = runOut.filter((e) => e.kind === 'calls' && e.target === bar.id);
expect(callsBar).toHaveLength(1);
// ...not soaked into the substring-named sibling FooBar::bar...
expect(runOut.find((e) => e.target === fooBarBar.id)).toBeUndefined();
// ...and NOT mis-promoted to `instantiates` on the class.
expect(
runOut.find((e) => e.kind === 'instantiates' && e.target === foo.id)
).toBeUndefined();

// The whole point: callers(staticMethod) now surfaces the real caller.
const callers = cg.getCallers(bar.id).map((c) => c.node.name);
expect(callers).toContain('run');

// Boundary: real construction `new Foo()` must still instantiate the class.
const makeOut = cg.getOutgoingEdges(make.id);
expect(
makeOut.find((e) => e.kind === 'instantiates' && e.target === foo.id)
).toBeDefined();
});

it('resolves Go cross-package qualified calls via go.mod module path (#388)', async () => {
// Pre-#388, every `pkga.FuncX(...)` call in a Go monorepo was flagged
// external (isExternalImport returned true for any non-`/internal/`
Expand Down
38 changes: 38 additions & 0 deletions src/resolution/import-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,44 @@ export function resolveViaImport(
);

if (targetNode) {
// A `calls` ref shaped `ImportedClass.method` is a static method call
// (`Foo.bar()` after `import { Foo }`). The named-import branch above
// resolves `targetNode` to the class `Foo`; returning it as-is makes
// createEdges promote the `calls` edge to `instantiates` on the class
// and drops the method entirely (#825). When the imported receiver is
// a class and the trailing member names one of its methods, resolve to
// that method instead — preserving the exact-file precision the import
// gives over a name-match fallback.
if (
ref.referenceKind === 'calls' &&
!imp.isNamespace &&
ref.referenceName.startsWith(imp.localName + '.') &&
// Only the kinds createEdges promotes to `instantiates`
// (index.ts) — an interface target is never mis-promoted.
(targetNode.kind === 'class' || targetNode.kind === 'struct')
) {
const memberLeaf = ref.referenceName
.slice(imp.localName.length + 1)
.split('.')[0];
// The method's OWNER segment must be exactly the imported class —
// a substring test would let class `Foo` soak up `FooBar::bar`
// when both live in the same file (a wrong edge is worse than none).
const method = context
.getNodesInFile(targetNode.filePath)
.find((n) => {
if (n.kind !== 'method' || n.name !== memberLeaf) return false;
const segs = n.qualifiedName.split('::');
return segs[segs.length - 2] === targetNode.name;
});
if (method) {
return {
original: ref,
targetNodeId: method.id,
confidence: 0.9,
resolvedBy: 'import',
};
}
}
return {
original: ref,
targetNodeId: targetNode.id,
Expand Down