mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 19:30:10 +08:00
fix(js,ts): .tsx and .jsx classes link their overrides as .ts and .js classes do (#2828)
The interface-impl pass links a base class's or an interface's method
to each subclass's override, so a call through the base reaches the
method that runs. It took a class's methods, and its supertype, by
language: typescript and javascript were in its language set, and tsx
and jsx were not. A class in a .tsx or .jsx file got no override
edges, and neither did a .ts class whose base or interface was in a
.tsx file. The pass's own gate, the static/instance split of #2761
and the other JS-family lists already count tsx and jsx, and the
set's history (#494, #708) gives no reason for leaving them out.
tsx and jsx are now in the set, so these classes get the edges that
.ts and .js classes get, statics paired with statics.
Validation: the new test fails 4 of 4 on main 013b91c1 and passes 4
of 4 on the fix, and dropping tsx or jsx alone from the set fails the
tests for it. A/B against main on 11 repos: outline gains 262 override
edges in 42 base-subclass pairs (26 emails overriding BaseEmail's
subject, render and preview; editor nodes, marks and extensions
overriding Node, Mark and Extension) and lexical 27 in 12, grouped by
pair and sampled against the source, none pairing a static with an
instance method. Nodes and all other edges are unchanged. excalidraw,
social-app, insomnia, two .jsx apps and the controls (three, webpack,
flask, gin) are byte-identical: their .tsx classes extend React's or
other packages' classes. explore on outline now leads with the flow
BaseEmail.send -> render -> WelcomeEmail.render, where main finds
none. A sync that adds an interface method, adds an override and
drops a subclass's base matches a fresh index. The pass takes 65-108
ms on both arms, and index time, median of 3 interleaved runs on
lexical, outline, excalidraw and social-app, is +1.7% in total, noise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
58eb57a324
commit
5b47bc4963
@@ -203,6 +203,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In Go, a type alias like `type Event = mvccpb.Event`, alone or inside a `type ( … )` group, is now indexed and links to the type it names, so impact on that type reaches the code that uses the alias. Before, aliases were missing: a use written through the alias's package, like `clientv3.Event` or prometheus's `storage.AOptions{…}`, linked to nothing, and one inside the package, like `&PrometheusVersion{}` in prometheus's `web` package, linked to whichever type of that name the project had, there a test helper's struct. A method called on a value of an alias's type now reaches the aliased type's method rather than a namesake's, and a generic alias like `type Items[T any] = List[T]` links to `List`, so `xs.Len()` on one reaches `List.Len` instead of nothing. Re-index Go projects after upgrading.
|
||||
- In Go, a struct that embeds another is no longer treated as overriding the methods it redeclares. gin's `Engine` embeds `RouterGroup` and has a `Use` of its own, but calling `Use` on a `RouterGroup` only ever runs `RouterGroup.Use`; before, `codegraph_explore`'s call path and impact went on from it into `Engine.Use` and the code only the engine runs. The same held for every struct or defined type embedded in another that redeclares one of its methods, like etcd's `Mutex` inside `lockerMutex`. A call through an interface still reaches each type that implements it, the embedding struct included. Re-index Go projects after upgrading.
|
||||
- In JavaScript and TypeScript, a subclass's `static` method no longer counts as overriding an instance method of the same name in its base class, or the reverse, since calling one never runs the other. In webpack, `codegraph_explore`'s call path and impact went on from `Module`'s `deserialize(context)` into the `static deserialize(context)` of `NormalModule` and other modules. A static method still overrides its base's static method and an instance method its base's instance method, and JavaScript class members are now marked static, as TypeScript ones already were. Re-index JavaScript and TypeScript projects after upgrading. Thanks @inth3shadows for the report and @yang0228 for the pull request. (#2761)
|
||||
- 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 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 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)
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* The override-dispatch edges (`synthesizedBy: interface-impl`) link an
|
||||
* interface's or a base class's method to the method of the same name that a
|
||||
* class implementing or extending it declares, so a call through the base
|
||||
* reaches the override. They were made for `.ts` and `.js` classes only: a
|
||||
* `.tsx` or `.jsx` class got none, though it is a TypeScript or JavaScript
|
||||
* class, and neither did a `.ts` class whose base or interface lives in a
|
||||
* `.tsx` file, like outline's emails extending `BaseEmail`.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
describe('override dispatch in .tsx and .jsx classes', () => {
|
||||
let root = '';
|
||||
let graph: CodeGraph | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-jsx-override-'));
|
||||
const write = (rel: string, lines: string[]) => fs.writeFileSync(path.join(root, rel), lines.join('\n') + '\n');
|
||||
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'app' }));
|
||||
fs.mkdirSync(path.join(root, 'src'));
|
||||
const classes = [
|
||||
'export class Widget { render() { return null; } static create() { return 1; } }',
|
||||
'export class Button extends Widget { render() { return null; } static create() { return 2; } }',
|
||||
// A static method overrides only a static one (#2761).
|
||||
'export class Toggle extends Widget { static render() { return null; } }',
|
||||
];
|
||||
write('src/widgets.tsx', classes);
|
||||
write('src/widgets.jsx', classes);
|
||||
// An interface and an abstract base in a .ts file, implemented in .tsx.
|
||||
write('src/shape.ts', [
|
||||
'export interface Shape { area(): number }',
|
||||
'export abstract class Figure { abstract draw(): string; describe(): string { return this.draw(); } }',
|
||||
]);
|
||||
write('src/Circle.tsx', [
|
||||
"import { Shape, Figure } from './shape';",
|
||||
'export class Circle extends Figure implements Shape {',
|
||||
' area(): number { return 3; }',
|
||||
' draw(): string { return "o"; }',
|
||||
' view() { return <div />; }',
|
||||
'}',
|
||||
]);
|
||||
// outline's shape: a base in a .tsx file, a subclass in a .ts one.
|
||||
write('src/BaseEmail.tsx', ['export class BaseEmail { subject() { return ""; } render() { return <p />; } }']);
|
||||
write('src/WelcomeEmail.ts', [
|
||||
"import { BaseEmail } from './BaseEmail';",
|
||||
'export class WelcomeEmail extends BaseEmail { subject() { return "Welcome"; } }',
|
||||
]);
|
||||
graph = await CodeGraph.init(root, { index: true });
|
||||
}, 120_000);
|
||||
|
||||
afterAll(() => {
|
||||
graph?.close();
|
||||
graph = null;
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** `base -> override` for every interface-impl edge out of a file's nodes, by qualified name (and file, when another). */
|
||||
function overrides(file: string): string[] {
|
||||
const g = graph!;
|
||||
const out: string[] = [];
|
||||
const label = (id: string) => {
|
||||
const n = g.getNode(id)!;
|
||||
return `${n.isStatic ? 'static ' : ''}${n.qualifiedName}${n.filePath === file ? '' : ` (${path.basename(n.filePath)})`}`;
|
||||
};
|
||||
for (const node of g.getNodesInFile(file)) {
|
||||
for (const e of g.getOutgoingEdges(node.id)) {
|
||||
if (e.kind === 'calls' && e.metadata?.synthesizedBy === 'interface-impl') out.push(`${label(e.source)} -> ${label(e.target)}`);
|
||||
}
|
||||
}
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
const sameFile = ['Widget::render -> Button::render', 'static Widget::create -> static Button::create'];
|
||||
|
||||
it('in a .tsx file, statics to statics', () => {
|
||||
expect(overrides('src/widgets.tsx')).toEqual(sameFile);
|
||||
});
|
||||
|
||||
it('in a .jsx file, statics to statics', () => {
|
||||
expect(overrides('src/widgets.jsx')).toEqual(sameFile);
|
||||
});
|
||||
|
||||
it('from a .ts interface and abstract base to the .tsx class implementing them', () => {
|
||||
expect(overrides('src/shape.ts')).toEqual([
|
||||
'Figure::draw -> Circle::draw (Circle.tsx)',
|
||||
'Shape::area -> Circle::area (Circle.tsx)',
|
||||
]);
|
||||
});
|
||||
|
||||
it('from a .tsx base to the .ts class extending it', () => {
|
||||
expect(overrides('src/BaseEmail.tsx')).toEqual(['BaseEmail::subject -> WelcomeEmail::subject (WelcomeEmail.ts)']);
|
||||
});
|
||||
});
|
||||
@@ -820,16 +820,17 @@ async function cppOverrideEdges(queries: QueryBuilder, ctx: ResolutionContext, o
|
||||
// kind that holds methods" — i.e. the shape this loop expects. Swift and
|
||||
// Scala fit shape-wise (Swift `protocol`/`class`, Scala `trait`/`class`)
|
||||
// and are added below; their concrete-side nodes can be a `struct` (Swift)
|
||||
// or an `object` (Scala) so the loop also iterates those kinds.
|
||||
// or an `object` (Scala) so the loop also iterates those kinds. A `.tsx` or
|
||||
// `.jsx` class is a TypeScript or JavaScript class, and its base or interface
|
||||
// is often in a `.ts` file, or the other way round.
|
||||
const IFACE_OVERRIDE_LANGS = new Set([
|
||||
'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust',
|
||||
'java', 'kotlin', 'csharp', 'typescript', 'tsx', 'javascript', 'jsx', 'swift', 'scala', 'go', 'rust',
|
||||
'arkts',
|
||||
]);
|
||||
/**
|
||||
* Languages whose static and instance methods override only their own kind:
|
||||
* a JS/TS class's statics are inherited and overridden by the subclass's
|
||||
* statics, its instance methods by instance methods (#2761). tsx and jsx
|
||||
* follow the same rule, though this pass doesn't walk their classes today.
|
||||
* statics, its instance methods by instance methods (#2761).
|
||||
*/
|
||||
const STATIC_SPLIT_LANGS: ReadonlySet<string> = new Set(['typescript', 'tsx', 'javascript', 'jsx']);
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user