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:
@@ -814,6 +814,130 @@ public class Calculator {
|
||||
expect(methodNode).toBeDefined();
|
||||
expect(methodNode?.isStatic).toBe(true);
|
||||
});
|
||||
|
||||
it('wraps top-level declarations in a namespace from package_declaration', () => {
|
||||
const code = `
|
||||
package com.example.foo;
|
||||
|
||||
public class Bar {
|
||||
public String greet() { return "hi"; }
|
||||
}
|
||||
`;
|
||||
const result = extractFromSource('Bar.java', code);
|
||||
|
||||
const ns = result.nodes.find((n) => n.kind === 'namespace');
|
||||
expect(ns?.name).toBe('com.example.foo');
|
||||
|
||||
const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar');
|
||||
expect(cls?.qualifiedName).toBe('com.example.foo::Bar');
|
||||
|
||||
const greet = result.nodes.find((n) => n.kind === 'method' && n.name === 'greet');
|
||||
expect(greet?.qualifiedName).toBe('com.example.foo::Bar::greet');
|
||||
});
|
||||
|
||||
it('does not wrap when no package is declared', () => {
|
||||
const code = `
|
||||
public class Bar {
|
||||
public String greet() { return "hi"; }
|
||||
}
|
||||
`;
|
||||
const result = extractFromSource('Bar.java', code);
|
||||
expect(result.nodes.find((n) => n.kind === 'namespace')).toBeUndefined();
|
||||
const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar');
|
||||
expect(cls?.qualifiedName).toBe('Bar');
|
||||
});
|
||||
|
||||
it('extracts anonymous-class overrides from `new T() { ... }`', () => {
|
||||
// The pattern that breaks the trace through `strategy.foo()` in
|
||||
// libraries like guava's Splitter: the lambda-returned anonymous
|
||||
// class overrides abstract methods on the base, but without
|
||||
// extracting those overrides the interface→impl synthesizer has
|
||||
// nothing to bridge.
|
||||
const code = `
|
||||
package com.example;
|
||||
|
||||
abstract class Base {
|
||||
abstract int compute(int x);
|
||||
}
|
||||
|
||||
public class Factory {
|
||||
public Base make() {
|
||||
return new Base() {
|
||||
@Override
|
||||
int compute(int x) { return x + 1; }
|
||||
};
|
||||
}
|
||||
}
|
||||
`;
|
||||
const result = extractFromSource('Factory.java', code);
|
||||
|
||||
const anon = result.nodes.find((n) => n.kind === 'class' && /Base\$anon@/.test(n.name));
|
||||
expect(anon, 'anonymous Base subclass should be extracted as a class').toBeDefined();
|
||||
|
||||
const compute = result.nodes.find(
|
||||
(n) => n.kind === 'method' && n.name === 'compute' && n.qualifiedName.includes('$anon@')
|
||||
);
|
||||
expect(compute, 'override method should be a method on the anon class').toBeDefined();
|
||||
expect(compute!.qualifiedName).toContain('Factory::make::<Base$anon@');
|
||||
expect(compute!.qualifiedName.endsWith('::compute')).toBe(true);
|
||||
|
||||
// Anon class must extend Base so Phase 5.5 (interface-impl) can bridge.
|
||||
const extendsRef = result.unresolvedReferences.find(
|
||||
(r) => r.referenceKind === 'extends' && r.referenceName === 'Base' && r.fromNodeId === anon!.id
|
||||
);
|
||||
expect(extendsRef, 'anon class should carry an `extends Base` reference').toBeDefined();
|
||||
|
||||
// The enclosing `make` method still emits an instantiates edge to Base —
|
||||
// anon extraction must not swallow that signal.
|
||||
const instantiatesRef = result.unresolvedReferences.find(
|
||||
(r) => r.referenceKind === 'instantiates' && r.referenceName === 'Base'
|
||||
);
|
||||
expect(instantiatesRef, 'enclosing method should still instantiate Base').toBeDefined();
|
||||
});
|
||||
|
||||
it('extracts anonymous-class overrides inside a lambda body', () => {
|
||||
// The exact guava pattern: a lambda is passed to a constructor, and the
|
||||
// lambda body returns `new T() { @Override ... }`. The anon class must
|
||||
// still surface even though it sits inside a lambda_expression node.
|
||||
const code = `
|
||||
package com.example;
|
||||
|
||||
interface Strategy {
|
||||
java.util.Iterator<String> iterator(String s);
|
||||
}
|
||||
|
||||
abstract class BaseIter implements java.util.Iterator<String> {
|
||||
abstract int separatorStart(int start);
|
||||
}
|
||||
|
||||
public class Splitter {
|
||||
private final Strategy strategy;
|
||||
public Splitter(Strategy s) { this.strategy = s; }
|
||||
|
||||
public static Splitter on(char c) {
|
||||
return new Splitter((seq) ->
|
||||
new BaseIter() {
|
||||
@Override
|
||||
int separatorStart(int start) { return start + 1; }
|
||||
@Override public boolean hasNext() { return false; }
|
||||
@Override public String next() { return null; }
|
||||
});
|
||||
}
|
||||
}
|
||||
`;
|
||||
const result = extractFromSource('Splitter.java', code);
|
||||
|
||||
const anon = result.nodes.find((n) => n.kind === 'class' && /BaseIter\$anon@/.test(n.name));
|
||||
expect(anon, 'anon BaseIter inside the lambda body should be extracted').toBeDefined();
|
||||
|
||||
const sepStart = result.nodes.find(
|
||||
(n) =>
|
||||
n.kind === 'method' &&
|
||||
n.name === 'separatorStart' &&
|
||||
n.qualifiedName.includes('$anon@')
|
||||
);
|
||||
expect(sepStart, 'override inside the lambda-returned anon class should be a method node').toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('C# Extraction', () => {
|
||||
@@ -1173,6 +1297,54 @@ interface WebSocket {
|
||||
expect(methodNames).toContain('send');
|
||||
expect(methodNames).toContain('cancel');
|
||||
});
|
||||
|
||||
it('wraps top-level declarations in a namespace from package_header', () => {
|
||||
const code = `
|
||||
package com.example.foo
|
||||
|
||||
class Bar {
|
||||
fun greet(): String = "hi"
|
||||
}
|
||||
|
||||
fun util(): Int = 42
|
||||
`;
|
||||
const result = extractFromSource('Bar.kt', code);
|
||||
|
||||
const ns = result.nodes.find((n) => n.kind === 'namespace');
|
||||
expect(ns?.name).toBe('com.example.foo');
|
||||
|
||||
const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar');
|
||||
expect(cls?.qualifiedName).toBe('com.example.foo::Bar');
|
||||
|
||||
const greet = result.nodes.find((n) => n.kind === 'method' && n.name === 'greet');
|
||||
expect(greet?.qualifiedName).toBe('com.example.foo::Bar::greet');
|
||||
|
||||
const util = result.nodes.find((n) => n.kind === 'function' && n.name === 'util');
|
||||
expect(util?.qualifiedName).toBe('com.example.foo::util');
|
||||
});
|
||||
|
||||
it('handles a single-segment package', () => {
|
||||
const code = `
|
||||
package foo
|
||||
|
||||
class Bar
|
||||
`;
|
||||
const result = extractFromSource('Bar.kt', code);
|
||||
const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar');
|
||||
expect(cls?.qualifiedName).toBe('foo::Bar');
|
||||
});
|
||||
|
||||
it('does not wrap when no package is declared', () => {
|
||||
const code = `
|
||||
class Bar {
|
||||
fun greet() = "hi"
|
||||
}
|
||||
`;
|
||||
const result = extractFromSource('Bar.kt', code);
|
||||
expect(result.nodes.find((n) => n.kind === 'namespace')).toBeUndefined();
|
||||
const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'Bar');
|
||||
expect(cls?.qualifiedName).toBe('Bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dart Extraction', () => {
|
||||
|
||||
Reference in New Issue
Block a user