Skip to content
Merged
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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,35 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- **Java / Kotlin imports now resolve by fully-qualified name.** Extraction
wraps every top-level declaration of a `.kt` / `.java` file in a `namespace`
node carrying the file's `package` (so a class `Bar` in
`package com.example.foo` is indexed with qualifiedName
`com.example.foo::Bar`), and `import com.example.foo.Bar` looks the target
up through that index — regardless of whether the class lives in `Bar.kt`,
`Models.kt`, or a top-level function. Disambiguates same-name classes
across packages (the central failure mode of the previous name-matcher
fallback in multi-module Spring / Android codebases), works across the
Java↔Kotlin interop boundary, and lays groundwork for binding-precise
Dagger2 / Hilt resolution. Wildcard imports (`com.example.*`) still go
through name-matcher.
- **Java / C# anonymous classes (`new T() { ... }`) are now extracted as
first-class class nodes with their overrides.** Previously, an anonymous
subclass returned from a factory or lambda — `return new BaseIter() {
@Override int separatorStart(int s) { ... } };` — produced only an
`instantiates` edge: the override methods were invisible to the graph and
Phase 5.5 interface-impl synthesis had no class to bridge. The anon class
now lands as `<TypeName$anon@line>` with an `extends` reference to the
named base/interface, scoped under the enclosing method, and its
`method_declaration` members become normal method nodes. The interface→impl
synthesizer then bridges the base's abstract methods to the anonymous
overrides automatically. Concrete effect on `google/guava` (3,227 .java
files): 3,608 anonymous classes extracted, +2,534 interface-impl edges
reach overrides hidden in `new T() { ... }` blocks (including lambda
bodies). An agent investigating `Splitter.SplittingIterator.separatorStart`
now sees the four anonymous overrides in its trail without a Read.

### Fixed
- **`codegraph index` / `init -i` summary now reports the true edge count.**
The per-file counter in the orchestrator only saw extraction-phase edges,
Expand Down
172 changes: 172 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading