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 @@ -261,6 +261,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Kotlin, a `private val` or `private var` property, and the members of a `private` class or object, no longer take calls from other files. Properties were indexed without their visibility, so a call like `token(...)` in one file could link to another class's `private var token`, and JavaScript, Go or Python callers could land on a Kotlin test's private property; a call to `now()` could also land on a method of a test's `private class Clock`. Re-index Kotlin projects after upgrading. Thanks @danusha2345.
- In Kotlin, a call on a library type you import, like Jetpack Compose's `Modifier.pad()`, now links to the extension your project declares on that type, `fun Modifier.pad()`, when the calling file can see it: in the same package, or imported by name or with `.*`. The library type's own methods, like `Modifier.fillMaxSize()`, still never link to a project method that shares the name. Re-index Kotlin projects after upgrading. Thanks @danusha2345.
- In Kotlin, when two packages each declare a class with the same name, a call on a value of that type now links to the class the file imports, even when that class lives in a file named for something else, like `FooConverter` in `Converters.kt`. Before, it went to whichever of the two came first. Java calls on such a class, like okhttp's Java tests calling the `mockwebserver3.MockWebServer` they import, now link through the type their variable is declared with (#1942). Re-index Kotlin projects after upgrading. Thanks @arttttt.
- In Kotlin and Java, a call through an import of a class written in the other language now links to that class: Kotlin's `JUtil.make()`, `JUtil.Nested.deep()` or `Outer.Inner()` on an imported Java class, and Java's `KUtil.INSTANCE.kmake()` on a Kotlin object, `Factory.Companion.create()` on a companion object or `UtilsKt.helper()` on a Kotlin file's top-level function, whose import now links that file. Before, the import was only matched against files in the calling file's own language, so the call went to a same-named class in another package, or to nothing. A Kotlin extension declared in another package, like `fun Modes.hidden()`, is now reached only from a file that imports it or its package. Re-index Kotlin and Java projects after upgrading. Thanks @mixxer.
- In JavaScript and TypeScript, a `super.run()` call now links to the method of the class's parent, or of the nearest ancestor that has it, like three.js's `super.copy(source)` reaching `Object3D.copy`, so `codegraph_explore`'s call path and impact follow an override into the method it builds on. Before, such a call inside a method of the same name was dropped, and elsewhere it went to whatever method had the name, often the class's own or a sibling class's in the same file. A call from a static method reaches the parent's static method, and a call to a parent from outside your project, like `HTMLElement`, `Set` or `Promise`, links nothing. Re-index JavaScript and TypeScript projects after upgrading. Thanks @Puma7 for the report and the pull request. (#2497)
- In JavaScript and TypeScript, a named class expression, like `register(class Widget extends Base {…})`, `customElements.define('x-card', class XCard extends HTMLElement {…})` or `const Panel = class PanelImpl extends Base {…}`, is now indexed as a class with its own methods and the class it extends, so calls between its methods and its `super` calls link as a class declaration's do. Before, its methods were free functions or weren't indexed at all, and it extended nothing. Its name stays bound inside the expression alone, as JavaScript has it: `new Widget()` elsewhere links nothing, a constant, class or interface of the same name around it keeps its links, and code outside never reaches its methods by a guess. A class a CommonJS module exports, like webpack's `module.exports = class MultiCompiler {…}` or `exports.Store = class Store {…}`, is reached through `require` as a class declaration is, along with its static methods and the methods called on its instances. Re-index JavaScript and TypeScript projects after upgrading. Thanks @Puma7 for the report and the pull request. (#2499)
- In Go, a call through a struct field that holds a function, like prometheus's `d.refreshf(ctx)`, now reaches the functions put in that field, so `codegraph_explore` follows service discovery from `Discovery.Run` into each provider's `refresh` and names the line where each provider wires its method in, like `RefreshF: d.refresh`. The field can be set in a struct literal or by an assignment, copied from another struct's field (`refreshf: opts.RefreshF`), or given a method of an interface value, and two unrelated structs with a field of the same name, like `fn` or `handler`, stay apart. Before, such a flow stopped at the field. Re-index Go projects after upgrading.
Expand Down
195 changes: 195 additions & 0 deletions __tests__/jvm-cross-language-imports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/**
* A Java or Kotlin import names a declaration by its qualified name, whichever
* JVM language declares it. Kotlin calls Java classes through their imports
* (`JUtil.make()`, `JUtil.Nested.deep()`, `Outer.Inner()`), and Java calls a
* Kotlin object through `INSTANCE`, a companion through `Companion`, and a
* Kotlin file's top-level functions through the class named for the file
* (`UtilsKt.helper()`). Before, the import was matched against a file path
* with the calling file's own extension, so it never bound across languages:
* the call went to a same-named class in another package, or nowhere. A
* Kotlin extension declared in another package is in scope only through an
* import of it or of its package.
*
* Cases from @mixxer's #1948 (and the T5 triage fixtures built on it).
*/
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-jvm-cross-imports-'));
const files: Record<string, string> = {
'p/JUtil.java': `package p;
public class JUtil {
public static int make() { return 1; }
public static class Nested { public static int deep() { return 2; } }
}
`,
'other/JUtil.java': `package other;
public class JUtil {
public static int make() { return 9; }
public static class Nested { public static int deep() { return 9; } }
}
`,
'p/Outer.java': `package p;
public class Outer { public static class Inner { public Inner() {} } }
`,
'p/Plain.java': `package p;
public class Plain { public static class Nest { void run() {} } }
`,
'p/KUtil.kt': `package p

object KUtil {
fun kmake(): Int = 1
}

class KCompanion {
companion object {
fun make(): Int = 1
}
}
`,
'other/KUtil.kt': `package other

object KUtil {
fun kmake(): Int = 9
}

class KCompanion {
companion object {
fun make(): Int = 9
}
}
`,
'p/Utils.kt': `package p

fun helper(): Int = 1
`,
'other/Utils.kt': `package other

fun helper(): Int = 9
`,
'p/Modes.kt': `package p

object Modes

fun Modes.shared() {}
`,
'p/Ext.kt': `package p

fun Modes.extOther() {}
fun Modes.hidden() {}
`,
'q/FromKotlin.kt': `package q

import p.JUtil
import p.Outer
import p.Plain
import p.Modes
import p.extOther

class FromKotlin {
fun staticCall() { JUtil.make() }
fun nestedStatic() { JUtil.Nested.deep() }
fun javaCtor() { Outer.Inner() }
fun javaNoCtor() { Plain.Nest() }
fun importedExtension() { Modes.extOther() }
fun unimportedExtension() { Modes.hidden() }
fun besideTypeNotImported() { Modes.shared() }
}
`,
'q2/Shared.kt': `package q2

import p.Modes
import p.shared

fun useShared() { Modes.shared() }
`,
'q/FromJava.java': `package q;

import p.KUtil;
import p.KCompanion;
import p.UtilsKt;

public class FromJava {
void objectCall() { KUtil.INSTANCE.kmake(); }
void companionCall() { KCompanion.Companion.make(); }
void topLevelCall() { UtilsKt.helper(); }
}
`,
};
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 });
});

