Przeglądaj źródła

feat(impact): Dart mixins (with) + method type references

Two Dart gaps, both gated to `language === 'dart'` in tree-sitter.ts:

- Mixins: `class C extends Base with M1, M2` packs the `with` mixins into a
  `mixins` child of the `superclass` node. The generic inheritance path read
  `superclass`'s first child as the base and dropped the mixins entirely — and
  `class C with M` (no extends) even had its `mixins` node misread as the
  superclass. Mixins are Dart's core composition mechanism (Flutter is built on
  them). Emit `extends` for the base type and `implements` for each mixin.
- Method type references: Dart was in the type-annotation languages but produced
  ZERO `references` edges, because a `method_signature` wraps the real
  `function_signature` (where params + return live) and the return type is a bare
  `type_identifier` child, not a `type` field — so the generic getChildByField
  walk found nothing. Walk the inner signature; a class or enum used only as a
  parameter/return type now records a dependency.

Measured (fair cross-file dependent coverage, symbol-bearing source files):
flutter/packages 88.8% → 92.4%, dio 86.4% → 87.9%. Node count stable (edges only).
Residual is genuine frontiers: see-through export barrels (`lib/<pkg>.dart`,
`types.dart`), platform-conditional files (`_io.dart`/`_web.dart`/`_stub.dart`),
and enum-value access (`MediaDeviceKind.videoInput` — the cross-language
static-member/value-read frontier, deferred). Full suite green; Dart-only change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby McHenry 2 tygodni temu
rodzic
commit
94879546bd
3 zmienionych plików z 127 dodań i 0 usunięć
  1. 1 0
      CHANGELOG.md
  2. 72 0
      __tests__/extraction.test.ts
  3. 54 0
      src/extraction/tree-sitter.ts

+ 1 - 0
CHANGELOG.md

@@ -24,6 +24,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Kotlin Multiplatform `expect`/`actual` declarations are now connected. A platform implementation — `actual fun`, `actual class`, or an `actual typealias` in a `jvm` / `native` / `js` / `wasm` source set — is linked to the common `expect` declaration it fulfills (including the common case of an `expect class` fulfilled by an `actual typealias`). Previously a caller in common code resolved to the `expect` declaration, so every platform `actual` looked like it had no dependents and editing one showed an empty blast radius; now changing a platform implementation surfaces the common API and everything that uses it. (Kotlin)
 - Scala impact and `codegraph affected` now connect the type graph that typeclass-style code is built on. A parameterized supertype (`trait Monoid[A] extends Semigroup[A] with Serializable`) now links to each parent; a type used in a `val`/`def` signature, as a type argument, or as a context bound (`def f[A: Monoid]`) — including the trailing implicit parameter list (`(implicit M: Monoid[A])`) where typeclass instances are passed — now records a dependency; and `new T[...] { … }` counts as an instantiation. Previously Scala linked only plain calls and bare, non-generic supertypes, so a trait extended with type parameters, used as a type, or required as an implicit looked like nothing depended on it — which on a typeclass-heavy codebase (cats, algebra) was most of the graph. (Scala)
 - PHP impact and `codegraph affected` now understand namespaces and `use` imports. Classes are tracked by their namespaced name, so the many same-named classes a framework defines (Laravel has 7+ `Factory` interfaces, several `Dispatcher`s, across namespaces) are told apart instead of collapsing into one arbitrary match. A `use App\Contracts\Cache\Factory;` now records a dependency on exactly that class — so a contract or interface that's imported and constructor-injected (the dependency-injection pattern) is no longer reported as having no dependents — and parameter, property, and return type-hints are recorded too. Previously PHP ignored namespaces entirely and linked only calls, `new`, and inheritance. (PHP)
+- Dart impact and `codegraph affected` now follow mixins and method type annotations. A `with` mixin — Dart's core composition mechanism, which Flutter is built on — now records a dependency, so editing a mixin surfaces every class that mixes it in (the whole `with` clause used to be dropped, and a class declared `with M` alone even lost its real superclass link). And types used in a method's parameters or return value now link to their definition, so a class or enum referenced only as a type — not constructed or called — is no longer reported as having no dependents. (Dart)
 - C++ free functions are now indexed under their real name. A function written with a qualified-type parameter (`std::string TableFileName(const std::string& dbname)`) or an `auto … -> std::string` trailing return type was mistakenly named after that type (`string`), so calls to it never resolved, `codegraph_node` couldn't find it by name, and the file defining it looked like nothing depended on it. The function now keeps its real name, so cross-file calls, callers, and blast radius work — a meaningful gain for any namespaced C++ codebase (this is how most free functions in a library look). (C++)
 - Ruby impact and `codegraph affected` now follow mixins and `require`s. `include`, `extend`, and `prepend` of a module — Ruby's primary composition mechanism (ActiveSupport concerns, `Comparable`, `Enumerable`) — now record a dependency on that module, so editing a concern surfaces every class that mixes it in; previously these were read as a call to a method named `include`, so a module whose methods are exercised only by application code looked like nothing depended on it. And `require "lib/foo"` / `require_relative "../foo"` now link to the required file, so a file pulled in only by a `require` (config-loaded components, gems that don't autoload) is no longer reported as having no dependents. Together these took a typical gem from ~71% of its files showing real dependents to ~100%. (Ruby)
 - C# `record` types are now indexed. `record`, `record class`, and `record struct` declarations (everywhere in modern C# — DTOs, value objects, CQRS messages, MediatR notifications) were previously skipped entirely, so every reference, generic type argument (`IEnumerable<MyRecord>`), and `new MyRecord(...)` pointed at nothing and the file defining a record looked like it had no callers or dependents. (#237)

