diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e069b70..88de4b51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, 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) diff --git a/__tests__/jsx-override-dispatch.test.ts b/__tests__/jsx-override-dispatch.test.ts new file mode 100644 index 00000000..ec7d3b70 --- /dev/null +++ b/__tests__/jsx-override-dispatch.test.ts @@ -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
; }', + '}', + ]); + // 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

; } }']); + 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)']); + }); +}); diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index 51a56a91..d747898a 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.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 = new Set(['typescript', 'tsx', 'javascript', 'jsx']); /**