Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/ast-analysis/rules/b2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/ast-analysis/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>): DataflowRulesConfig {
Expand Down
13 changes: 13 additions & 0 deletions src/ast-analysis/visitors/dataflow-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1384,6 +1384,18 @@ export interface DataflowRulesConfig {
equalsClauseType: string | null;
argumentWrapperType: string | null;
extraIdentifierTypes: Set<string> | 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. */
Expand Down
34 changes: 34 additions & 0 deletions tests/parsers/dataflow-dart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Loading