Explorar o código

fix(dart): resolve chained static-factory / constructor calls Foo.create().bar() (#750) (#762)

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>
Colby Mchenry hai 1 semana
pai
achega
16c73e2b0e

+ 1 - 0
CHANGELOG.md

@@ -32,6 +32,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Go method calls made through a chained factory function now resolve to the correct type. A call like `New().Method()` used to drop the receiver, so the chained method attached to a same-named method on an unrelated type — or didn't resolve. CodeGraph now captures Go return types (a pointer `*Foo` resolves to `Foo`, and a multi-return `(*Foo, error)` to its first result), infers the chained receiver's type from what the factory function returns, and resolves the method on it — including methods promoted from an embedded struct — creating the edge only when the type or an embedded type genuinely has the method. Existing Go indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Go)
 - Go method calls made through a chained factory function now resolve to the correct type. A call like `New().Method()` used to drop the receiver, so the chained method attached to a same-named method on an unrelated type — or didn't resolve. CodeGraph now captures Go return types (a pointer `*Foo` resolves to `Foo`, and a multi-return `(*Foo, error)` to its first result), infers the chained receiver's type from what the factory function returns, and resolves the method on it — including methods promoted from an embedded struct — creating the edge only when the type or an embedded type genuinely has the method. Existing Go indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Go)
 - Scala method calls made through a companion-object factory, a fluent chain, or a case-class `apply` now resolve to the correct type. A call like `Foo.create().bar()` or `Builder(cfg).bar()` used to drop the receiver, so the chained method silently attached to a same-named method on an unrelated type — most often mis-attributing a standard-library `Option` / `Iterator` `.map` / `.flatMap` / `.foreach` onto your own same-named class. CodeGraph now captures Scala return types (a generic `List[Foo]` resolves to its container `List`, a qualified `pkg.Foo` to `Foo`), infers the chained receiver's type from what the inner call returns or constructs, and resolves the method on it — including methods inherited from a trait the type extends — creating the edge only when that type or one of its traits genuinely has the method (so a wrong inference produces no edge instead of a misleading one). Existing Scala indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Scala)
 - Scala method calls made through a companion-object factory, a fluent chain, or a case-class `apply` now resolve to the correct type. A call like `Foo.create().bar()` or `Builder(cfg).bar()` used to drop the receiver, so the chained method silently attached to a same-named method on an unrelated type — most often mis-attributing a standard-library `Option` / `Iterator` `.map` / `.flatMap` / `.foreach` onto your own same-named class. CodeGraph now captures Scala return types (a generic `List[Foo]` resolves to its container `List`, a qualified `pkg.Foo` to `Foo`), infers the chained receiver's type from what the inner call returns or constructs, and resolves the method on it — including methods inherited from a trait the type extends — creating the edge only when that type or one of its traits genuinely has the method (so a wrong inference produces no edge instead of a misleading one). Existing Scala indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Scala)
 - Rust method calls made through a chained associated function now resolve to the correct type. A call like `Foo::new().bar()` or `Foo::with(cfg).build()` used to drop the receiver, so the chained method silently attached to a same-named method on an unrelated type — or didn't resolve. CodeGraph now captures Rust return types (`-> Self` resolves to the implementing type), infers the chained receiver's type from what the associated function returns, and resolves the method on it — including methods provided by a trait the type implements (via the new `impl Trait for Type` relationships) — creating the edge only when the type or one of its traits genuinely has the method. Existing Rust indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Rust)
 - Rust method calls made through a chained associated function now resolve to the correct type. A call like `Foo::new().bar()` or `Foo::with(cfg).build()` used to drop the receiver, so the chained method silently attached to a same-named method on an unrelated type — or didn't resolve. CodeGraph now captures Rust return types (`-> Self` resolves to the implementing type), infers the chained receiver's type from what the associated function returns, and resolves the method on it — including methods provided by a trait the type implements (via the new `impl Trait for Type` relationships) — creating the edge only when the type or one of its traits genuinely has the method. Existing Rust indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Rust)
