fix(java): a call on a value a type holds in a static field reaches the method of the field's type (#3055)

A Java call written through a type's static field, Consts.HELPER.go()
or Holder.SHARED.run(), was resolved by the method's name alone: the
receiver Type.FIELD said nothing of its type, so the call went to a
same-named method of whatever class the name matcher found, and a field
of a library class (Intent.ACTION_VIEW.equals(...), System.out.print())
could reach a project method of that name. An interface's constants were
not indexed at all (neither engine read a constant_declaration), so a
constant a type declares in an interface, or inherits from one, had no
declaration to read.

A Java Type.FIELD.m() (Outer.Inner.FIELD.m(), a package written in
front) now finds the type as the calling file names it (javaTypeRef),
then the field, the type's own or one it inherits (five levels up), and
calls the method on the value the field's declaration gives it, read
with the field's own file's imports: an enum constant's own override,
else the enum's method; the method of the anonymous class the field is
initialized with (a CREATOR's createFromParcel); the class a new X(...)
initializer builds; else the type the declaration writes. A Kotlin
object's INSTANCE or Companion reaches the object's member. A library
owner, or a field of a library type, links nothing. A receiver whose
head is a local or a field links as before. Both engines index an
interface's constant_declaration as a static final constant.

Validation:
- java-static-field-receivers.test.ts: 9 tests, 7 fail on main 60b0d7a9
  (two already pass there: a field typed by its own declaration and an
  anonymous class's method), all pass here.
- Kernel parity (java), 0 files with diffs: Torture.java 1/1,
  TortureRecord.java 1/1, jextract 218/218, spring-modulith 682/682,
  SystemUI 1141/1141, okhttp 71/71, Minestom 1859/1894 (deferrals are
  main's).
- A/B against main 60b0d7a9, sites added/removed/retargeted (conf-only):
  Minestom +26003/-8/122 (751; 12,584 interface constants now indexed,
  409 calls typed), spring-ai +14/-21/786 (823; 719 enum constants' own
  overrides), kestra +27/-273/14 (37), playframework +5/-1/0 (21),
  SystemUI 0/-11/0 (2), spring-modulith +5/-3/4 (26), jextract
  +1/-9/6 (1); okhttp, nowinandroid, mihon, click, gin, serilog
  identical. The removed sites are guesses on library fields and
  methods (kestra's HttpStatus.X.getCode() x180).
- Cost (median of 3 interleaved): SystemUI CPU 92.0 -> 93.7 s, wall
  59.4 -> 58.6 s; Minestom CPU 46.7 -> 56.4 s, wall 17.5 -> 19.4 s, as
  its graph grows by the generated registries' 12,584 interface
  constants (29% more nodes, 30% more edges).

Reimplements #1949 by @mixxer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-30 09:46:25 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 7021de5c30
commit bc9291afe6
6 changed files with 340 additions and 2 deletions
+1
View File
@@ -266,6 +266,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Java and C#, a method called on a newly constructed object, like `new SqlSessionFactoryBuilder().build(reader)` or `new JsonSerializer().Serialize(writer, value)`, now links to that class's own or inherited method. When several packages or namespaces declare a class of that name, it links to the one the file imports, and to none when the file could mean either. Re-index Java and C# projects after upgrading.
- In Java, a `record` is now indexed as a class, with its methods, its constructors, a field for each component and the accessor each component gets, like `info.remoteAddress()`. Before, a record had no symbol of its own and its methods were filed under the package or the class around it, so a call on a record-typed value linked to nothing or to the wrong method, and records were missing from callers, impact and implementations. A call like `Circle.unit()` on a type that a sealed interface's `permits` list names first now links too. Re-index Java projects after upgrading. Thanks @danusha2345.
- In Java, a call now resolves on the type its receiver is declared with in more cases: a field of an outer class read inside an inner or anonymous class, `Outer.this.field`, a chain of fields like `this.owner.repo.save()`, a local declared with type arguments, a for-each variable, a variable declared with its package, like `org.app.Message message`, and a nested class that a parent class declares. When that type comes from a library, like Android's `Parcel`, `Handler` or `Notification.Builder`, or `java.util.List`, the call no longer links to a same-named method of your own. A type the file imports from a library (`import java.util.Iterator`) or as a nested class (`import p.Http.Cookie`) is no longer mistaken for a same-named type elsewhere in the project, and a Java type wins over a same-named Scala one. Re-index Java projects after upgrading. Thanks @danusha2345.
- In Java, a call on a value a type holds in a static field, like `Consts.HELPER.go()`, `Holder.SHARED.run()`, `Outer.Inner.NESTED.run()`, an enum constant's `Color.RED.hex()` or a `CREATOR`'s `createFromParcel(…)`, now links to the method of the field's type, as the field's declaration reads with its own file's imports: the class a `new FastRunner()` initializer builds, or the anonymous class the field holds. A field the type inherits counts, an enum constant's own override comes first, and a Kotlin object's `INSTANCE` reaches the object's member. An interface's constants are now indexed. Before, such a call was guessed by the method's name, often onto a same-named class in another package, and a field of a library class, like `Intent.ACTION_VIEW.equals(…)`, could link to a project method of that name; it now links nothing. Re-index Java projects after upgrading. Thanks @mixxer.
- In Kotlin, a call on what one of your functions returns, like `repository().pending(1)` or `val repo = repository(); repo.pending(1)` with a `private fun repository(): RealRepo` in a test class, now links to the method of the type that function declares, including one the type inherits or an extension on it. Before, the chained call went to whichever `pending` the name matched first, like a test double's in another package, and the local was guessed from its name, which reached the interface. The function is found in the calling class, a class it inherits from or one around it, or at the top level, and a nested type like `Request.Builder` is that type, not another class's `Builder`. When the declared type has no such method, the call now links to nothing rather than a method of the same name elsewhere; a type from outside your project is matched by name as before. Re-index Kotlin projects after upgrading. Thanks @mindcipherrr for the report. (#2805)
- In Kotlin, a call through a property now links to the method of the type the property is declared with, like `server.enqueue(…)` on a `lateinit var server: MockWebServer` or a call on a constructor property such as `class A(private val repo: Repo)`. So does a call through a chain like `engine.pump.drain()` or `Mode.ON.next()`, through a property one of your classes inherits, through a local set from a call like `val lease = HardwareLock.tryBegin()`, and through a local of the function around an anonymous `object : …`. Before, such a call went to an interface's method or to whichever method had the name: on okhttp, the tests' calls reached the deprecated `MockWebServer` instead of the one each file imports. A call on a type from outside your project, like `Regex` or a JDK class, no longer links to a same-named method of your own, unless your project extends that type with it. Re-index Kotlin projects after upgrading. Thanks @danusha2345.
- In Kotlin, a `fun interface` is now indexed as an interface with its methods, like okhttp's `HttpLoggingInterceptor.Logger`. Before, it was read as a broken function, and when a comment followed it, the declaration after it went missing too, its members turning into loose functions. Re-index Kotlin projects after upgrading. Thanks @danusha2345.
@@ -140,3 +140,9 @@ class DottedAnon {
public Integer call() { return 1; }
};
}
interface TortureConsts {
int MAX = 10;
Helper HELPER = new Helper();
String NAME = "x", OTHER = "y";
Runnable TASK = new Runnable() { public void run() { go(); } };
}
@@ -0,0 +1,196 @@
/**
* Java's `Type.FIELD.m()` calls a method on a value a type holds in a static
* field: an interface's constant, a class's `static final`, an enum constant.
* The value is typed by the field's own declaration, with its own file's
* imports: the method an anonymous class the field is initialized with
* declares, else the class a `new X(…)` builds, else the declared type's.
* Before, nothing read the field: an interface's constants weren't indexed
* at all, and the call was guessed by the method's name alone, often onto a
* same-named class in another package, or onto a project method for a field
* of a library class (`Intent.ACTION_VIEW.equals(…)`).
*
* Cases from @mixxer's #1949 and the T5 triage fixtures.
*/
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-static-field-'));
const files: Record<string, string> = {
'r/Consts.java': `package r;
public interface Consts {
int MAX = 10;
Helper HELPER = new Helper();
String NAME = "x";
}
`,
'r/Helper.java': `package r;
public class Helper { public void go() {} }
`,
'r/Holder.java': `package r;
public class Holder {
public static final Helper SHARED = new Helper();
}
`,
'r/User.java': `package r;
public class User {
void viaInterface() { Consts.HELPER.go(); }
void viaClass() { Holder.SHARED.go(); }
}
`,
'com/lib/ProjectInterpolator.java': `package com.lib;
public class ProjectInterpolator { public float getInterpolation(float t) { return t; } }
`,
'p/ProjectInterpolator.java': `package p;
public class ProjectInterpolator { public float getInterpolation(float t) { return t; } }
`,
'r/ProjectInterpolators.java': `package r;
import p.ProjectInterpolator;
class ProjectInterpolators { static final ProjectInterpolator LOCAL = new ProjectInterpolator(); }
`,
'r/Consumer.java': `package r;
public class Consumer {
float projectCall() { return ProjectInterpolators.LOCAL.getInterpolation(1); }
}
`,
's/Shapes.java': `package s;
import android.os.Parcel;
import android.os.Parcelable;
import android.content.Intent;
import android.view.animation.Interpolators;
class Parent {
public static final Runner SHARED = new Runner();
}
class Child extends Parent {}
class Runner { public void run() {} public String getName() { return "r"; } }
class FastRunner extends Runner { @Override public void run() {} }
interface Defaults {
Runner DEFAULT = new FastRunner();
}
class Impl implements Defaults {}
class Outer {
static class Inner {
static final Runner NESTED = new Runner();
}
}
enum Color {
RED,
GREEN {
@Override String hex() { return "0f0"; }
};
String hex() { return "f00"; }
}
class Foo implements Parcelable {
public static final Parcelable.Creator<Foo> CREATOR = new Parcelable.Creator<Foo>() {
public Foo createFromParcel(Parcel in) { return new Foo(); }
public Foo[] newArray(int size) { return new Foo[size]; }
};
}
class Calls {
void inherited() { Child.SHARED.run(); }
void interfaceConstant() { Impl.DEFAULT.run(); }
void nested() { Outer.Inner.NESTED.run(); }
void enumConstant() { Color.RED.hex(); }
void enumBody() { Color.GREEN.hex(); }
void anonymous(Parcel p) { Foo.CREATOR.createFromParcel(p); }
boolean library(String a) { return Intent.ACTION_VIEW.equals(a); }
float libraryField(float t) { return Interpolators.FAST_OUT_SLOW_IN.getInterpolation(t); }
String javaLang() { return Integer.TYPE.getName(); }
void kotlinObject() { KObj.INSTANCE.go(); }
}
`,
's/KObj.kt': `package s
object KObj {
fun go() {}
}
class Other {
fun equals(x: String): Boolean = true
}
`,
};
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 });
});
/** Qualified names of what the method `name` in `file` calls. */
function callees(file: string, name: string): string[] {
const from = cg.getNodesInFile(file).find((n) => n.kind === 'method' && n.name === name)!;
return cg
.getOutgoingEdgesFrom([from.id])
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!.qualifiedName)
.sort();
}
describe('Java: an interface constant', () => {
it('is indexed, as a static final field is', () => {
const constants = cg.getNodesInFile('r/Consts.java').filter((n) => n.kind === 'constant').map((n) => n.qualifiedName);
expect(constants.sort()).toEqual(['r::Consts::HELPER', 'r::Consts::MAX', 'r::Consts::NAME']);
});
});
describe('Java: Type.FIELD.m()', () => {
it('reaches the method of the field\'s type', () => {
expect(callees('r/User.java', 'viaInterface')).toEqual(['r::Helper::go']);
expect(callees('r/User.java', 'viaClass')).toEqual(['r::Helper::go']);
});
it('reads the field\'s type with the field\'s own file\'s imports', () => {
expect(callees('r/Consumer.java', 'projectCall')).toEqual(['p::ProjectInterpolator::getInterpolation']);
});
it('finds a field the type inherits, from a class or an interface', () => {
expect(callees('s/Shapes.java', 'inherited')).toEqual(['s::Runner::run']);
expect(callees('s/Shapes.java', 'interfaceConstant')).toEqual(['s::FastRunner::run']);
});
it('walks a nested type', () => {
expect(callees('s/Shapes.java', 'nested')).toEqual(['s::Runner::run']);
});
it('reaches an enum constant\'s own override, else the enum\'s method', () => {
expect(callees('s/Shapes.java', 'enumConstant')).toEqual(['s::Color::hex']);
expect(callees('s/Shapes.java', 'enumBody')).toEqual(['s::Color::GREEN::hex']);
});
it('reaches the method of an anonymous class the field holds', () => {
expect(callees('s/Shapes.java', 'anonymous')).toEqual(['s::Foo::CREATOR::<Creator$anon@39>::createFromParcel']);
});
it('links nothing for a library type\'s field', () => {
expect(callees('s/Shapes.java', 'library')).toEqual([]);
expect(callees('s/Shapes.java', 'libraryField')).toEqual([]);
expect(callees('s/Shapes.java', 'javaLang')).toEqual([]);
});
it('reaches a Kotlin object\'s member through INSTANCE', () => {
expect(callees('s/Shapes.java', 'kotlinObject')).toEqual(['s::KObj::go']);
});
});
+8 -1
View File
@@ -429,6 +429,10 @@ impl<'t> Walker<'t> {
}
fn is_static(&self, node: Node) -> bool {
// An interface constant is implicitly `public static final`.
if node.kind() == "constant_declaration" {
return true;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "modifiers" && self.text(child).contains("static") {
@@ -441,6 +445,9 @@ impl<'t> Walker<'t> {
/// javaExtractor.isConst: `static final` field → constant.
fn is_const(&self, node: Node) -> bool {
if node.kind() == "constant_declaration" {
return true;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "modifiers" {
@@ -510,7 +517,7 @@ impl<'t> Walker<'t> {
} else if kind == "enum_declaration" {
self.extract_enum(node);
skip_children = true;
} else if kind == "field_declaration" && self.inside_class_like() {
} else if (kind == "field_declaration" || kind == "constant_declaration") && self.inside_class_like() {
// The initializers extract_field walked under their fields took
// their own candidates; the scan covers the rest.
let walked = self.extract_field(node);
+5 -1
View File
@@ -331,7 +331,8 @@ export const javaExtractor: LanguageExtractor = {
importTypes: ['import_declaration'],
callTypes: ['method_invocation'],
variableTypes: ['local_variable_declaration'],
fieldTypes: ['field_declaration'],
// An interface's constants (`Helper HELPER = new Helper();`) are fields too.
fieldTypes: ['field_declaration', 'constant_declaration'],
nameField: 'name',
bodyField: 'body',
paramsField: 'parameters',
@@ -358,6 +359,8 @@ export const javaExtractor: LanguageExtractor = {
return undefined;
},
isStatic: (node) => {
// An interface constant is implicitly `public static final`.
if (node.type === 'constant_declaration') return true;
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'modifiers' && child.text.includes('static')) {
@@ -370,6 +373,7 @@ export const javaExtractor: LanguageExtractor = {
// shared config). Drives `constant` kind so value-reference edges target it;
// instance / `final`-only / `static`-only fields stay mutable `field`s.
isConst: (node) => {
if (node.type === 'constant_declaration') return true;
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'modifiers') {
+124
View File
@@ -19811,6 +19811,123 @@ function resolveOnJavaType(
return null;
}
/** `Type.FIELD`, `Outer.Inner.FIELD`, `pkg.Type.FIELD`: a value a type holds, as a call's receiver. */
const JAVA_STATIC_FIELD_RECEIVER = /^((?:[a-z_$][\w$]*\.)*(?:[A-Z][\w$]*\.)*[A-Z][\w$]*)\.([A-Za-z_$][\w$]*)$/;
/**
* The static field `name` of Java type `owner`, its own or one it inherits
* from the types it extends or implements (an interface's constant), a few
* levels up. An enum's constant counts.
*/
function javaStaticField(owner: Node, name: string, context: ResolutionContext): Node | undefined {
const seen = new Set<string>([owner.id]);
let frontier = [owner];
for (let depth = 0; depth < 5 && frontier.length > 0; depth++) {
const next: Node[] = [];
for (const type of frontier) {
const own = context
.getNodesByQualifiedName(`${type.qualifiedName}::${name}`)
.find((n) => n.filePath === type.filePath && (n.kind === 'field' || n.kind === 'constant' || n.kind === 'enum_member'));
if (own) return own;
for (const sup of declaredSupertypes(type, context)) {
const written = javaTypeName(sup);
const ref = written ? javaTypeRef(written, type.filePath, context) : null;
const tail = written?.split('.').pop() ?? '';
const decls = ref?.decl ? [ref.decl]
: ref ? jvmTypeDecls(tail.split('::').pop()!, context).filter((d) => d.qualifiedName === ref.name || d.qualifiedName.endsWith(`::${ref.name}`))
: [];
for (const decl of decls) {
if (seen.has(decl.id)) continue;
seen.add(decl.id);
next.push(decl);
}
}
}
frontier = next;
}
return undefined;
}
/**
* Java's `Type.FIELD.m()` — `Consts.HELPER.go()`, `Outer.Inner.SHARED.run()`,
* `ProjectInterpolators.LOCAL.getInterpolation(t)`, `Color.RED.hex()`: a call
* on a value a type holds in a static field, an interface's constant or an
* enum's constant. The field is the type's own or one it inherits, and its
* value is typed by its own declaration, read with its own file's imports: the
* method an anonymous class it is initialized with declares (a `CREATOR`'s
* `createFromParcel`), else the class a `new X(…)` initializer builds, else
* the declared type's. A Kotlin object's `INSTANCE` and a class's
* `Companion`, as Java writes them, are the type itself. Null — no edge — for
* a field of a type from outside the project (`Intent.ACTION_VIEW.equals(…)`)
* or a value of one; undefined when the receiver is no type's field (a local,
* a nested type), for the strategies below.
*/
function matchJavaStaticFieldCall(
receiver: string,
methodName: string,
ref: UnresolvedRef,
context: ResolutionContext,
): ResolvedRef | null | undefined {
const m = JAVA_STATIC_FIELD_RECEIVER.exec(receiver);
if (!m) return undefined;
const [, ownerWritten, fieldName] = m;
const head = ownerWritten!.split('.')[0]!;
// A local or a field of that name holds a value, not a type.
if (/^[A-Z]/.test(head) && (inferLocalReceiverType(head, ref, context) || inferJavaFieldReceiverType(head, ref, context))) return undefined;
const written = javaTypeName(ownerWritten!);
if (!written) return undefined;
const ownerType = javaTypeRef(written, ref.filePath, context);
if (!ownerType) return null;
const owners = ownerType.decl ? [ownerType.decl]
: jvmTypeDecls(ownerType.name.split('::').pop()!, context).filter((d) =>
(d.qualifiedName === ownerType.name || d.qualifiedName.endsWith(`::${ownerType.name}`)) && isJavaTypeNameable(d, ref.filePath, context));
const owner = owners.length === 1 ? owners[0]! : owners.find((d) => d.filePath === ref.filePath);
if (!owner) return undefined;
const found = (n: Node | undefined): ResolvedRef | null =>
n ? { original: ref, targetNodeId: n.id, confidence: 0.9, resolvedBy: 'instance-method' } : null;
const methodOf = (typeQn: string, file: string): Node | undefined =>
context.getNodesByQualifiedName(`${typeQn}::${methodName}`).find((n) => n.kind === 'method' && n.filePath === file);
if (owner.language === 'kotlin' && (fieldName === 'INSTANCE' || fieldName === 'Companion')) {
return found(methodOf(owner.qualifiedName, owner.filePath));
}
// `Outer.Inner.m()` is a static call on a nested type, not on a field.
if (context.getNodesByQualifiedName(`${owner.qualifiedName}::${fieldName}`).some((n) => JVM_DECLARED_TYPE_KINDS.has(n.kind))) return undefined;
const field = javaStaticField(owner, fieldName!, context);
if (!field || field.language !== 'java') return undefined;
if (field.kind === 'enum_member') {
// A constant's own body overrides first (`PLUS { int apply(…) }`), then the enum's method.
const enumQn = field.qualifiedName.slice(0, field.qualifiedName.lastIndexOf('::'));
const enumDecl = context.getNodesByQualifiedName(enumQn).find((n) => n.filePath === field.filePath && JVM_DECLARED_TYPE_KINDS.has(n.kind));
const segments = enumQn.split('::');
const chain = segments.length > 1 && /^[a-z]/.test(segments[0]!) ? segments.slice(1).join('::') : enumQn;
return found(methodOf(field.qualifiedName, field.filePath) ?? methodOf(enumQn, field.filePath)) ??
(enumDecl ? resolveOnJavaType({ name: chain, decl: enumDecl }, methodName, ref, context) : null);
}
// An anonymous class the field is initialized with: its own method.
const anonMethod = context
.getNodesInFile(field.filePath)
.filter((n) => n.kind === 'class' && n.qualifiedName.startsWith(`${field.qualifiedName}::<`) && n.name.includes('$anon@'))
.map((anon) => methodOf(anon.qualifiedName, field.filePath))
.find((n) => n !== undefined);
if (anonMethod) return found(anonMethod);
// A `new X(…)` initializer builds X, the declared type's subtype at most.
const lines = context.getFileLines?.(field.filePath) ?? context.readFile(field.filePath)?.split(/\r?\n/) ?? [];
const declText = lines.slice(field.startLine - 1, Math.min(lines.length, (field.endLine ?? field.startLine))).join('\n');
const built = new RegExp(`\\b${field.name.replace(/\$/g, '\\$')}\\s*=\\s*new\\s+((?:[\\w$]+\\s*\\.\\s*)*[\\w$]+)\\s*(?:<[^()]*>)?\\s*\\(`).exec(declText)?.[1]?.replace(/\s+/g, '');
if (built) {
const builtName = javaTypeName(built);
const builtType = builtName ? javaTypeRef(builtName, field.filePath, context) : null;
if (builtType) {
const viaBuilt = resolveOnJavaType(builtType, methodName, ref, context);
if (viaBuilt) return viaBuilt;
}
}
const declared = fieldSignatureType(field);
if (!declared) return null;
const declaredType = javaTypeRef(declared, field.filePath, context);
return declaredType ? resolveOnJavaType(declaredType, methodName, ref, context) : null;
}
/**
* Java: the declared type of a receiver the field lookup reaches — a field
* (`mRepo`, `this.mRepo`, `Outer.this.mRepo`) or a chain of up to three
@@ -22335,6 +22452,13 @@ export function matchMethodCall(
if (typed !== undefined) rustBindingLine = typed.unread;
}
// A Java call on a value a type holds in a static field, `Consts.HELPER.go()`:
// typed by the field's declaration, or no edge for a library type's field.
if (ref.language === 'java' && dotMatch) {
const onField = nmTimedT('mc-javastaticfield', ref, () => matchJavaStaticFieldCall(objectOrClass!, methodName!, ref, context));
if (onField !== undefined) return onField;
}
// Infer the receiver's type from its local declaration/initializer in the
// enclosing scope, then resolve the method on that type (#1108). C++ keeps its
// dedicated inferrer (header scan + `auto`); every other language uses the