fix(dart): index a Dart 3 extension type as the type it is (#1784) (#1865)

`extension_type_declaration` appears in none of the three places Dart's other
type-like declarations do. `extension_declaration` — the older `extension` — is
in all three, and the two names are near neighbours.

That is why #1780's `isInsideClassLikeNode()` gate dropped these members and no
others: the gate asks whether a class-like node is on the stack, and an
`extension type` never put one there, while `extension`, `mixin` and `class`
all did. The gate surfaced the omission rather than causing it. Before it, the
members were still reached — but as top-level `function:km` rather than
`method:MetersT::km`, indexed and attributed to nothing.

The kernel omits the same node type, so it walks the members through its own
fallback and mints them as top-level functions with no `MetersT` to belong to.
`kernel-dart-parity.test.ts` fails four cases on `main` because of it; fixing
only the wasm side would leave those red.

Listing it alongside the others is the whole fix — `extraClassNodeTypes` and
`dartEnclosingTypeName` on the wasm side, the two `matches!` arms on the
kernel's. Both paths now give:

    class:MetersT · method:MetersT::km · method:MetersT::report

with `report`'s span running to its closing brace rather than stopping at the
signature line, and an ordinary class untouched.
This commit is contained in:
yucong-dong
2026-09-27 12:23:04 +00:00
committed by GitHub
parent cba44c9cad
commit 6f4ec88350
3 changed files with 74 additions and 5 deletions
+60
View File
@@ -0,0 +1,60 @@
/**
* A Dart 3 `extension type` is a type, and its members are its methods (#1784).
*
* `extension_type_declaration` was listed in neither extraction path's class
* types. `extension_declaration` — the older `extension` — was, and the two
* names are near neighbours, so the omission reads as an oversight.
*
* That is why #1780's `isInsideClassLikeNode()` gate dropped these members and
* no others: the gate asks whether a class-like node is on the stack, and an
* `extension type` never put one there. Before that gate the members were still
* reached, but as top-level `function:km` rather than `method:MetersT::km` —
* indexed, and attributed to nothing.
*/
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();
});
const SRC = [
'extension type MetersT(double value) {',
' double get km => value / 1000;',
' void report() {',
' print(km);',
' }',
'}',
'',
'class Widget {',
' double get half => 1.0;',
'}',
'',
].join('\n');
describe('Dart extension type (#1784)', () => {
it('is a type of its own, and its members are its methods', () => {
const result = extractFromSource('probe.dart', SRC);
expect(result.nodes.find((n) => n.name === 'MetersT')?.kind).toBe('class');
const km = result.nodes.find((n) => n.name === 'km');
expect(km, 'the getter must have a node').toBeDefined();
expect(km?.kind).toBe('method');
expect(km?.qualifiedName).toBe('MetersT::km');
});
it('does not cut the span of the member that follows a getter', () => {
const result = extractFromSource('probe.dart', SRC);
const report = result.nodes.find((n) => n.name === 'report');
// The body runs to its closing brace, not to the signature line.
expect(report?.endLine).toBeGreaterThan(report!.startLine);
});
it('leaves an ordinary class as it was', () => {
const result = extractFromSource('probe.dart', SRC);
expect(result.nodes.find((n) => n.name === 'Widget')?.kind).toBe('class');
expect(result.nodes.find((n) => n.name === 'half')?.kind).toBe('method');
});
});
+6 -2
View File
@@ -398,7 +398,7 @@ 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 +630,11 @@ impl<'t> Walker<'t> {
self.extract_function(node);
return;
}
"class_definition" | "mixin_declaration" | "extension_declaration" => {
// `extension_type_declaration` is Dart 3.3's extension type. It is a
// different node from `extension_declaration` above, which is the
// older `extension` — the names are near neighbours and only one of
// them was listed.
"class_definition" | "mixin_declaration" | "extension_declaration" | "extension_type_declaration" => {
self.extract_class(node);
return;
}
+8 -3
View File
@@ -34,13 +34,14 @@ function dartConstructorSignature(node: SyntaxNode): SyntaxNode | undefined {
return undefined;
}
/** The name of the class/mixin/extension/enum lexically enclosing `node`. */
/** The name of the class/mixin/extension/extension type/enum lexically enclosing `node`. */
function dartEnclosingTypeName(node: SyntaxNode): string | undefined {
let p = node.parent;
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;
}
@@ -132,7 +133,11 @@ 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. It sits beside the
// older `extension_declaration` — near-neighbour names — and its members live
// in an ordinary `class_body`, so it belongs on this list for the same reason
// the other two do.
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