From a2ed181055033ec6113e509bf5cad0550c59ecad Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Tue, 7 Apr 2026 11:01:53 -0500 Subject: [PATCH] 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. --- src/extraction/languages/dart.ts | 62 +++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/extraction/languages/dart.ts b/src/extraction/languages/dart.ts index c0341b3..5b545d0 100644 --- a/src/extraction/languages/dart.ts +++ b/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; + }, };