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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions __tests__/fixtures/kernel-parity/Torture.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,9 @@ public void on() {}
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(); } };
}
196 changes: 196 additions & 0 deletions __tests__/java-static-field-receivers.test.ts
Original file line number Diff line number Diff line change
@@ -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']);
});
});
9 changes: 8 additions & 1 deletion codegraph-kernel/src/java.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand All @@ -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" {
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 5 additions & 1 deletion src/extraction/languages/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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')) {
Expand All @@ -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') {
Expand Down
Loading