Ports the #645/#608 chained-receiver mechanism to Dart, plus makes Dart factory and named constructors first-class so their chains can resolve at all. A call whose receiver is itself a call — `Foo.create().bar()` (static factory or factory/named constructor) — used to drop the receiver to a bare `bar`, which name-matched a same-named method on an unrelated type (commonly a stdlib `Option`/`Iterator` `.map`/`.where` mis-tied to the project's own class). - dart.ts: extractBareCall now re-encodes `Foo.create().bar` when the chain starts with a capitalized type; getReturnType captures the return type (generic `List<Foo>` → `List`); factory (`factory Foo.create()`) and named (`Foo._()`) constructors are indexed as `Foo::create` / `Foo::_` with return type = the class (via resolveName + getReturnType + constructor_signature in methodTypes). - The UNNAMED ctor `Foo()` is deliberately NOT extracted (isMisparsedFunction), so plain construction stays an `instantiates` edge to the class rather than a call to a phantom `Foo::Foo` method. - dartCtorInfo validates a "constructor" against the enclosing class name, so a method tree-sitter MISPARSES as a constructor — `@override (A, B) m()`, where the annotation swallows the record return type and `m()` looks like a one-id constructor_signature — is still extracted as the method it is (regression found on localsend; covered by a new test). - name-matcher.ts / index.ts: `dart` joins the dotted-chain gate, CONSTRUCTS_VIA_BARE_CALL (case construction), and CHAIN_LANGUAGES (conformance for superclass/mixin methods). resolveMethodOnType validates, so a wrong inference yields no edge. Validation: 7 synthetic tests (static factory, factory/named ctor, construction, conformance, absent-method safety, the misparse regression, instantiation-not- hijacked). Real-repo A/B on localsend (368 Dart files): hand-written +17/-10 — all corrections (the -10 = 7 wrong stdlib/extension misattributions removed + 3 ctor source-renames), plus additive factory/named-ctor call resolution. Instantiation preserved; no node explosion. EXTRACTION_VERSION 13->14. Full suite green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2f96f58cbb
commit
16c73e2b0e
@@ -21,4 +21,4 @@
|
||||
* turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty
|
||||
* in the product is load-bearing").
|
||||
*/
|
||||
export const EXTRACTION_VERSION = 13;
|
||||
export const EXTRACTION_VERSION = 14;
|
||||
|
||||
@@ -2,10 +2,128 @@ import type { Node as SyntaxNode } from 'web-tree-sitter';
|
||||
import { getNodeText } from '../tree-sitter-helpers';
|
||||
import type { LanguageExtractor } from '../tree-sitter-types';
|
||||
|
||||
/**
|
||||
* The `function_signature` carrying a method's return type — unwrapped from a
|
||||
* `method_signature` wrapper (Dart nests the signature one level for methods).
|
||||
*/
|
||||
function dartInnerSignature(node: SyntaxNode): SyntaxNode {
|
||||
if (node.type === 'method_signature') {
|
||||
const inner = node.namedChildren.find((c: SyntaxNode) =>
|
||||
c.type === 'function_signature' || c.type === 'getter_signature' || c.type === 'setter_signature'
|
||||
);
|
||||
if (inner) return inner;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* The factory/named-constructor signature inside a node, if any. A constructor
|
||||
* parses as `method_signature > {factory_,}constructor_signature` (e.g.
|
||||
* `factory Foo.create()` or `Foo._()`), whose children are the class identifier
|
||||
* and — for a named ctor — the constructor-name identifier.
|
||||
*/
|
||||
function dartConstructorSignature(node: SyntaxNode): SyntaxNode | undefined {
|
||||
if (node.type === 'factory_constructor_signature' || node.type === 'constructor_signature') {
|
||||
return node;
|
||||
}
|
||||
if (node.type === 'method_signature') {
|
||||
return node.namedChildren.find((c: SyntaxNode) =>
|
||||
c.type === 'factory_constructor_signature' || c.type === 'constructor_signature'
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** The name of the class/mixin/extension/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'
|
||||
) {
|
||||
return p.childForFieldName('name')?.text;
|
||||
}
|
||||
p = p.parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validated constructor info for `node`, or undefined if it isn't genuinely a
|
||||
* constructor. A constructor signature is structurally `<Class>` or
|
||||
* `<Class>.<name>`, but tree-sitter-dart MISPARSES `@override (T) m()` — the
|
||||
* annotation swallows the record return type `(T)`, leaving `m()` looking like a
|
||||
* single-identifier constructor_signature. We disambiguate by the class name:
|
||||
* a real ctor's class identifier matches the enclosing type; a misparsed method
|
||||
* (`reduce` inside class `Action`) doesn't, and is treated as the method it is.
|
||||
*/
|
||||
function dartCtorInfo(node: SyntaxNode): { className: string; ctorName: string } | undefined {
|
||||
const ctor = dartConstructorSignature(node);
|
||||
if (!ctor) return undefined;
|
||||
const ids = ctor.namedChildren.filter((c: SyntaxNode) => c.type === 'identifier');
|
||||
const className = dartEnclosingTypeName(node);
|
||||
if (!className || !ids[0]) return undefined;
|
||||
if (ids[0].text !== className) return undefined; // misparsed method, not a ctor
|
||||
// `<Class>.<name>` is a named ctor; bare `<Class>` is the unnamed ctor.
|
||||
return { className, ctorName: ids[1]?.text ?? className };
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture a Dart method/function's declared return type as a bare type name, for
|
||||
* the chained static-factory / fluent call mechanism (#750). `Bar makeBar()`
|
||||
* yields `Bar`; a generic `List<Foo>` yields its container `List` (the method is
|
||||
* on the container, not the element); a prefixed `prefix.Bar` yields `Bar`. A
|
||||
* factory / named constructor returns its enclosing class implicitly, so its
|
||||
* "return type" is the class.
|
||||
*/
|
||||
function extractDartReturnType(node: SyntaxNode, source: string): string | undefined {
|
||||
const ctor = dartCtorInfo(node);
|
||||
if (ctor) return ctor.className;
|
||||
const sig = dartInnerSignature(node);
|
||||
// The return type precedes the method name; it's the first type_identifier
|
||||
// (generic args sit in a sibling `type_arguments`, so this is the container).
|
||||
const retType = sig.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier');
|
||||
if (!retType) return undefined;
|
||||
const text = getNodeText(retType, source).replace(/<[^>]*>/g, '').trim();
|
||||
const last = text.split('.').pop(); // prefixed `p.Bar` → `Bar`
|
||||
if (!last || !/^[A-Za-z_]\w*$/.test(last)) return undefined;
|
||||
return last;
|
||||
}
|
||||
|
||||
/**
|
||||
* The callee name of the Dart call whose `argument_part` selector is `argPart`
|
||||
* — mirrors the main extractBareCall accessor logic so a chained receiver
|
||||
* (`Foo.create()` in `Foo.create().bar()`) can be reconstructed. Returns
|
||||
* `Foo.create`, a bare `create`, or `Foo` (constructor) — or undefined.
|
||||
*/
|
||||
function dartCalleeOfArgPart(argPart: SyntaxNode): string | undefined {
|
||||
const prev = argPart.previousNamedSibling;
|
||||
if (!prev) return undefined;
|
||||
if (prev.type === 'identifier') return prev.text; // bare `Foo()` / `create()`
|
||||
if (prev.type === 'selector') {
|
||||
const accessor = prev.namedChildren.find((c: SyntaxNode) =>
|
||||
c.type === 'unconditional_assignable_selector' || c.type === 'conditional_assignable_selector'
|
||||
);
|
||||
const methodId = accessor?.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
|
||||
if (methodId) {
|
||||
const accessorPrev = prev.previousNamedSibling;
|
||||
if (accessorPrev?.type === 'identifier') return accessorPrev.text + '.' + methodId.text;
|
||||
return methodId.text;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const dartExtractor: LanguageExtractor = {
|
||||
functionTypes: ['function_signature'],
|
||||
classTypes: ['class_definition'],
|
||||
methodTypes: ['method_signature'],
|
||||
// `method_signature` covers regular methods AND factory constructors (which
|
||||
// parse as method_signature > factory_constructor_signature). A plain named
|
||||
// constructor `Foo._()` parses as a bare `constructor_signature`, so include
|
||||
// it too — resolveName names it by the ctor name and getReturnType gives it
|
||||
// the class as its return type, so `Foo._().bar()` chains resolve (#750).
|
||||
methodTypes: ['method_signature', 'constructor_signature'],
|
||||
interfaceTypes: [],
|
||||
structTypes: [],
|
||||
enumTypes: ['enum_declaration'],
|
||||
@@ -33,6 +151,19 @@ export const dartExtractor: LanguageExtractor = {
|
||||
bodyField: 'body', // class_definition uses 'body' field
|
||||
paramsField: 'formal_parameter_list',
|
||||
returnField: 'type',
|
||||
getReturnType: extractDartReturnType,
|
||||
isMisparsedFunction: (_name, node) => {
|
||||
// Skip the UNNAMED constructor `Foo()` (its ctor name equals the class). It's
|
||||
// ordinary construction — an `instantiates` edge to the class `Foo` — so
|
||||
// extracting it as a `Foo::Foo` method node would hijack instantiation
|
||||
// resolution (a `Foo(...)` call would resolve to the ctor method, not the
|
||||
// class). NAMED ctors `Foo.create()` / `Foo._()` ARE kept so their chains
|
||||
// resolve (#750). dartCtorInfo validates against the class name, so a method
|
||||
// tree-sitter misparsed as a ctor (`@override (T) m()`) is NOT skipped here.
|
||||
// (isMisparsedFunction skips node creation but still visits the body.)
|
||||
const ctor = dartCtorInfo(node);
|
||||
return ctor != null && ctor.ctorName === ctor.className;
|
||||
},
|
||||
getSignature: (node, source) => {
|
||||
// For function_signature: extract params + return type
|
||||
// For method_signature: delegate to inner function_signature
|
||||
@@ -88,6 +219,23 @@ export const dartExtractor: LanguageExtractor = {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
resolveName: (node) => {
|
||||
// Name a factory / named constructor by its constructor name — the 2nd
|
||||
// identifier (`create` in `factory Foo.create()`, `_` in `Foo._()`) — not
|
||||
// the class, so a call `Foo.create()` resolves to `Foo::create` (#750). The
|
||||
// default Dart naming returns the FIRST identifier (the class), which
|
||||
// collides every named ctor onto `Foo::Foo` and leaves `Foo.create()`
|
||||
// unresolvable. An unnamed ctor `Foo()` has a single identifier — fall
|
||||
// through (undefined) to the default class name. Letting the core's
|
||||
// extractMethod own the factory (rather than a custom visitNode) keeps the
|
||||
// body attribution intact: calls inside `factory Foo.create() { … }` are
|
||||
// attributed to `Foo::create`, and getReturnType gives it return type Foo.
|
||||
const ctor = dartCtorInfo(node);
|
||||
// A named ctor `Foo.create` → `create`; the unnamed ctor `Foo()` → undefined
|
||||
// (default naming gives the class name `Foo`, which is correct).
|
||||
if (ctor && ctor.ctorName !== ctor.className) return ctor.ctorName;
|
||||
return undefined;
|
||||
},
|
||||
extractImport: (node, source) => {
|
||||
const importText = source.substring(node.startIndex, node.endIndex).trim();
|
||||
let moduleName = '';
|
||||
@@ -160,6 +308,21 @@ export const dartExtractor: LanguageExtractor = {
|
||||
if (accessorPrev?.type === 'identifier') {
|
||||
return accessorPrev.text + '.' + methodId.text;
|
||||
}
|
||||
// Chained static-factory / fluent call: the receiver is itself a call
|
||||
// (`Foo.create().bar()`), so accessorPrev is that call's argument_part
|
||||
// selector. Encode `<innerCallee>().<method>` so resolution can infer
|
||||
// bar's class from what `Foo.create` RETURNS (#645/#608 mechanism) —
|
||||
// but only when the chain starts with a capitalized type (a companion
|
||||
// factory / static method / constructor); an instance chain
|
||||
// (`obj.foo().bar()`) keeps the bare name (its receiver's type can't
|
||||
// be recovered here).
|
||||
if (accessorPrev?.type === 'selector' &&
|
||||
accessorPrev.namedChildren.some((c: SyntaxNode) => c.type === 'argument_part')) {
|
||||
const innerCallee = dartCalleeOfArgPart(accessorPrev);
|
||||
if (innerCallee && /^[A-Z]/.test(innerCallee)) {
|
||||
return `${innerCallee}().${methodId.text}`;
|
||||
}
|
||||
}
|
||||
return methodId.text;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user