+ 72 - 0
__tests__/extraction.test.ts

@@ -3614,6 +3614,78 @@ std::string use() {
   });
 });
 
+describe('Dart mixins and type references', () => {
+  let tempDir: string;
+  let cg: CodeGraph;
+
+  beforeEach(() => {
+    tempDir = createTempDir();
+  });
+
+  afterEach(() => {
+    if (cg) cg.close();
+    if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
+  });
+
+  it('links `with` mixins and method parameter/return types across files', async () => {
+    const lib = path.join(tempDir, 'lib');
+    fs.mkdirSync(lib, { recursive: true });
+
+    fs.writeFileSync(
+      path.join(lib, 'models.dart'),
+      `class User {
+  final String name;
+  User(this.name);
+}
+
+mixin Loggable {
+  void log() {}
+}
+
+abstract class Repository {
+  User find(int id);
+}
+`
+    );
+    fs.writeFileSync(
+      path.join(lib, 'service.dart'),
+      `import 'models.dart';
+
+class UserService extends Repository with Loggable {
+  @override
+  User find(int id) => User('x');
+
+  List<User> all() => [];
+}
+`
+    );
+
+    cg = CodeGraph.initSync(tempDir);
+    await cg.indexAll();
+    cg.resolveReferences();
+
+    const inModels = (name: string) =>
+      cg.getNodesByKind('class').concat(cg.getNodesByKind('module'))
+        .find((n) => n.name === name && n.filePath.endsWith('models.dart'));
+
+    // The `with Loggable` mixin records a dependency — editing the mixin surfaces
+    // the class that mixes it in (across files). Loggable is a `mixin`, indexed
+    // as a class-like node.
+    const loggable = cg.getNodesByKind('class').find((n) => n.name === 'Loggable')
+      ?? cg.getNodesByKind('module').find((n) => n.name === 'Loggable');
+    expect(loggable, 'Loggable mixin indexed').toBeDefined();
+    const mixinUsers = [...cg.getImpactRadius(loggable!.id, 3).nodes.values()].map((n) => n.name);
+    expect(mixinUsers).toContain('UserService');
+
+    // `User` is used only as a method parameter/return type in service.dart —
+    // editing it must still surface service.dart via the type references.
+    const user = inModels('User') ?? cg.getNodesByKind('class').find((n) => n.name === 'User');
+    expect(user, 'User indexed').toBeDefined();
+    const userDeps = [...cg.getImpactRadius(user!.id, 3).nodes.values()].map((n) => n.filePath ?? '');
+    expect(userDeps.some((p) => p.endsWith('service.dart'))).toBe(true);
+  });
+});
+
 describe('Full Indexing', () => {
   let tempDir: string;
 

+ 54 - 0
src/extraction/tree-sitter.ts

@@ -2781,6 +2781,39 @@ export class TreeSitterExtractor {
           }
           continue;
         }
+        // Dart: `class C extends Base with M1, M2` — the `superclass` node holds
+        // the extends type as a direct `type_identifier` AND a `mixins` child
+        // listing the `with` mixins (and `class C with M` has ONLY mixins, no
+        // extends type). The generic `namedChild(0)` path would read the
+        // `mixins` node itself as the superclass and drop every mixin — yet
+        // mixins are Dart's core composition mechanism (Flutter is built on
+        // them). Emit `extends` for the base and `implements` for each mixin.
+        if (this.language === 'dart' && child.type === 'superclass') {
+          for (const t of child.namedChildren) {
+            if (t.type === 'mixins') {
+              for (const m of t.namedChildren) {
+                if (m.type === 'type_identifier') {
+                  this.unresolvedReferences.push({
+                    fromNodeId: classId,
+                    referenceName: getNodeText(m, this.source),
+                    referenceKind: 'implements',
+                    line: m.startPosition.row + 1,
+                    column: m.startPosition.column,
+                  });
+                }
+              }
+            } else if (t.type === 'type_identifier') {
+              this.unresolvedReferences.push({
+                fromNodeId: classId,
+                referenceName: getNodeText(t, this.source),
+                referenceKind: 'extends',
+                line: t.startPosition.row + 1,
+                column: t.startPosition.column,
+              });
+            }
+          }
+          continue;
+        }
         // Extract parent class/interface names
         // Java uses type_list wrapper: superclass -> type_identifier, extends_interfaces -> type_list -> type_identifier
         const typeList = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_list');
@@ -3142,6 +3175,27 @@ export class TreeSitterExtractor {
       return;
     }
 
+    // Dart: a `method_signature` wraps the real `function_signature` (where the
+    // params and return type live), and the return type is a bare
+    // `type_identifier` child, not a `type` field — so getChildByField below
+    // finds neither. Walk the inner signature: param names / the method name are
+    // `identifier` (not `type_identifier`), so only types surface.
+    if (this.language === 'dart') {
+      let sig: SyntaxNode | undefined = node;
+      if (node.type === 'method_signature') {
+        sig = node.namedChildren.find(
+          (c: SyntaxNode) =>
+            c.type === 'function_signature' ||
+            c.type === 'getter_signature' ||
+            c.type === 'setter_signature' ||
+            c.type === 'constructor_signature' ||
+            c.type === 'factory_constructor_signature'
+        ) ?? node;
+      }
+      this.extractTypeRefsFromSubtree(sig, nodeId);
+      return;
+    }
+
     // Extract parameter type annotations. Scala curries — `def f(a)(implicit
     // M: TC)` has MULTIPLE `parameters` siblings, and the typeclass is almost
     // always in the trailing implicit list — so walk every parameter list, not