+- Dart method calls made through a static factory, a factory or named constructor, or a fluent chain now resolve to the correct type. A call like `Foo.create().bar()` used to drop the receiver, so the chained method silently attached to a same-named method on an unrelated type — most often mis-attributing a standard-library `Option` / `Iterator` `.map` / `.where` onto your own same-named class. CodeGraph now indexes Dart **factory and named constructors** (`factory Foo.create()`, `Foo.named()`) as first-class members so calls to them resolve, captures Dart return types (a generic `List<Foo>` resolves to its container `List`), infers the chained receiver's type from what the inner call returns or constructs, and resolves the method on it — including methods inherited from a superclass or mixin — creating the edge only when that type genuinely has the method. Plain construction (`Foo(...)`) is still recorded as instantiation. Existing Dart indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Dart)
 - Chained method calls now resolve when the chained method is **inherited from a superclass or declared on an interface/protocol** the receiver's type conforms to — for example a call on a sealed-subclass instance (`Either.Right(x).combine(...)`) that invokes a method defined on its parent type. Previously these chains found no caller edge even though the factory's type was known, so the call was invisible to callers, impact, and trace. CodeGraph now walks the type's supertypes (its `extends` / `implements` relationships) to find the method, creating the edge only when a supertype genuinely declares it (so a wrong inference still produces no edge). This makes Java, Kotlin, and C# factory and fluent chains more complete. Existing indexes should be re-indexed (`codegraph index -f`) to benefit. (#750)
 - Chained method calls now resolve when the chained method is **inherited from a superclass or declared on an interface/protocol** the receiver's type conforms to — for example a call on a sealed-subclass instance (`Either.Right(x).combine(...)`) that invokes a method defined on its parent type. Previously these chains found no caller edge even though the factory's type was known, so the call was invisible to callers, impact, and trace. CodeGraph now walks the type's supertypes (its `extends` / `implements` relationships) to find the method, creating the edge only when a supertype genuinely declares it (so a wrong inference still produces no edge). This makes Java, Kotlin, and C# factory and fluent chains more complete. Existing indexes should be re-indexed (`codegraph index -f`) to benefit. (#750)
 - Swift method calls made through a static factory, fluent chain, or constructor now resolve to the correct class. A call like `Foo.make().draw()` or `Foo().draw()` used to drop the receiver, so the chained method silently attached to a same-named method on an unrelated class — or didn't resolve at all. CodeGraph now captures Swift return types and infers the chained receiver's type from what the inner call returns (or the constructed type), creating the edge only when that class genuinely has the method (so a wrong inference produces no edge instead of a misleading one). Existing Swift indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Swift)
 - Swift method calls made through a static factory, fluent chain, or constructor now resolve to the correct class. A call like `Foo.make().draw()` or `Foo().draw()` used to drop the receiver, so the chained method silently attached to a same-named method on an unrelated class — or didn't resolve at all. CodeGraph now captures Swift return types and infers the chained receiver's type from what the inner call returns (or the constructed type), creating the edge only when that class genuinely has the method (so a wrong inference produces no edge instead of a misleading one). Existing Swift indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Swift)
 - C# method calls made through a static factory or fluent chain now resolve to the correct class. A call like `Foo.Create().Bar()` or `JObject.Parse(s).Property(...)` used to lose the receiver's type, so the chained method didn't resolve and the call was invisible to callers/impact/trace. CodeGraph now captures C# return types and infers the chained receiver's type from what the inner call returns, creating the edge only when that class genuinely has the method (so a wrong inference produces no edge). Existing C# indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (C#)
 - C# method calls made through a static factory or fluent chain now resolve to the correct class. A call like `Foo.Create().Bar()` or `JObject.Parse(s).Property(...)` used to lose the receiver's type, so the chained method didn't resolve and the call was invisible to callers/impact/trace. CodeGraph now captures C# return types and infers the chained receiver's type from what the inner call returns, creating the edge only when that class genuinely has the method (so a wrong inference produces no edge). Existing C# indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (C#)

+ 168 - 0
__tests__/resolution.test.ts

