Просмотр исходного кода

feat: Add Dart bare call extraction for selector-based method calls

Addresses Dart method calls like `obj.method()` and `runApp()` that parse as identifier+selector combinations instead of dedicated call nodes. Adds extractBareCall hook to detect selector nodes with argument_part, handling simple function calls, method chains, constructor calls (new/const), and super/this method calls. Enables proper call relationship tracking for Dart's selector-based call syntax.
Colby McHenry 2 месяцев назад
Родитель
Сommit
a2ed181055
1 измененных файлов с 61 добавлено и 1 удалено
  1. 61 1
      src/extraction/languages/dart.ts

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

@@ -12,7 +12,7 @@ export const dartExtractor: LanguageExtractor = {
   enumMemberTypes: ['enum_constant'],
   typeAliasTypes: ['type_alias'],
   importTypes: ['import_or_export'],
-  callTypes: [],  // Dart calls use identifier+selector, handled via function body traversal
+  callTypes: [],  // Dart calls use identifier+selector, handled via extractBareCall
   variableTypes: [],
   extraClassNodeTypes: ['mixin_declaration', 'extension_declaration'],
   resolveBody: (node, bodyField) => {
@@ -132,4 +132,64 @@ export const dartExtractor: LanguageExtractor = {
     }
     return null;
   },
+  extractBareCall: (node, _source) => {
+    // Dart calls are: identifier + selector(argument_part), not a dedicated call node.
+    // Match on selector nodes that contain argument_part.
+    if (node.type === 'selector') {
+      const hasArgPart = node.namedChildren.some((c: SyntaxNode) => c.type === 'argument_part');
+      if (!hasArgPart) return undefined;
+
+      const prev = node.previousNamedSibling;
+      if (!prev) return undefined;
+
+      // Simple function/constructor call: prev is identifier (e.g., runApp(...), MyWidget(...))
+      if (prev.type === 'identifier') {
+        return prev.text;
+      }
+
+      // Method call: prev is selector with accessor (e.g., obj.method(...), Navigator.push(...))
+      if (prev.type === 'selector') {
+        const accessor = prev.namedChildren.find((c: SyntaxNode) =>
+          c.type === 'unconditional_assignable_selector' || c.type === 'conditional_assignable_selector'
+        );
+        if (accessor) {
+          const methodId = accessor.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
+          if (methodId) {
+            // Include receiver for first call in chain (receiver is a direct identifier)
+            const accessorPrev = prev.previousNamedSibling;
+            if (accessorPrev?.type === 'identifier') {
+              return accessorPrev.text + '.' + methodId.text;
+            }
+            return methodId.text;
+          }
+        }
+      }
+
+      // super.method() / this.method(): prev is bare unconditional_assignable_selector
+      if (prev.type === 'unconditional_assignable_selector' || prev.type === 'conditional_assignable_selector') {
+        const methodId = prev.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
+        if (methodId) return methodId.text;
+      }
+
+      return undefined;
+    }
+
+    // new MyWidget() — explicit constructor call
+    if (node.type === 'new_expression') {
+      const typeId = node.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier');
+      if (typeId) return typeId.text;
+      return undefined;
+    }
+
+    // const EdgeInsets.all(8.0) — const constructor call
+    if (node.type === 'const_object_expression') {
+      const typeId = node.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier');
+      const nameId = node.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
+      if (typeId && nameId) return typeId.text + '.' + nameId.text;
+      if (typeId) return typeId.text;
+      return undefined;
+    }
+
+    return undefined;
+  },
 };