From b4a0172829ca8662a4fcd9659440a10dcde45769 Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Tue, 20 Oct 2026 14:10:21 +0300 Subject: [PATCH] fix(java,ts): a parameterized supertype names its type, so `implements Store` links the interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Java and TypeScript grammars wrap a parameterized supertype in a `generic_type`, and the supertype clause reader took the node's whole text as the name — `Store`, which the index holds nothing under. A class implementing a generic interface had no `implements` edge, and so none of the interface-impl links the synthesizer draws from it; the same for `extends Base`, an interface's `extends Store`, and TypeScript's `implements Store` (its `extends Base` already worked: the grammar keeps that type's arguments beside the name). The reference now names the type before its arguments — the `generic_type`'s first child, a `type_identifier` or a scoped one — on the wasm path and in the Java and TS/JS kernels alike; any other supertype node is its own text, as before. C++ (`stripCppTemplateArgs`), Scala (`scalaBaseTypeName`) and Kotlin already did this for their grammars. A package- or namespace-qualified supertype (`extends app.Base`, `implements ns.Other`) stays unresolved with or without arguments: that is a resolver question, not this one. Co-Authored-By: Claude Fable 5.1 --- 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 8611ba779..3864baeaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- In Java and TypeScript, a class that implements or extends a parameterized type — `implements Store`, `extends Base`, an interface's `extends Store`, `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. - In TypeScript, an awaited call with type arguments like `await fetchApi("/me")` or `await api.get(url)` now links to the function it calls. Before, the call was dropped, so typed fetch wrappers looked like they had no callers. (#2586) - In React, a component rendered at module scope, in a top-level constant or inside a test's `it(…)` callback, like `createRoot(el).render()` or `render()`, now counts as rendered there, so an app's entry reaches its root component and a component's tests show among its callers. (#2587) - In Python, `import a.b as X` written inside a function now links its calls like `X.f()`, as the same import at module level already did. Thanks @Egrn for the report. (#2600) 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 abaaa24a1..f9213336c 100644 --- a/codegraph-kernel/src/tsjs/extractors.rs +++ b/codegraph-kernel/src/tsjs/extractors.rs @@ -1851,14 +1851,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); } } @@ -1894,6 +1894,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 e6d627ac3..803b98f70 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -212,6 +212,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 @@ -7484,7 +7502,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, @@ -7551,7 +7569,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,