diff --git a/src/ast-analysis/rules/b2.ts b/src/ast-analysis/rules/b2.ts index 66f201b9b..d85445c9e 100644 --- a/src/ast-analysis/rules/b2.ts +++ b/src/ast-analysis/rules/b2.ts @@ -596,6 +596,12 @@ export const dataflowDart: DataflowRulesConfig = makeDataflowRules({ extractParamName: extractDartParamName, returnNode: 'return_statement', + // Arrow/`=>`-style implicit return (issue #2356): tree-sitter-dart's + // function_body node's ONLY child is either a `block` (`{ ... }`) or one + // of many bare expression types directly (`int f() => x + 1;` — no + // return_statement at all). blockBodyNode defaults to 'block', which + // already matches Dart's own block type name. + implicitReturnBodyNode: 'function_body', callNode: 'call_expression', // tree-sitter-dart does not have standard named fields for calls; diff --git a/src/ast-analysis/shared.ts b/src/ast-analysis/shared.ts index 2086e28ee..4661e93ea 100644 --- a/src/ast-analysis/shared.ts +++ b/src/ast-analysis/shared.ts @@ -137,6 +137,10 @@ export const DATAFLOW_DEFAULTS: DataflowRulesConfig = { equalsClauseType: null, // C#: equals_value_clause wraps variable initializer argumentWrapperType: null, // PHP: individual args wrapped in 'argument' nodes extraIdentifierTypes: null, // Set of additional identifier-like types (PHP: variable_name, name) + + // Arrow/`=>`-style implicit-return body (issue #2356) + implicitReturnBodyNode: null, + blockBodyNode: 'block', }; export function makeDataflowRules(overrides: Partial): DataflowRulesConfig { diff --git a/src/ast-analysis/visitors/dataflow-visitor.ts b/src/ast-analysis/visitors/dataflow-visitor.ts index ebc687a3c..e6df47172 100644 --- a/src/ast-analysis/visitors/dataflow-visitor.ts +++ b/src/ast-analysis/visitors/dataflow-visitor.ts @@ -484,6 +484,19 @@ function dispatchDataflowNode(ctx: DataflowDispatchCtx, node: TreeSitterNode): v if (rules.functionNodes.has(t)) return; + // Arrow/`=>`-style implicit return (issue #2356): this node's grammar + // allows a bare expression directly in place of a block-wrapped statement + // body. When it's NOT block-wrapped, treat the node itself as if it were a + // return_statement — handleReturn only reads namedChildren[0], so it works + // unmodified regardless of the actual wrapper node's type. + if (rules.implicitReturnBodyNode && t === rules.implicitReturnBodyNode) { + const sole = node.namedChildCount === 1 ? node.namedChildren[0] : null; + if (sole && sole.type !== rules.blockBodyNode) { + handleReturn(node, rules, ctx.scopeStack, ctx.returns); + } + return; + } + if (rules.returnNode && t === rules.returnNode) { handleReturn(node, rules, ctx.scopeStack, ctx.returns); return; diff --git a/src/types.ts b/src/types.ts index 3699379c2..36787caf4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1384,6 +1384,18 @@ export interface DataflowRulesConfig { equalsClauseType: string | null; argumentWrapperType: string | null; extraIdentifierTypes: Set | null; + /** + * Node type whose grammar allows EITHER a `blockBodyNode`-wrapped statement + * body OR a bare expression directly as its child (an arrow/`=>`-style + * implicit return, e.g. tree-sitter-dart's `function_body` in + * `int f() => x + 1;`). When set and the node's only named child is not + * `blockBodyNode`, that child is recorded as an implicit `returns` entry + * (issue #2356) — mirrors what a `return_statement` around the same + * expression would have produced. + */ + implicitReturnBodyNode: string | null; + /** The "real" statement-block wrapper type `implicitReturnBodyNode` is compared against. */ + blockBodyNode: string; } /** Language rule module: exports from each language rule file. */ diff --git a/tests/parsers/dataflow-dart.test.ts b/tests/parsers/dataflow-dart.test.ts index b479c69e0..d5f678101 100644 --- a/tests/parsers/dataflow-dart.test.ts +++ b/tests/parsers/dataflow-dart.test.ts @@ -109,5 +109,39 @@ describe('extractDataflow — Dart', () => { ]), ); }); + + // Issue #2356: tree-sitter-dart represents an arrow-bodied function's + // function_body as containing the expression DIRECTLY — no + // return_statement node at all — so returnNode's exact-type match never + // fires for these, independent of the #2182 sibling-body architecture. + it('captures an implicit return from a top-level arrow-bodied function (no return_statement in the grammar)', () => { + const data = parseAndExtract('int multiply(int x, int y) => x * y;\n'); + expect(data!.returns).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + funcName: 'multiply', + referencedNames: expect.arrayContaining(['x', 'y']), + }), + ]), + ); + }); + + it('captures an implicit return from an arrow-bodied class method', () => { + const data = parseAndExtract('class Calculator {\n int add(int a, int b) => a + b;\n}\n'); + expect(data!.returns).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + funcName: 'add', + referencedNames: expect.arrayContaining(['a', 'b']), + }), + ]), + ); + }); + + it('does not double-count a block-bodied function as an implicit return', () => { + const data = parseAndExtract('int multiply(int x, int y) {\n return x * y;\n}\n'); + const multiplyReturns = (data!.returns as any[]).filter((r) => r.funcName === 'multiply'); + expect(multiplyReturns).toHaveLength(1); + }); }); });