From 8f8f04b7fc14e1d2a01db5d0a51a6c8c9a1ef6c7 Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Fri, 23 Oct 2026 12:50:18 -0500 Subject: [PATCH] fix(java,ts): a parameterized supertype links its type, like implements Store A Java class implementing a parameterized interface (implements Store), one extending a generic base (extends Base), a Java interface's extends Store, and a TypeScript class's implements Store 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 58 times, TypeHandler, ResultHandler, BaseMapper...) 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, RenderPipe, InstructionPipe...) and 11 interface-impl calls; excalidraw 2 implements DeltaContainer and 4 interface-impl calls; vscode's base and editor 142 implements (IListRenderer, IListVirtualDelegate, ISpliceable...) 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 --- CHANGELOG.md | 1 + __tests__/generic-supertypes.test.ts | 103 ++++++++++++++++++++++++ codegraph-kernel/src/java.rs | 18 ++++- codegraph-kernel/src/tsjs/extractors.rs | 18 ++++- src/extraction/tree-sitter.ts | 22 ++++- 5 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 __tests__/generic-supertypes.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e5fcdcef..8a1d3dfd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, 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`, `extends Base`, an interface's `extends Store` or TypeScript's `implements Store`, 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) diff --git a/__tests__/generic-supertypes.test.ts b/__tests__/generic-supertypes.test.ts new file mode 100644 index 000000000..3b68aa81f --- /dev/null +++ b/__tests__/generic-supertypes.test.ts @@ -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` 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` had no `implements` edge, and none of the interface-impl + * links that follow from it; the same for `extends Base`, an + * interface's `extends Store`, and TypeScript's `implements Store`. + * 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 = { + 'src/Store.java': `package app; + +public interface Store { + void save(T entity); +} +`, + 'src/Sub.java': `package app; + +interface Sub extends Store {} +`, + 'src/MapStore.java': `package app; + +import java.io.Serializable; + +public class MapStore implements Store, Serializable { + public void save(String entity) {} +} +`, + 'src/Base.java': `package app; + +public class Base { + public void init() {} +} +`, + 'src/Derived.java': `package app; + +public class Derived extends Base { + public void init() {} +} +`, + 'src/store.ts': `export interface Store { save(entity: T): void } +export interface Named { name: string } +export class MapStore implements Store, 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, extends Base, interface extends Store', () => { + 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, beside a plain interface', () => { + expect(supertypes('src/store.ts', 'MapStore')).toEqual(['implements -> Named', 'implements -> Store']); + }); +}); diff --git a/codegraph-kernel/src/java.rs b/codegraph-kernel/src/java.rs index 42c30f76e..35cea85a6 100644 --- a/codegraph-kernel/src/java.rs +++ b/codegraph-kernel/src/java.rs @@ -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`. + 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() { diff --git a/codegraph-kernel/src/tsjs/extractors.rs b/codegraph-kernel/src/tsjs/extractors.rs index f97ae6d6c..f10c5a507 100644 --- a/codegraph-kernel/src/tsjs/extractors.rs +++ b/codegraph-kernel/src/tsjs/extractors.rs @@ -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`. + 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) { diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 68b1a8081..fb47f50a5 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -214,6 +214,24 @@ function extractNameRaw(node: SyntaxNode, source: string, extractor: LanguageExt return ''; } +/** + * The type a Java or TypeScript supertype clause names, without its type + * arguments: `Store` for `implements Store`, `a.Base` for `extends + * a.Base`, `ns.Other` for TypeScript's `implements ns.Other`. + * 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`) 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,