diff --git a/gitnexus/src/core/ingestion/languages/dart/captures.ts b/gitnexus/src/core/ingestion/languages/dart/captures.ts index c3a09b91b..371373160 100644 --- a/gitnexus/src/core/ingestion/languages/dart/captures.ts +++ b/gitnexus/src/core/ingestion/languages/dart/captures.ts @@ -204,6 +204,19 @@ export function emitDartScopeCaptures( '@type-binding.name': syntheticCapture('@type-binding.name', propNode, fieldName), '@type-binding.type': syntheticCapture('@type-binding.type', propNode, fieldType), }); + } else { + // No written type, so the field's type comes from the constructor its + // initializer calls (#2807). `constructor-inferred` is the weakest + // source, and the annotated branch above already returned, so an + // annotated field is untouched either way. + const callee = dartFieldConstructorCallee(propNode); + if (callee !== null) { + out.push({ + '@type-binding.constructor': nodeToCapture('@type-binding.constructor', propNode), + '@type-binding.name': syntheticCapture('@type-binding.name', propNode, fieldName), + '@type-binding.type': syntheticCapture('@type-binding.type', propNode, callee.text), + }); + } } continue; } @@ -535,6 +548,46 @@ function findDirectCallValue(initVarDef: SyntaxNode): SyntaxNode | null { return null; } +/** + * Callee identifier of a class field initialized by a direct constructor call — + * `var b = Outer();` / `final b = Outer();` — or `null` for anything else. + * + * Dart spells a class field as `declaration(, initialized_identifier_list( + * initialized_identifier))`, NOT the `initialized_variable_definition` that + * `emitVarTypeBinding` handles — that is the LOCAL form. So an unannotated field + * had no type binding and could not act as a call receiver (#2807), even though + * its annotated twin resolved fine. + * + * Accepts the same construction shape `findDirectCallValue` accepts for locals: + * a bare identifier followed by a `selector` carrying an `argument_part`. + * Anything else — a literal, a member call, an await — is left alone rather than + * guessed at. + */ +function dartFieldConstructorCallee(propNode: SyntaxNode): SyntaxNode | null { + const initialized = firstDescendantOfType(propNode, 'initialized_identifier'); + if (initialized === null) return null; + // namedChild(0) is the field NAME; the initializer starts after it. + const value = initialized.namedChild(1); + if (value === null || value.type !== 'identifier') return null; + const next = value.nextNamedSibling; + if (next === null || next.type !== 'selector') return null; + return next.namedChild(0)?.type === 'argument_part' ? value : null; +} + +/** First strict descendant of `type`, breadth-first, or `null`. */ +function firstDescendantOfType(root: SyntaxNode, type: string): SyntaxNode | null { + const queue: SyntaxNode[] = [root]; + while (queue.length > 0) { + const node = queue.shift()!; + if (node !== root && node.type === type) return node; + for (let i = 0; i < node.namedChildCount; i++) { + const child = node.namedChild(i); + if (child !== null) queue.push(child); + } + } + return null; +} + function emitVarTypeBinding(initVarDef: SyntaxNode, out: CaptureMatch[]): void { const nameNode = initVarDef.childForFieldName('name'); if (nameNode === null) return; diff --git a/gitnexus/src/core/ingestion/languages/dart/query.ts b/gitnexus/src/core/ingestion/languages/dart/query.ts index d6646b92f..38496f2ec 100644 --- a/gitnexus/src/core/ingestion/languages/dart/query.ts +++ b/gitnexus/src/core/ingestion/languages/dart/query.ts @@ -163,6 +163,27 @@ const DART_SCOPE_QUERY = ` (initialized_identifier . (identifier) @declaration.name))) @declaration.property +; Inference-typed fields — \`var b = Outer();\`, \`final b = Outer();\`, +; \`late final b = Outer();\`, \`static var b = Outer();\` (#2807). The two +; patterns above require a written type, so a field whose type comes from its +; initializer produced NO property declaration at all — no Property node, and +; nothing for captures.ts to hang a type binding on, so \`b.inner()\` could not +; resolve its receiver while the annotated twin resolved fine. +; +; Dart spells the keyword as \`inferred_type\` for \`var\` and \`final_builtin\` +; for \`final\` / \`late final\`; both are class fields and both are idiomatic, +; so covering only one would leave the more common Dart style broken. +(declaration + (inferred_type) + (initialized_identifier_list + (initialized_identifier + . (identifier) @declaration.name))) @declaration.property +(declaration + (final_builtin) + (initialized_identifier_list + (initialized_identifier + . (identifier) @declaration.name))) @declaration.property + ; ── Declarations — closure bindings (#2693) ────────────────────────────────── ; \`var f = (x) => x;\` binds a callable. Without a declaration the binding has ; no SymbolDefinition, so callable-value-flow has nothing to attach its seed to