@@ -2826,4 +2826,172 @@ object Main {
       expect(callerNamesOf('Other::onlyOther')).toEqual([]);
       expect(callerNamesOf('Other::onlyOther')).toEqual([]);
     });
     });
   });
   });
+
+  describe('Dart chained static-factory / factory-constructor call resolution (#645/#608 mechanism)', () => {
+    function callerNamesOf(qualifiedName: string): string[] {
+      const target = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
+      if (!target) return [];
+      const names = cg
+        .getIncomingEdges(target.id)
+        .filter((e) => e.kind === 'calls')
+        .map((e) => cg.getNode(e.source)?.name)
+        .filter((n): n is string => !!n);
+      return [...new Set(names)].sort();
+    }
+
+    it('resolves a static-factory chain Foo.makeBar().doIt() to the return type, never a same-named decoy', async () => {
+      fs.writeFileSync(
+        path.join(tempDir, 'main.dart'),
+        `class Foo {
+  static Bar makeBar() => Bar();
+}
+class Bar {
+  void doIt() {}
+}
+class Decoy {
+  void doIt() {}
+}
+void run() {
+  Foo.makeBar().doIt();
+}
+`
+      );
+      cg = await CodeGraph.init(tempDir, { index: true });
+      expect(callerNamesOf('Bar::doIt')).toEqual(['run']);
+      expect(callerNamesOf('Decoy::doIt')).toEqual([]);
+    });
+
+    it('resolves a named factory-constructor chain Foo.create().ship() on the constructed class', async () => {
+      fs.writeFileSync(
+        path.join(tempDir, 'main.dart'),
+        `class Foo {
+  Foo._();
+  factory Foo.create() => Foo._();
+  void ship() {}
+}
+class Decoy {
+  void ship() {}
+}
+void run() {
+  Foo.create().ship();
+}
+`
+      );
+      cg = await CodeGraph.init(tempDir, { index: true });
+      // The factory constructor `Foo.create` is now a node whose return type is Foo,
+      // so `ship` resolves on Foo, not the same-named Decoy.
+      expect(callerNamesOf('Foo::ship')).toEqual(['run']);
+      expect(callerNamesOf('Decoy::ship')).toEqual([]);
+    });
+
+    it('resolves a constructor-receiver chain Bar().doIt() on the constructed class', async () => {
+      fs.writeFileSync(
+        path.join(tempDir, 'main.dart'),
+        `class Bar {
+  void doIt() {}
+}
+class Decoy {
+  void doIt() {}
+}
+void run() {
+  Bar().doIt();
+}
+`
+      );
+      cg = await CodeGraph.init(tempDir, { index: true });
+      expect(callerNamesOf('Bar::doIt')).toEqual(['run']);
+      expect(callerNamesOf('Decoy::doIt')).toEqual([]);
+    });
+
+    it('resolves a chained method inherited from a superclass the return type extends (via conformance)', async () => {
+      fs.writeFileSync(
+        path.join(tempDir, 'main.dart'),
+        `class Base {
+  void render() {}
+}
+class Widget extends Base {
+  static Widget make() => Widget();
+}
+class Decoy {
+  void render() {}
+}
+void run() {
+  Widget.make().render();
+}
+`
+      );
+      cg = await CodeGraph.init(tempDir, { index: true });
+      expect(callerNamesOf('Base::render')).toEqual(['run']);
+      expect(callerNamesOf('Decoy::render')).toEqual([]);
+    });
+
+    it('creates NO edge when neither the factory return type nor a supertype has the method (silent miss)', async () => {
+      fs.writeFileSync(
+        path.join(tempDir, 'main.dart'),
+        `class Foo {
+  static Bar makeBar() => Bar();
+}
+class Bar {
+}
+class Other {
+  void onlyOther() {}
+}
+void run() {
+  Foo.makeBar().onlyOther();
+}
+`
+      );
+      cg = await CodeGraph.init(tempDir, { index: true });
+      // Bar has no onlyOther() — must not mis-attach to the same-named Other::onlyOther.
+      expect(callerNamesOf('Other::onlyOther')).toEqual([]);
+    });
+
+    it('still extracts a method tree-sitter misparses as a constructor (@override + record return)', async () => {
+      // tree-sitter-dart misparses `@override (A, B) reduce()` — the annotation
+      // swallows the record return type, so `reduce()` looks like a single-
+      // identifier constructor_signature. It must NOT be skipped as an unnamed
+      // ctor (its name doesn't match the class); its body call must attribute to
+      // `reduce`, not the class.
+      fs.writeFileSync(
+        path.join(tempDir, 'main.dart'),
+        `class Base {}
+class Action extends Base {
+  Action({required int x});
+  @override
+  (int, String) reduce() {
+    return (compute(), "y");
+  }
+  int compute() => 1;
+}
+`
+      );
+      cg = await CodeGraph.init(tempDir, { index: true });
+      // reduce must be a node and its body call must resolve to Action::compute.
+      expect(callerNamesOf('Action::compute')).toEqual(['reduce']);
+    });
+
+    it('keeps plain construction Foo() as instantiation, not a Foo::Foo method call', async () => {
+      // The unnamed constructor is intentionally NOT extracted as a `Foo::Foo`
+      // method, so `Foo(...)` resolves to the class (an `instantiates` edge),
+      // never hijacked into a call to a phantom constructor method.
+      fs.writeFileSync(
+        path.join(tempDir, 'main.dart'),
+        `class Widget {
+  final int x;
+  Widget(this.x);
+}
+void run() {
+  Widget(3);
+}
+`
+      );
+      cg = await CodeGraph.init(tempDir, { index: true });
+      // No Foo::Foo phantom method node.
+      expect(cg.getNodesByKind('method').some((n) => n.qualifiedName === 'Widget::Widget')).toBe(false);
+      // The construction resolves to the class as an `instantiates` edge.
+      const widget = cg.getNodesByKind('class').find((n) => n.name === 'Widget')!;
+      const incoming = cg.getIncomingEdges(widget.id);
+      expect(incoming.some((e) => e.kind === 'instantiates')).toBe(true);
+    });
+  });
 });
 });

