A Java method called through a static factory or fluent chain — `Foo.getInstance().bar()`, `Config.create(opts).build()` — lost the receiver's type, so the chained method either didn't resolve at all or (when a same-named method existed on an unrelated class) attached to whichever class was indexed first. Ports the #645 (C++) / #608 (PHP) 3-part mechanism: - Part 1: capture Java return types in the extractor (skip void/primitives/arrays, unwrap generics, strip package qualifier). - Part 2: encode a chained-call receiver as `inner().method` with normalized empty parens, so factory calls that take arguments still split. - Part 3: matchJavaCallChain resolves the chained method on the factory's return type, validated via resolveMethodOnType so a wrong inference yields NO edge (never a wrong one). Validated: synthetic decoy + absent-method safety tests; real-repo A/B on google/guava (3,227 files) — node count identical (no explosion), 0 edges lost, +1,507 unique chained edges recovered, precision spot-checked verbatim (Splitter.on().split(), CacheBuilder.newBuilder().recordStats(), GraphBuilder.directed().build(), nested MultimapBuilder.linkedHashKeys().arrayListValues()). EXTRACTION_VERSION 5 -> 6. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
eb5960b535
commit
7f6bdf7ad1
@@ -21,4 +21,4 @@
|
||||
* turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty
|
||||
* in the product is load-bearing").
|
||||
*/
|
||||
export const EXTRACTION_VERSION = 5;
|
||||
export const EXTRACTION_VERSION = 6;
|
||||
|
||||
@@ -2,6 +2,40 @@ import type { Node as SyntaxNode } from 'web-tree-sitter';
|
||||
import { getNodeText, getChildByField } from '../tree-sitter-helpers';
|
||||
import type { LanguageExtractor } from '../tree-sitter-types';
|
||||
|
||||
/**
|
||||
* Tree-sitter-java node types for a method's `type` (return) field that can
|
||||
* never be a method receiver — there's no class to chain a `.method()` on, so we
|
||||
* store no `returnType` for them.
|
||||
*/
|
||||
const JAVA_NON_CLASS_RETURN_NODES = new Set([
|
||||
'void_type',
|
||||
'integral_type', // int, long, short, byte, char
|
||||
'floating_point_type', // float, double
|
||||
'boolean_type',
|
||||
]);
|
||||
|
||||
/**
|
||||
* A Java method's declared return type, normalized to the bare class name a
|
||||
* chained `Foo.getInstance().bar()` could be called on (the #645/#608 mechanism).
|
||||
* Reads the `type` field: primitives/void/arrays yield undefined (no class to
|
||||
* chain on), `List<Foo>` is unwrapped to its base type `List`, and a dotted
|
||||
* package/outer-class qualifier (`java.util.List`) is stripped to the simple
|
||||
* name. Constructors have no `type` field → undefined.
|
||||
*/
|
||||
function extractJavaReturnType(node: SyntaxNode, source: string): string | undefined {
|
||||
const typeNode = getChildByField(node, 'type');
|
||||
if (!typeNode) return undefined;
|
||||
if (JAVA_NON_CLASS_RETURN_NODES.has(typeNode.type)) return undefined;
|
||||
// An array return (`Foo[]`) isn't a receiver you call instance methods on.
|
||||
if (typeNode.type === 'array_type') return undefined;
|
||||
// Strip type arguments (`List<Foo>` → `List`) — the chain resolves on the base.
|
||||
const raw = getNodeText(typeNode, source).trim().replace(/<[^>]*>/g, '');
|
||||
// Strip a dotted package / outer-class qualifier (`java.util.List` → `List`).
|
||||
const last = raw.split('.').pop()?.trim();
|
||||
if (!last || !/^[A-Za-z_]\w*$/.test(last)) return undefined;
|
||||
return last;
|
||||
}
|
||||
|
||||
export const javaExtractor: LanguageExtractor = {
|
||||
functionTypes: [],
|
||||
classTypes: ['class_declaration'],
|
||||
@@ -23,6 +57,7 @@ export const javaExtractor: LanguageExtractor = {
|
||||
bodyField: 'body',
|
||||
paramsField: 'parameters',
|
||||
returnField: 'type',
|
||||
getReturnType: extractJavaReturnType,
|
||||
getSignature: (node, source) => {
|
||||
const params = getChildByField(node, 'parameters');
|
||||
const returnType = getChildByField(node, 'type');
|
||||
|
||||
@@ -2376,6 +2376,33 @@ export class TreeSitterExtractor {
|
||||
return;
|
||||
}
|
||||
|
||||
// Java static-factory / fluent chain: `Foo.getInstance().bar()` — the
|
||||
// receiver is itself a method call, so resolution must infer bar's class
|
||||
// from what `Foo.getInstance` RETURNS (its declared return type), the
|
||||
// #645/#608 mechanism. Encode `<inner-receiver>.<inner-method>().<method>`;
|
||||
// the `().` marker lets the Java chain resolver split it, and normalizing to
|
||||
// empty parens drops any factory args (`Foo.create(cfg).bar()`) that would
|
||||
// otherwise leave a `(cfg)` in the receiver text and break the split.
|
||||
if (
|
||||
methodName &&
|
||||
this.language === 'java' &&
|
||||
objectField.type === 'method_invocation'
|
||||
) {
|
||||
const innerObj = getChildByField(objectField, 'object');
|
||||
const innerName = getChildByField(objectField, 'name');
|
||||
if (innerObj && innerName) {
|
||||
calleeName = `${getNodeText(innerObj, this.source)}.${getNodeText(innerName, this.source)}().${methodName}`;
|
||||
this.unresolvedReferences.push({
|
||||
fromNodeId: callerId,
|
||||
referenceName: calleeName,
|
||||
referenceKind: 'calls',
|
||||
line: node.startPosition.row + 1,
|
||||
column: node.startPosition.column,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let receiverName: string;
|
||||
if (objectField.type === 'field_access') {
|
||||
const inner = getChildByField(objectField, 'object');
|
||||
|
||||
@@ -576,6 +576,39 @@ export function matchPhpCallChain(
|
||||
return resolveMethodOnType(resolvedClass, method, ref, context, 0.85, 'instance-method');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a Java chained call whose receiver is a static factory / fluent call —
|
||||
* `Foo.getInstance().bar()`, encoded by the extractor as `Foo.getInstance().bar`
|
||||
* (#645/#608 mechanism). The receiver's type is what `Foo.getInstance` returns
|
||||
* (its declared return type); the outer method is then resolved and VALIDATED on
|
||||
* it (resolveMethodOnType requires `Type::method` to exist), so a wrong inference
|
||||
* yields no edge rather than a wrong one (e.g. a same-named `bar()` on an
|
||||
* unrelated class is never matched).
|
||||
*/
|
||||
export function matchJavaCallChain(
|
||||
ref: UnresolvedRef,
|
||||
context: ResolutionContext,
|
||||
): ResolvedRef | null {
|
||||
const m = ref.referenceName.match(/^(.+)\(\)\.(\w+)$/);
|
||||
if (!m || !m[1] || !m[2]) return null;
|
||||
const inner = m[1]; // `Foo.getInstance`
|
||||
const method = m[2]; // `bar`
|
||||
// Require an explicit receiver (`Receiver.factory`) — a bare `factory().bar`
|
||||
// chain (a method on `this`) isn't handled here.
|
||||
const lastDot = inner.lastIndexOf('.');
|
||||
if (lastDot <= 0) return null;
|
||||
const factoryClass = inner.slice(0, lastDot).split('.').pop(); // simple class name
|
||||
const factoryMethod = inner.slice(lastDot + 1);
|
||||
if (!factoryClass || !factoryMethod) return null;
|
||||
const ret = lookupCalleeReturnType(`${factoryClass}::${factoryMethod}`, ref, context);
|
||||
if (!ret) return null;
|
||||
// When several classes share the returned simple name, the caller file's
|
||||
// import of that type is the only signal that names WHICH one (#314).
|
||||
const imports = context.getImportMappings(ref.filePath, ref.language);
|
||||
const importedFqn = imports.find((i) => i.localName === ret)?.source;
|
||||
return resolveMethodOnType(ret, method, ref, context, 0.85, 'instance-method', importedFqn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Java/Kotlin: infer a receiver's declared type by walking field declarations
|
||||
* in the class enclosing the call site. The field's `signature` is already in
|
||||
@@ -1006,6 +1039,14 @@ export function matchReference(
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
// 1d. Java chained static-factory / fluent call — `Foo.getInstance().bar()`
|
||||
// encoded as `Foo.getInstance().bar` (#645/#608 mechanism). Resolve bar's class
|
||||
// from getInstance's declared return type, then validate the method on it.
|
||||
if (ref.language === 'java') {
|
||||
result = matchJavaCallChain(ref, context);
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
// 2. Method call pattern
|
||||
result = matchMethodCall(ref, context);
|
||||
if (result) return result;
|
||||
|
||||
Reference in New Issue
Block a user