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.
This commit is contained in:
Colby Mchenry
2026-05-26 17:42:14 -05:00
committed by GitHub
parent 186632fa88
commit 8c69001289
4 changed files with 228 additions and 2 deletions
+58
View File
@@ -802,6 +802,64 @@ export async function finaliseRecording(recorder: RecorderHandle) {
expect(handleStop!.kind).toBe('method');
});
it('Java import disambiguates same-name classes across modules (#314)', async () => {
// Pre-#314 the import resolver had no Java branch at all, so a
// multi-module Maven repo where `dao/converter/FooConverter` and
// `service/converter/FooConverter` both export a `convert` method
// resolved 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.
const daoDir = path.join(tempDir, 'dao/src/main/java/com/example/dao/converter');
const serviceDir = path.join(tempDir, 'service/src/main/java/com/example/service/converter');
const webDir = path.join(tempDir, 'web/src/main/java/com/example/web');
fs.mkdirSync(daoDir, { recursive: true });
fs.mkdirSync(serviceDir, { recursive: true });
fs.mkdirSync(webDir, { recursive: true });
fs.writeFileSync(
path.join(daoDir, 'FooConverter.java'),
`package com.example.dao.converter;
public class FooConverter { public String convert(String x) { return "dao:" + x; } }
`
);
fs.writeFileSync(
path.join(serviceDir, 'FooConverter.java'),
`package com.example.service.converter;
public class FooConverter { public String convert(String x) { return "svc:" + x; } }
`
);
// The caller imports the SERVICE version — even though dao is
// alphabetically/lexically first in the candidate list, the
// import must trump that order.
fs.writeFileSync(
path.join(webDir, 'Handler.java'),
`package com.example.web;
import com.example.service.converter.FooConverter;
public class Handler {
private FooConverter fooConverter;
public String use() { return fooConverter.convert("input"); }
}
`
);
cg = await CodeGraph.init(tempDir, { index: true });
const use = cg
.getNodesByKind('method')
.find((n) => n.qualifiedName === 'Handler::use');
expect(use).toBeDefined();
const calls = cg.getOutgoingEdges(use!.id).filter((e) => e.kind === 'calls');
expect(calls.length).toBeGreaterThanOrEqual(1);
const target = cg.getNode(calls[0]!.target);
expect(target?.name).toBe('convert');
expect(target?.filePath.replace(/\\/g, '/')).toBe(
'service/src/main/java/com/example/service/converter/FooConverter.java'
);
});
it('C# extracts references from method/property/field types (#381)', async () => {
// Pre-#381, every C# project produced ZERO `references` edges:
// csharp.ts was missing returnField, and the type-leaf walker