/** `kind qualified-name` of every call or instantiation `caller` makes. */
function callees(file: string, caller: string): string[] {
const from = cg.getNodesInFile(file).find((n) => n.name === caller)!;
return cg
.getOutgoingEdgesFrom([from.id])
.filter((e) => e.kind === 'calls' || e.kind === 'instantiates')
.map((e) => `${e.kind} ${cg.getNode(e.target)!.qualifiedName}`)
.sort();
}

describe('Kotlin calls a Java class through its import', () => {
it('reaches the imported package\'s class, not a same-named one elsewhere', () => {
expect(callees('q/FromKotlin.kt', 'staticCall')).toEqual(['calls p::JUtil::make']);
});

it('walks a nested class', () => {
expect(callees('q/FromKotlin.kt', 'nestedStatic')).toEqual(['calls p::JUtil::Nested::deep']);
});

it('builds a nested class through its own constructor, or instantiates one that declares none', () => {
expect(callees('q/FromKotlin.kt', 'javaCtor')).toEqual(['calls p::Outer::Inner::Inner']);
expect(callees('q/FromKotlin.kt', 'javaNoCtor')).toEqual(['instantiates p::Plain::Nest']);
});
});

describe('Java calls Kotlin through its import', () => {
it('reaches an object\'s member through INSTANCE', () => {
expect(callees('q/FromJava.java', 'objectCall')).toEqual(['calls p::KUtil::kmake']);
});

it('reaches a companion\'s member through Companion', () => {
expect(callees('q/FromJava.java', 'companionCall')).toEqual(['calls p::KCompanion::make']);
});

it('reaches a top-level function through its file\'s class, and the import links that file', () => {
expect(callees('q/FromJava.java', 'topLevelCall')).toEqual(['calls p::helper']);
const ids = cg.getNodesInFile('q/FromJava.java').map((n) => n.id);
const imported = cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === 'imports')
.map((e) => cg.getNode(e.target)!)
.map((n) => (n.kind === 'file' ? `file ${n.filePath}` : `${n.kind} ${n.qualifiedName}`))
.sort();
expect(imported).toEqual(['class p::KCompanion', 'class p::KUtil', 'file p/Utils.kt']);
});
});

describe('A Kotlin extension in another package', () => {
it('is in scope through an import of it, and otherwise not', () => {
expect(callees('q/FromKotlin.kt', 'importedExtension')).toEqual(['calls Modes::extOther']);
expect(callees('q/FromKotlin.kt', 'unimportedExtension')).toEqual([]);
});

it('is not in scope through an import of its receiver type alone, even beside that type', () => {
expect(callees('q/FromKotlin.kt', 'besideTypeNotImported')).toEqual([]);
expect(callees('q2/Shared.kt', 'useShared')).toEqual(['calls Modes::shared']);
});
});
Loading