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 @@ -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)
Expand Down
103 changes: 103 additions & 0 deletions __tests__/generic-supertypes.test.ts
Original file line number Diff line number Diff line change
@@ -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']);
});
});
18 changes: 16 additions & 2 deletions codegraph-kernel/src/java.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand All @@ -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);
}
}
Expand All @@ -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() {
Expand Down
18 changes: 16 additions & 2 deletions codegraph-kernel/src/tsjs/extractors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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) {
Expand Down
22 changes: 20 additions & 2 deletions src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down