Files
codegraph/__tests__/java-declared-receiver.test.ts
Colby MchenryandClaude Opus 5.5 f9530d7d5f fix(resolution): a call through a declared member calls that member's type (#2171)
C# / Java / Kotlin receivers that are fields, properties or parameters were
typed only when declared in the calling method without generics, so a call
through a field fell to the receiver-word guess: Newtonsoft's
_innerWriter.WriteValue() landed on the wrapper's own WriteValue, a
TextWriter's Write() on a test writer's, commons-lang's DateFormat field's
parse() on DateParser 284 times.

- read class-level member declarations (brace depth 1, comments skipped),
  walking base classes with their type arguments substituted, C# using
  aliases, type-parameter bounds, generic and for-each declarations
- C# interface members default public and namespace-level types internal
  (TS extractor + kernel, parity 0 diffs); interface calls resolve at all
- the receiver-word guess weighs the owner type's name only, the receiver's
  last link (a constant's type), and its head noun; .NET std names need a
  receiver named after the owner; production code never lands in a test
  suite's mock

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:54:31 +00:00

136 lines
3.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* A Java receiver's declared type is read through generics and for-each
* loops (`final Pair<L, R> pair`, `for (Element el : this)`), from a field of
* the class (`private DateFormat dateTimeParser;` — outside the project, so
* no edge), or as the bound of a type parameter (`T extends ExceptionContext`).
* A constant's receiver is its type (`Phase.CONTROLLERS.getPhase()`). None of
* these fall back to a same-named method of some other project class:
* commons-lang's `dateTimeParser.parse(…)` went to its own `DateParser.parse`
* 284 times.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-java-receiver-'));
const files: Record<string, string> = {
'src/main/java/app/Pair.java': `package app;
public abstract class Pair<L, R> {
public abstract L getKey();
}
`,
'src/main/java/app/MutablePair.java': `package app;
public class MutablePair<L, R> extends Pair<L, R> {
private L left;
public L getKey() { return left; }
/**
* Copies the key of each element into this pair.
*/
public void setValue(final Pair<L, R> pair) {
left = pair.getKey();
}
}
`,
'src/main/java/app/DateParser.java': `package app;
public interface DateParser {
Object parse(String source);
}
`,
'src/main/java/app/Element.java': `package app;
public class Element {
public void val(String value) { }
}
`,
'src/main/java/app/Elements.java': `package app;
import java.util.ArrayList;
public class Elements extends ArrayList<Element> {
public Elements val(String value) {
for (Element element : this)
element.val(value);
return this;
}
}
`,
'src/main/java/app/Phase.java': `package app;
public enum Phase {
CONTROLLERS;
public int getPhase() { return 0; }
}
`,
'src/main/java/app/ExceptionContext.java': `package app;
public interface ExceptionContext {
int getContextEntries();
}
`,
'src/test/java/app/DateUtilsTest.java': `package app;
import java.text.DateFormat;
public abstract class DateUtilsTest<T extends ExceptionContext> {
private DateFormat dateTimeParser;
protected T exceptionContext;
void rounds() throws Exception {
dateTimeParser.parse("January 1, 2008");
exceptionContext.getContextEntries();
Phase.CONTROLLERS.getPhase();
}
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
/** `Owner::member` of every call edge out of a file. */
function callsFrom(file: string): string[] {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!.qualifiedName)
.sort();
}
describe('Java calls through declared receivers', () => {
it('read a generic parameter’s type', () => {
expect(callsFrom('src/main/java/app/MutablePair.java')).toEqual(['app::Pair::getKey']);
});
it('read a for-each variable’s type', () => {
expect(callsFrom('src/main/java/app/Elements.java')).toEqual(['app::Element::val']);
});
it('follow a field’s type, a type parameter’s bound and a constant’s type — and stop at an outside type', () => {
expect(callsFrom('src/test/java/app/DateUtilsTest.java')).toEqual([
'app::ExceptionContext::getContextEntries',
'app::Phase::getPhase',
]);
});
});