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 @@ -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)
Expand Down
97 changes: 97 additions & 0 deletions __tests__/jsx-override-dispatch.test.ts
Original file line number Diff line number Diff line change
@@ -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)']);
});
});
9 changes: 5 additions & 4 deletions src/resolution/callback-synthesizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
/**
Expand Down