+ 1 - 1
src/extraction/extraction-version.ts

@@ -21,4 +21,4 @@
  * turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty
  * turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty
  * in the product is load-bearing").
  * in the product is load-bearing").
  */
  */
-export const EXTRACTION_VERSION = 13;
+export const EXTRACTION_VERSION = 14;

+ 164 - 1
src/extraction/languages/dart.ts

@@ -2,10 +2,128 @@ import type { Node as SyntaxNode } from 'web-tree-sitter';
 import { getNodeText } from '../tree-sitter-helpers';
 import { getNodeText } from '../tree-sitter-helpers';
 import type { LanguageExtractor } from '../tree-sitter-types';
 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 = {
 export const dartExtractor: LanguageExtractor = {
   functionTypes: ['function_signature'],
   functionTypes: ['function_signature'],
   classTypes: ['class_definition'],
   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: [],
   interfaceTypes: [],
   structTypes: [],
   structTypes: [],
   enumTypes: ['enum_declaration'],
   enumTypes: ['enum_declaration'],
@@ -33,6 +151,19 @@ export const dartExtractor: LanguageExtractor = {
   bodyField: 'body', // class_definition uses 'body' field
   bodyField: 'body', // class_definition uses 'body' field
   paramsField: 'formal_parameter_list',
   paramsField: 'formal_parameter_list',
   returnField: 'type',
   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) => {
   getSignature: (node, source) => {
     // For function_signature: extract params + return type
     // For function_signature: extract params + return type
     // For method_signature: delegate to inner function_signature
     // For method_signature: delegate to inner function_signature
@@ -88,6 +219,23 @@ export const dartExtractor: LanguageExtractor = {
     }
     }
     return false;
     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) => {
   extractImport: (node, source) => {
     const importText = source.substring(node.startIndex, node.endIndex).trim();
     const importText = source.substring(node.startIndex, node.endIndex).trim();
     let moduleName = '';
     let moduleName = '';
@@ -160,6 +308,21 @@ export const dartExtractor: LanguageExtractor = {
             if (accessorPrev?.type === 'identifier') {
             if (accessorPrev?.type === 'identifier') {
               return accessorPrev.text + '.' + methodId.text;
               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;
             return methodId.text;
           }
           }
         }
         }

+ 1 - 1
src/resolution/index.ts

@@ -37,7 +37,7 @@ const SUPERTYPE_BEARING_KINDS = new Set<Node['kind']>([
  * second pass. Dotted-receiver languages resolve via matchDottedCallChain; the
  * second pass. Dotted-receiver languages resolve via matchDottedCallChain; the
  * `::`-receiver ones (Rust) via matchScopedCallChain.
  * `::`-receiver ones (Rust) via matchScopedCallChain.
  */
  */
-const CHAIN_LANGUAGES = new Set(['java', 'kotlin', 'csharp', 'swift', 'rust', 'go', 'scala']);
+const CHAIN_LANGUAGES = new Set(['java', 'kotlin', 'csharp', 'swift', 'rust', 'go', 'scala', 'dart']);
 const SCOPED_CHAIN_LANGUAGES = new Set(['rust']);
 const SCOPED_CHAIN_LANGUAGES = new Set(['rust']);
 
 
 /** The extractor's chained-receiver encoding: `<inner>().<method>`. */
 /** The extractor's chained-receiver encoding: `<inner>().<method>`. */

+ 8 - 6
src/resolution/name-matcher.ts

@@ -605,7 +605,7 @@ export function matchScopedCallChain(
  * `Foo` — and resolveMethodOnType validates, so a non-conventional `apply` that
  * `Foo` — and resolveMethodOnType validates, so a non-conventional `apply` that
  * returns another type simply yields no edge rather than a wrong one.
  * returns another type simply yields no edge rather than a wrong one.
  */
  */
-const CONSTRUCTS_VIA_BARE_CALL = new Set(['kotlin', 'swift', 'scala']);
+const CONSTRUCTS_VIA_BARE_CALL = new Set(['kotlin', 'swift', 'scala', 'dart']);
 
 
 /**
 /**
  * Resolve a dotted chained call whose receiver is a static factory / fluent call —
  * Resolve a dotted chained call whose receiver is a static factory / fluent call —
@@ -1123,17 +1123,19 @@ export function matchReference(
   }
   }
 
 
   // 1d. Dotted chained static-factory / fluent call (Java / Kotlin / C# / Swift /
   // 1d. Dotted chained static-factory / fluent call (Java / Kotlin / C# / Swift /
-  // Go / Scala) — `Foo.getInstance().bar()` encoded as `Foo.getInstance().bar`,
-  // Go's bare-factory `New().Method()` as `New().Method`, or Scala's companion
-  // factory `Foo.create().bar()` (#645/#608 mechanism). Resolve the method's class
-  // from the inner call's declared return type, then validate it.
+  // Go / Scala / Dart) — `Foo.getInstance().bar()` encoded as `Foo.getInstance().bar`,
+  // Go's bare-factory `New().Method()` as `New().Method`, Scala's companion factory
+  // `Foo.create().bar()`, or Dart's static factory / factory-constructor
+  // `Foo.create().bar()` (#645/#608 mechanism). Resolve the method's class from the
+  // inner call's declared return type, then validate it.
   if (
   if (
     ref.language === 'java' ||
     ref.language === 'java' ||
     ref.language === 'kotlin' ||
     ref.language === 'kotlin' ||
     ref.language === 'csharp' ||
     ref.language === 'csharp' ||
     ref.language === 'swift' ||
     ref.language === 'swift' ||
     ref.language === 'go' ||
     ref.language === 'go' ||
-    ref.language === 'scala'
+    ref.language === 'scala' ||
+    ref.language === 'dart'
   ) {
   ) {
     result = matchDottedCallChain(ref, context);
     result = matchDottedCallChain(ref, context);
     if (result) return result;
     if (result) return result;