mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 22:43:17 +08:00
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 main60b0d7a9(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 main60b0d7a9, 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>
197 lines
6.5 KiB
TypeScript
197 lines
6.5 KiB
TypeScript
/**
|
|
* 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']);
|
|
});
|
|
});
|