fix(java,ts): a parameterized supertype links its type, like implements Store<String>

A Java class implementing a parameterized interface (implements
Store<String>), one extending a generic base (extends Base<Integer>), a
Java interface's extends Store<T>, and a TypeScript class's implements
Store<string> got no implements or extends edge. The grammars wrap the
type in a generic_type node, and the supertype reader took its whole
text, which no node is named. Without the edge the interface-impl
synthesizer had nothing to walk, so a call on the interface stopped there.

Both engines (extractInheritance on the wasm path, the kernel's java.rs
and tsjs) now name a generic_type by its first child that is not
type_arguments; any other supertype node is its own text, as before.

Validation: the new tests fail 3 of 3 on main with both engines and pass
here. A/B against main 0d46dba0, kernel per arm: flask and Slim (PHP)
byte-identical. mybatis-3 gains 107 supertype edges (extends
BaseTypeHandler<T> 58 times, TypeHandler<T>, ResultHandler<T>,
BaseMapper<T>...) with 31 interface-impl and inherited calls, 2 calls
retargeted from another package's Mapper to the BaseMapper the interface
extends, and 17 inherited calls typed. pixijs gains 43 implements
(System<T>, RenderPipe<T>, InstructionPipe<T>...) and 11 interface-impl
calls; excalidraw 2 implements DeltaContainer<T> and 4 interface-impl
calls; vscode's base and editor 142 implements (IListRenderer<T>,
IListVirtualDelegate<T>, ISpliceable<T>...) and 183 interface-impl calls.
Nothing removed. Kernel parity holds on mybatis-3 (1,396 Java files),
pixijs, excalidraw and vscode's base and editor, 0 diffs. Index CPU time
within noise: mybatis-3 29.9 s to 30.1 s, pixijs 37.3 s to 38.1 s over 6
interleaved rounds in both orders (spread 35.8 to 38.5 s).

The older PR #729 by @paiyahooJay made the same fix in the wasm extractor;
it is credited in the CHANGELOG.

