From 2ceb8b9b2facf78c3ccd5459148136e6615ed072 Mon Sep 17 00:00:00 2001 From: Eric Minish Date: Wed, 23 Sep 2026 09:04:25 -0400 Subject: [PATCH 1/2] fix(dart): index extension type members in both extraction arms (#1784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dart spells an ordinary implemented method `method_signature` — the same node type TypeScript uses for a bodiless interface member. #1780 gated that node type behind isInsideClassLikeNode() to stop a TS interface member minting a phantom free function. Correct for TS, but a Dart 3 `extension type` body was not class-like, so every member in one failed the gate and was dropped. The declaration itself was missing too, not only its members: on main, extension type Meters(double value) { double get km => value / 1000; void show() { print(km); } } extracts `function:show` alone — no `Meters`, no `km`. So `extension type` was never a class-like node in the first place; #1780 only made the omission visible by making the members depend on it. Adding `extension_type_declaration` to Dart's extraClassNodeTypes fixes both halves at once, and gives the members the right owner: `method:Meters::km` rather than a loose top-level `km`. MIRRORED INTO THE KERNEL. Dart is default-routed to the native kernel, so a change made only in the TypeScript arm would not run in a published bundle — and here the two arms had already diverged: the kernel still emitted `function:km` while wasm emitted nothing, which is why kernel-dart-parity is RED on main today. dart.rs listed the class-like node types in two places (enclosing_type_name and the extract dispatch); both now include it. That parity suite is the regression pin this already had — it just never runs without a staged .node, since every kernel-*-parity suite describe.skipIf's itself. From source `npm test` reports 188 skipped; after build-kernel.sh, 10. Verified (Linux, Node 22.23.2, kernel built and staged): kernel-dart-parity 18/18 (4 of these fail on main) dart-extension-type 2/2 (new; both fail without the change) tsc --noEmit clean npm test 252 files, 4373 passed, 3 failed The 3 remaining failures — object-literal-methods and two in ui-steps-api — reproduce on a clean upstream/main worktree and are unrelated. Thanks to the reporter for the diagnosis; this is their suggested option 2, now measured, plus the kernel half. --- CHANGELOG.md | 1 + __tests__/dart-extension-type.test.ts | 54 +++++++++++++++++++++++++++ codegraph-kernel/src/dart.rs | 11 +++++- src/extraction/languages/dart.ts | 10 ++++- 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 __tests__/dart-extension-type.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 75535a0bbd..80d7fa2eaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -283,6 +283,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - PHP static calls through imported class aliases now reach the correct class when services and repositories share method names, so callers and impact analysis show the right dependencies after re-indexing. (#1545) - TypeScript/JavaScript: a call through a field of the enclosing class — `this.mailer.send()` — now resolves on the field's declared type, so a delegating wrapper that shares the method's name no longer records itself as its own callee and `callers`, `impact` and trace stop lying on that shape. A field whose type is external or a builtin stays unresolved rather than guessed. Re-index after upgrading. (#1496) - TypeScript and JavaScript collection calls through local variables and their nested properties no longer link to unrelated project methods; re-index after upgrading. (#1566) +- **Dart `extension type` members are indexed again.** A Dart 3 `extension type` and everything declared inside it had stopped appearing in the graph, so its methods showed no callers and calls into them went nowhere. They are back, and now belong to the extension type itself rather than looking like loose top-level functions. Re-index to pick them up. (#1784) - Objective-C headers now index in a project that has no `.m` file. A `.h` file is read as C from its name alone, and only later — once its contents are read — recognized as Objective-C; the grammar for that was never loaded up front, so the file failed with a parser error and nothing in it reached the index. Adding any `.m` file used to make the same header work, which is what made this look arbitrary. Thanks @Juddd. (#1628) diff --git a/__tests__/dart-extension-type.test.ts b/__tests__/dart-extension-type.test.ts new file mode 100644 index 0000000000..1d915b074e --- /dev/null +++ b/__tests__/dart-extension-type.test.ts @@ -0,0 +1,54 @@ +/** + * Dart 3 `extension type` members (#1784). + * + * Dart spells an ordinary implemented method `method_signature`, the same node + * type TypeScript uses for a bodiless interface member. #1780 gated that node + * type behind `isInsideClassLikeNode()` to stop a TS interface member minting a + * phantom free function — correct for TS, but an `extension type` body was not + * class-like, so its members stopped being indexed at all. + */ +import { describe, it, expect, beforeAll } from 'vitest'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadAllGrammars } from '../src/extraction/grammars'; + +beforeAll(async () => { + await initGrammars(); + await loadAllGrammars(); +}); + +describe('Dart extension type members (#1784)', () => { + it('indexes an extension type member, as a method of the extension type', () => { + const code = `extension type Meters(double value) { + double get km => value / 1000; + void show() { + print(km); + } +} +`; + const result = extractFromSource('meters.dart', code); + const kinds = result.nodes.filter((n) => n.kind !== 'file').map((n) => `${n.kind}:${n.qualifiedName}`).sort(); + expect(kinds).toContain('class:Meters'); + expect(kinds).toContain('method:Meters::km'); + expect(kinds).toContain('method:Meters::show'); + }); + + it('leaves extension, mixin and class bodies alone', () => { + const code = `extension StringHelpers on String { + String shout() => toUpperCase(); +} + +mixin Logger { + void log(String m) {} +} + +class Widget { + void build() {} +} +`; + const result = extractFromSource('rest.dart', code); + const kinds = result.nodes.filter((n) => n.kind !== 'file').map((n) => `${n.kind}:${n.qualifiedName}`).sort(); + expect(kinds).toContain('method:StringHelpers::shout'); + expect(kinds).toContain('method:Logger::log'); + expect(kinds).toContain('method:Widget::build'); + }); +}); diff --git a/codegraph-kernel/src/dart.rs b/codegraph-kernel/src/dart.rs index 748410b10c..774244a776 100644 --- a/codegraph-kernel/src/dart.rs +++ b/codegraph-kernel/src/dart.rs @@ -398,7 +398,8 @@ impl<'t> Walker<'t> { while let Some(parent) = p { if matches!( parent.kind(), - "class_definition" | "mixin_declaration" | "extension_declaration" | "enum_declaration" + "class_definition" | "mixin_declaration" | "extension_declaration" + | "extension_type_declaration" | "enum_declaration" ) { return parent.child_by_field_name("name").map(|n| self.text(n)); } @@ -630,7 +631,13 @@ impl<'t> Walker<'t> { self.extract_function(node); return; } - "class_definition" | "mixin_declaration" | "extension_declaration" => { + // `extension_type_declaration` is Dart 3's `extension type Meters(double v)`. + // Mirrors dart.ts's extraClassNodeTypes: its body holds ordinary members, + // and a `method_signature` is only extracted when its enclosing node is + // class-like, so both arms must agree on this list or they disagree on + // every member of an extension type (#1784). + "class_definition" | "mixin_declaration" | "extension_declaration" + | "extension_type_declaration" => { self.extract_class(node); return; } diff --git a/src/extraction/languages/dart.ts b/src/extraction/languages/dart.ts index 8fc7bdc9ff..a12ab01d0a 100644 --- a/src/extraction/languages/dart.ts +++ b/src/extraction/languages/dart.ts @@ -132,7 +132,15 @@ export const dartExtractor: LanguageExtractor = { importTypes: ['import_or_export'], callTypes: [], // Dart calls use identifier+selector, handled via extractBareCall variableTypes: [], - extraClassNodeTypes: ['mixin_declaration', 'extension_declaration'], + // `extension_type_declaration` is Dart 3's `extension type Meters(double v)`. + // It belongs here for the same reason `mixin`/`extension` do — its body holds + // ordinary members — but it also has to be here for them to be indexed AT ALL: + // Dart spells an implemented method `method_signature`, the node type #1780 + // gated behind `isInsideClassLikeNode()` to stop a bodiless TS interface + // member minting a phantom free function. Without the extension type itself + // being a class-like node, its body is not class-like, so every + // `method_signature` in it failed that gate and was dropped (#1784). + extraClassNodeTypes: ['mixin_declaration', 'extension_declaration', 'extension_type_declaration'], // A Dart `static_final_declaration` is exactly a top-level or class-`static` // `const`/`final` — the shared-constant idiom — so extract it as `constant` // for value-reference edges. Instance fields, `var`, and typed declarations From de9d7888916b031ca119cc6a064ca9098180a396 Mon Sep 17 00:00:00 2001 From: Eric Minish Date: Wed, 23 Sep 2026 09:15:01 -0400 Subject: [PATCH 2/2] fix(dart): name extension type constructors in the wasm arm too (#1784) --- __tests__/dart-extension-type.test.ts | 12 ++++++++++++ src/extraction/languages/dart.ts | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/__tests__/dart-extension-type.test.ts b/__tests__/dart-extension-type.test.ts index 1d915b074e..e4e237be9b 100644 --- a/__tests__/dart-extension-type.test.ts +++ b/__tests__/dart-extension-type.test.ts @@ -32,6 +32,18 @@ describe('Dart extension type members (#1784)', () => { expect(kinds).toContain('method:Meters::show'); }); + it('names an extension type constructor after the constructor, not the type', () => { + const code = `extension type Meters(double value) { + Meters.fromKm(double km) : this(km * 1000); + factory Meters.zero() => Meters(0); +} +`; + const result = extractFromSource('meters.dart', code); + const ctors = result.nodes.filter((n) => n.kind === 'method'); + expect(ctors.map((n) => n.qualifiedName).sort()).toEqual(['Meters::fromKm', 'Meters::zero']); + for (const c of ctors) expect(c.returnType).toBe('Meters'); + }); + it('leaves extension, mixin and class bodies alone', () => { const code = `extension StringHelpers on String { String shout() => toUpperCase(); diff --git a/src/extraction/languages/dart.ts b/src/extraction/languages/dart.ts index a12ab01d0a..45b07cfe01 100644 --- a/src/extraction/languages/dart.ts +++ b/src/extraction/languages/dart.ts @@ -40,7 +40,8 @@ function dartEnclosingTypeName(node: SyntaxNode): string | undefined { while (p) { if ( p.type === 'class_definition' || p.type === 'mixin_declaration' || - p.type === 'extension_declaration' || p.type === 'enum_declaration' + p.type === 'extension_declaration' || p.type === 'extension_type_declaration' || + p.type === 'enum_declaration' ) { return p.childForFieldName('name')?.text; }