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).
This commit is contained in:
@@ -948,6 +948,41 @@ export function extractReExports(content: string, language: Language): ReExport[
|
||||
/**
|
||||
* Resolve a reference using import mappings
|
||||
*/
|
||||
/**
|
||||
* JVM (Java / Kotlin) imports use fully-qualified names (`import
|
||||
* com.example.foo.Bar`) decoupled from filenames, so the JS/Python
|
||||
* style filesystem path lookup misses them whenever the file isn't
|
||||
* named after its primary symbol (Kotlin `Utils.kt` exporting `Bar`,
|
||||
* top-level fns, extension fns). Resolve them through the
|
||||
* `qualifiedName` index instead — populated by the package_header /
|
||||
* package_declaration namespace wrappers in the extractor.
|
||||
*/
|
||||
export function resolveJvmImport(
|
||||
ref: UnresolvedRef,
|
||||
context: ResolutionContext
|
||||
): ResolvedRef | null {
|
||||
if (ref.referenceKind !== 'imports') return null;
|
||||
if (ref.language !== 'java' && ref.language !== 'kotlin') return null;
|
||||
|
||||
const fqn = ref.referenceName;
|
||||
const lastDot = fqn.lastIndexOf('.');
|
||||
if (lastDot <= 0) return null;
|
||||
const pkg = fqn.substring(0, lastDot);
|
||||
const sym = fqn.substring(lastDot + 1);
|
||||
// Wildcard imports (`com.example.*`) deliberately punt to name-matcher.
|
||||
if (sym === '*') return null;
|
||||
|
||||
const candidates = context.getNodesByQualifiedName(`${pkg}::${sym}`);
|
||||
if (candidates.length === 0) return null;
|
||||
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: candidates[0]!.id,
|
||||
confidence: 0.95,
|
||||
resolvedBy: 'import',
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveViaImport(
|
||||
ref: UnresolvedRef,
|
||||
context: ResolutionContext
|
||||
|
||||
+15
-1
@@ -17,7 +17,7 @@ import {
|
||||
ImportMapping,
|
||||
} from './types';
|
||||
import { matchReference } from './name-matcher';
|
||||
import { resolveViaImport, extractImportMappings, extractReExports, loadCppIncludeDirs } from './import-resolver';
|
||||
import { resolveViaImport, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs } from './import-resolver';
|
||||
import { detectFrameworks } from './frameworks';
|
||||
import { synthesizeCallbackEdges } from './callback-synthesizer';
|
||||
import { loadProjectAliases, type AliasMap } from './path-aliases';
|
||||
@@ -528,6 +528,14 @@ export class ReferenceResolver {
|
||||
// Also check capitalized receiver (instance-method resolution)
|
||||
const capitalized = receiver.charAt(0).toUpperCase() + receiver.slice(1);
|
||||
if (this.knownNames.has(capitalized)) return true;
|
||||
// JVM FQN: `com.example.foo.Bar` — the only useful segment is the
|
||||
// last one (`Bar`); the earlier check finds `example.foo.Bar` which
|
||||
// never matches a node name.
|
||||
const lastDot = name.lastIndexOf('.');
|
||||
if (lastDot > dotIdx) {
|
||||
const tail = name.substring(lastDot + 1);
|
||||
if (tail && this.knownNames.has(tail)) return true;
|
||||
}
|
||||
}
|
||||
const colonIdx = name.indexOf('::');
|
||||
if (colonIdx > 0) {
|
||||
@@ -588,6 +596,12 @@ export class ReferenceResolver {
|
||||
return null;
|
||||
}
|
||||
|
||||
// JVM FQN imports skip framework/name-matcher: `import com.example.Bar`
|
||||
// resolves directly through the qualifiedName index, which is unambiguous
|
||||
// even when several `Bar` classes exist in different packages.
|
||||
const jvmImport = resolveJvmImport(ref, this.context);
|
||||
if (jvmImport) return jvmImport;
|
||||
|
||||
const candidates: ResolvedRef[] = [];
|
||||
|
||||
// Strategy 1: Try framework-specific resolution
|
||||
|
||||
Reference in New Issue
Block a user