Original PR #2705 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
danusha2345
2026-10-23 19:21:32 +00:00
committed by Colby Mchenry
co-authored by Claude Opus 5.5
parent 428b3c487d
commit 8f8f04b7fc
5 changed files with 156 additions and 6 deletions
+1
View File
@@ -218,6 +218,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In TypeScript and JavaScript, a class in a `.tsx` or `.jsx` file now links its base class's and interfaces' methods to its overrides, as a class in a `.ts` or `.js` file already did. `codegraph_explore`'s call path, callers and impact now follow a call through the base into the override, like outline's `BaseEmail.send()` reaching each email's `render()`. This also works across file types, such as an interface in a `.ts` file implemented in a `.tsx` one. Before, these classes had no override links at all. Re-index projects with `.tsx` or `.jsx` files after upgrading.
- In Ruby, a class method, `def self.build` or a `def` inside `class << self`, is now told apart from an instance method of the same name. A call on the class, like `Widget.build`, links to the class method; a call on an instance, like `widget.build` after `widget = Widget.new`, links to the instance method; and a call without a receiver links to the one that matches the method it is written in. Before, both were stored as instance methods under one name, so a call linked to whichever the class declared first: in a service object with `def self.call` and `def call`, `card.call` on an instance linked to the class method. Re-index Ruby projects after upgrading.
- In Java, Kotlin, C# and Scala, each overload of an interface or base-class method now links only to the overrides with the same parameters. Before, every overload linked to every method of that name in the implementing class, so `ExampleService.putTairValue(String)` also led to `putTairValue(List<String>, double)`, and `codegraph_explore`'s call path, callers and impact went into overloads that call never runs. Where a parameter is a type parameter, like `save(T entity)` implemented as `save(String entity)`, or the class is written in another language than its interface, the number of parameters decides; a method declared once on each side links as before. Re-index Java, Kotlin, C# and Scala projects after upgrading. Thanks @hxj1225 for the report and @danusha2345 for the pull request. (#2663)
- In Java and TypeScript, a class that implements or extends a parameterized type, like `implements Store<String>`, `extends Base<Integer>`, an interface's `extends Store<T>` or TypeScript's `implements Store<string>`, is now linked to that type, so its methods get their interface-to-implementation links and `codegraph_explore` follows a call on the interface into the class. Before, such a supertype was read with its type arguments and matched nothing. Re-index Java and TypeScript projects after upgrading. Thanks @danusha2345, and @paiyahooJay for the earlier pull request. (#2705)
- 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 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)
+103
View File
@@ -0,0 +1,103 @@
/**
* A class that implements or extends a parameterized type is linked to that
* type. The Java and TypeScript grammars wrap `Store<String>` in a
* `generic_type`, and the supertype clause reader took the whole text as the
* name — which the index holds nothing under. So `class MapStore implements
* Store<String>` had no `implements` edge, and none of the interface-impl
* links that follow from it; the same for `extends Base<Integer>`, an
* interface's `extends Store<T>`, and TypeScript's `implements Store<string>`.
* The reference now names the type before its arguments, on the wasm path
* and in the kernel alike. (A package- or namespace-qualified supertype,
* `extends app.Base`, `implements ns.Other`, stays unresolved as before,
* with or without arguments.)
*/
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-generic-supertypes-'));
const files: Record<string, string> = {
'src/Store.java': `package app;
public interface Store<T> {
void save(T entity);
}
`,
'src/Sub.java': `package app;
interface Sub<T> extends Store<T> {}
`,
'src/MapStore.java': `package app;
import java.io.Serializable;
public class MapStore implements Store<String>, Serializable {
public void save(String entity) {}
}
`,
'src/Base.java': `package app;
public class Base<T> {
public void init() {}
}
`,
'src/Derived.java': `package app;
public class Derived extends Base<Integer> {
public void init() {}
}
`,
'src/store.ts': `export interface Store<T> { save(entity: T): void }
export interface Named { name: string }
export class MapStore implements Store<string>, Named {
name = 'map';
save(entity: string) {}
}
`,
};
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 the supertype edges of `name` in `file`. */
const supertypes = (file: string, name: string): string[] => {
const node = cg.getNodesInFile(file).find((n) => n.name === name && n.kind !== 'file')!;
expect(node, `${file} ${name}`).toBeDefined();
return cg.getOutgoingEdges(node.id).filter((e) => e.kind === 'extends' || e.kind === 'implements')
.map((e) => `${e.kind} -> ${cg.getNode(e.target)!.qualifiedName}`).sort();
};
describe('a parameterized supertype names its type (Java, TypeScript)', () => {
it('Java: implements Store<String>, extends Base<Integer>, interface extends Store<T>', () => {
expect(supertypes('src/MapStore.java', 'MapStore')).toEqual(['implements -> app::Store']);
expect(supertypes('src/Derived.java', 'Derived')).toEqual(['extends -> app::Base']);
expect(supertypes('src/Sub.java', 'Sub')).toEqual(['extends -> app::Store']);
});
it('Java: the interface method links its implementation, the base method its override', () => {
const save = cg.getNodesInFile('src/Store.java').find((n) => n.name === 'save')!;
expect(cg.getOutgoingEdges(save.id).filter((e) => e.kind === 'calls' && e.metadata?.synthesizedBy === 'interface-impl')
.map((e) => cg.getNode(e.target)!.qualifiedName)).toEqual(['app::MapStore::save']);
const init = cg.getNodesInFile('src/Base.java').find((n) => n.name === 'init')!;
expect(cg.getOutgoingEdges(init.id).filter((e) => e.kind === 'calls' && e.metadata?.synthesizedBy === 'interface-impl')
.map((e) => cg.getNode(e.target)!.qualifiedName)).toEqual(['app::Derived::init']);
});
it('TypeScript: implements Store<string>, beside a plain interface', () => {
expect(supertypes('src/store.ts', 'MapStore')).toEqual(['implements -> Named', 'implements -> Store']);
});
});
+16 -2
View File
@@ -1034,7 +1034,7 @@ impl<'t> Walker<'t> {
None => child.named_child(0).into_iter().collect(),
};
for target in targets {
let name = self.text(target).to_string();
let name = self.supertype_name(target);
self.push_ref_at(class_row, &name, extends_kind, target);
}
}
@@ -1047,7 +1047,7 @@ impl<'t> Walker<'t> {
None => (0..child.named_child_count()).filter_map(|j| child.named_child(j)).collect(),
};
for iface in targets {
let name = self.text(iface).to_string();
let name = self.supertype_name(iface);
self.push_ref_at(class_row, &name, implements_kind, iface);
}
}
@@ -1056,6 +1056,20 @@ impl<'t> Walker<'t> {
}
}
/// supertypeName — a parameterized supertype (`generic_type`) by the name
/// before its type arguments: `Store` for `implements Store<String>`.
fn supertype_name(&self, node: Node<'t>) -> String {
if node.kind() == "generic_type" {
let named = (0..node.named_child_count())
.filter_map(|j| node.named_child(j))
.find(|c| c.kind() != "type_arguments");
if let Some(named) = named {
return self.text(named).to_string();
}
}
self.text(node).to_string()
}
/// extractDecoratorsFor — Java annotations live inside `modifiers`.
fn extract_decorators_for(&mut self, decl: Node<'t>, decorated_row: u32) {
for i in 0..decl.named_child_count() {
+16 -2
View File
@@ -1984,14 +1984,14 @@ impl<'t> Walker<'t> {
// TS `extends_clause` (the other spellings are other grammars').
"extends_clause" | "superclass" | "base_clause" | "extends_interfaces" => {
if let Some(target) = child.named_child(0) {
let name = self.text(target).to_string();
let name = self.supertype_name(target);
self.push_ref(class_row, &name, extends_kind, target);
}
}
"implements_clause" | "class_interface_clause" | "super_interfaces" | "interfaces" => {
for j in 0..child.named_child_count() {
if let Some(iface) = child.named_child(j) {
let name = self.text(iface).to_string();
let name = self.supertype_name(iface);
self.push_ref(class_row, &name, implements_kind, iface);
}
}
@@ -2027,6 +2027,20 @@ impl<'t> Walker<'t> {
}
}
/// supertypeName — a parameterized supertype (`generic_type`) by the name
/// before its type arguments: `Store` for `implements Store<string>`.
fn supertype_name(&self, node: Node<'t>) -> String {
if node.kind() == "generic_type" {
let named = (0..node.named_child_count())
.filter_map(|j| node.named_child(j))
.find(|c| c.kind() != "type_arguments");
if let Some(named) = named {
return self.text(named).to_string();
}
}
self.text(node).to_string()
}
// --- type annotations (#381 — TS family only) ----------------------------------------------
pub(super) fn extract_type_annotations(&mut self, node: Node<'t>, from_row: u32) {
+20 -2
View File
@@ -214,6 +214,24 @@ function extractNameRaw(node: SyntaxNode, source: string, extractor: LanguageExt
return '<anonymous>';
}
/**
* The type a Java or TypeScript supertype clause names, without its type
* arguments: `Store` for `implements Store<String>`, `a.Base` for `extends
* a.Base<Integer>`, `ns.Other` for TypeScript's `implements ns.Other<number>`.
* The grammars wrap a parameterized supertype in a `generic_type` whose first
* child is the name; the index holds the interface or class under that name,
* so the full text (`Store<String>`) matched nothing and a class implementing
* a generic interface had no `implements` edge, nor the interface-impl links
* that follow from it. Any other node is its own text, as before.
*/
function supertypeName(node: SyntaxNode, source: string): string {
if (node.type === 'generic_type') {
const named = node.namedChildren.find((c: SyntaxNode) => c.type !== 'type_arguments');
if (named) return getNodeText(named, source);
}
return getNodeText(node, source);
}
/**
* Resolve a Scala type node to its base type NAME for name-matching — unwrapping
* `generic_type` (`Monoid[Int]` → `Monoid`), taking the last segment of a
@@ -7802,7 +7820,7 @@ export class TreeSitterExtractor {
const targets = typeList ? typeList.namedChildren : [child.namedChild(0)];
for (const target of targets) {
if (target) {
const name = getNodeText(target, this.source);
const name = supertypeName(target, this.source);
this.unresolvedReferences.push({
fromNodeId: classId,
referenceName: name,
@@ -7869,7 +7887,7 @@ export class TreeSitterExtractor {
const targets = typeList ? typeList.namedChildren : child.namedChildren;
for (const iface of targets) {
if (iface) {
const name = getNodeText(iface, this.source);
const name = supertypeName(iface, this.source);
this.unresolvedReferences.push({
fromNodeId: classId,
referenceName: name,