From e3e33474048adb9a5961dadb1e245f4082e6dd27 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 14 Aug 2026 05:50:55 -0600 Subject: [PATCH 1/2] fix(dataflow): grouped param wrapper gives every name the same paramIndex extractParams incremented index once per child of the parameter list, so a grammar node grouping multiple genuinely separate slots (Dart's optional_formal_parameters for `{int times, bool loud}`) gave every name inside it the same index instead of each getting its own. Adds groupedParamTypes to unpack such wrappers into distinct slots, while still sharing one index for true single-slot destructuring (JS/TS object/array patterns) and skipping non-parameter siblings (default value literals Dart's grammar attaches flat inside the group). Closes #2358 docs check acknowledged Impact: 4 functions changed, 2 affected --- src/ast-analysis/rules/b2.ts | 17 +++++++------- src/ast-analysis/shared.ts | 1 + src/ast-analysis/visitor-utils.ts | 27 +++++++++++++++++++---- src/types.ts | 10 +++++++++ tests/parsers/dataflow-dart.test.ts | 24 ++++++++++++++++++++ tests/parsers/dataflow-javascript.test.ts | 11 +++++++++ 6 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/ast-analysis/rules/b2.ts b/src/ast-analysis/rules/b2.ts index 864ed2870..2e3edfa44 100644 --- a/src/ast-analysis/rules/b2.ts +++ b/src/ast-analysis/rules/b2.ts @@ -547,15 +547,9 @@ function extractDartParamName(node: TreeSitterNode): string[] | null { // groups wrap MULTIPLE formal_parameter children in one // optional_formal_parameters node (node-types.json — confirmed there is // no singular optional_formal_parameter/named_formal_parameter type in - // this grammar). Recurse to collect every name inside the group. - if (node.type === 'optional_formal_parameters') { - const names: string[] = []; - for (const child of node.namedChildren) { - const childNames = extractDartParamName(child); - if (childNames) names.push(...childNames); - } - return names.length > 0 ? names : null; - } + // this grammar). Each grandchild is its own parameter slot, so + // `groupedParamTypes` (issue #2358) makes `extractParams` iterate them + // directly rather than calling this function on the group node itself. if (node.type === 'formal_parameter') { const nameNode = node.childForFieldName('name'); if (nameNode) return [nameNode.text]; @@ -657,6 +651,11 @@ export const dataflowDart: DataflowRulesConfig = makeDataflowRules({ // parameter extraction "happened to work." getParamListNode: getDartParamListNode, extractParamName: extractDartParamName, + // `{int times, bool loud}` (named) / `[int x, int y]` (optional-positional) + // groups wrap multiple genuinely separate formal_parameter slots in one + // optional_formal_parameters node — each must get its own paramIndex, not + // the group's single index (issue #2358). + groupedParamTypes: new Set(['optional_formal_parameters']), // local_variable_declaration's ONLY child is initialized_variable_definition, // which has real `name`/`value` fields (confirmed via node-types.json) — diff --git a/src/ast-analysis/shared.ts b/src/ast-analysis/shared.ts index e59d8c53b..60a36a153 100644 --- a/src/ast-analysis/shared.ts +++ b/src/ast-analysis/shared.ts @@ -98,6 +98,7 @@ export const DATAFLOW_DEFAULTS: DataflowRulesConfig = { shorthandPropPattern: null, pairPatternType: null, extractParamName: null, // override: (node) => string[] + groupedParamTypes: new Set(), // node types grouping multiple separate param slots (Dart's optional_formal_parameters) // Return returnNode: null, diff --git a/src/ast-analysis/visitor-utils.ts b/src/ast-analysis/visitor-utils.ts index a207b8564..03220127d 100644 --- a/src/ast-analysis/visitor-utils.ts +++ b/src/ast-analysis/visitor-utils.ts @@ -33,6 +33,16 @@ interface LanguageRules { memberObjectField: string; optionalChainNode?: string; extractParamName?(node: TreeSitterNode): string[] | null; + /** + * Node types where ONE child of the parameter list groups multiple + * logically separate parameter declarations — e.g. Dart's + * `optional_formal_parameters` for `{int times, bool loud}` (issue #2358). + * Unlike `objectDestructType`/`arrayDestructType`, where multiple bound + * names are extracted from a SINGLE argument slot and must share one + * index, each of this node's own named children is its own slot and gets + * its own index. + */ + groupedParamTypes?: Set; } /** @@ -84,11 +94,20 @@ export function extractParams( const result: ParamInfo[] = []; let index = 0; for (const child of paramsNode.namedChildren) { - const names = extractParamNames(child, rules); - for (const name of names) { - result.push({ name, index }); + // Grouped wrapper types (e.g. Dart's optional_formal_parameters) can mix + // real formal_parameter slots with unrelated named siblings — e.g. a + // default value's literal, which the grammar attaches as a flat sibling + // rather than nesting inside its formal_parameter (issue #2358). Only a + // slot that actually yields a name consumes an index. + const slots = rules.groupedParamTypes?.has(child.type) ? child.namedChildren : [child]; + for (const slot of slots) { + const names = extractParamNames(slot, rules); + if (names.length === 0) continue; + for (const name of names) { + result.push({ name, index }); + } + index++; } - index++; } return result; } diff --git a/src/types.ts b/src/types.ts index 2459d66ce..82c8e2a12 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1359,6 +1359,16 @@ export interface DataflowRulesConfig { shorthandPropPattern: string | null; pairPatternType: string | null; extractParamName: ((node: TreeSitterNode) => string[] | null) | null; + /** + * Node types where ONE child of the parameter list groups multiple + * logically separate parameter declarations — e.g. Dart's + * `optional_formal_parameters` for `{int times, bool loud}` (issue #2358). + * Unlike `objectDestructType`/`arrayDestructType`, where multiple bound + * names are extracted from a SINGLE argument slot and must share one + * index, each of this node's own named children is its own slot and gets + * its own index. + */ + groupedParamTypes: Set; returnNode: string | null; varDeclaratorNode: string | null; varDeclaratorNodes: Set | null; diff --git a/tests/parsers/dataflow-dart.test.ts b/tests/parsers/dataflow-dart.test.ts index b3f5941a8..5fa2f48fa 100644 --- a/tests/parsers/dataflow-dart.test.ts +++ b/tests/parsers/dataflow-dart.test.ts @@ -67,6 +67,30 @@ describe('extractDataflow — Dart', () => { ]), ); }); + + it('gives each name in a named-parameter group its own paramIndex (#2358)', () => { + const data = parseAndExtract( + 'int greet(String name, {int times = 1, bool loud = false}) {\n return times;\n}\n', + ); + expect(data!.parameters).toEqual( + expect.arrayContaining([ + expect.objectContaining({ funcName: 'greet', paramName: 'name', paramIndex: 0 }), + expect.objectContaining({ funcName: 'greet', paramName: 'times', paramIndex: 1 }), + expect.objectContaining({ funcName: 'greet', paramName: 'loud', paramIndex: 2 }), + ]), + ); + }); + + it('gives each name in an optional-positional parameter group its own paramIndex', () => { + const data = parseAndExtract('int f(int a, [int b, int c]) {\n return a;\n}\n'); + expect(data!.parameters).toEqual( + expect.arrayContaining([ + expect.objectContaining({ funcName: 'f', paramName: 'a', paramIndex: 0 }), + expect.objectContaining({ funcName: 'f', paramName: 'b', paramIndex: 1 }), + expect.objectContaining({ funcName: 'f', paramName: 'c', paramIndex: 2 }), + ]), + ); + }); }); describe('returns', () => { diff --git a/tests/parsers/dataflow-javascript.test.ts b/tests/parsers/dataflow-javascript.test.ts index cb23eea89..515bb5022 100644 --- a/tests/parsers/dataflow-javascript.test.ts +++ b/tests/parsers/dataflow-javascript.test.ts @@ -51,6 +51,17 @@ describe('extractDataflow — JavaScript', () => { ); }); + it('keeps destructured names sharing one argument slot at the same paramIndex (#2358)', () => { + const data = parseAndExtract(`function greet({ name, age }, city) { return name; }`); + expect(data.parameters).toEqual( + expect.arrayContaining([ + expect.objectContaining({ funcName: 'greet', paramName: 'name', paramIndex: 0 }), + expect.objectContaining({ funcName: 'greet', paramName: 'age', paramIndex: 0 }), + expect.objectContaining({ funcName: 'greet', paramName: 'city', paramIndex: 1 }), + ]), + ); + }); + it('extracts default parameters', () => { const data = parseAndExtract(`function inc(x, step = 1) { return x + step; }`); expect(data.parameters).toEqual( From 34347395ce81af89a6a765fedc6d8cb6a030d94f Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 14 Aug 2026 06:18:41 -0600 Subject: [PATCH 2/2] fix(dataflow): unnamed parameter no longer skips its argument position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit's skip-empty-slot guard applied to every child of the parameter list, not just grouped ones — an unnamed C/C++ parameter (e.g. void f(int, int value)) yields zero names and was no longer consuming an index, shifting every later named parameter's position down by one. Scopes the skip to grouped slots only; an ordinary child still always consumes an index regardless of name count. docs check acknowledged Impact: 1 functions changed, 2 affected --- src/ast-analysis/visitor-utils.ts | 35 ++++++++++++++++++++---------- tests/parsers/dataflow-cpp.test.ts | 9 ++++++++ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/ast-analysis/visitor-utils.ts b/src/ast-analysis/visitor-utils.ts index 03220127d..0c52ee72e 100644 --- a/src/ast-analysis/visitor-utils.ts +++ b/src/ast-analysis/visitor-utils.ts @@ -94,20 +94,31 @@ export function extractParams( const result: ParamInfo[] = []; let index = 0; for (const child of paramsNode.namedChildren) { - // Grouped wrapper types (e.g. Dart's optional_formal_parameters) can mix - // real formal_parameter slots with unrelated named siblings — e.g. a - // default value's literal, which the grammar attaches as a flat sibling - // rather than nesting inside its formal_parameter (issue #2358). Only a - // slot that actually yields a name consumes an index. - const slots = rules.groupedParamTypes?.has(child.type) ? child.namedChildren : [child]; - for (const slot of slots) { - const names = extractParamNames(slot, rules); - if (names.length === 0) continue; - for (const name of names) { - result.push({ name, index }); + if (rules.groupedParamTypes?.has(child.type)) { + // Grouped wrapper types (e.g. Dart's optional_formal_parameters) can + // mix real formal_parameter slots with unrelated named siblings — + // e.g. a default value's literal, which the grammar attaches as a + // flat sibling rather than nesting inside its formal_parameter + // (issue #2358). Only a slot that actually yields a name consumes an + // index; an ordinary (non-grouped) child still consumes one below + // even with zero names, since an unnamed parameter (e.g. C++'s + // `void f(int, int value)`) is a real slot that must not collapse + // into the next one. + for (const slot of child.namedChildren) { + const names = extractParamNames(slot, rules); + if (names.length === 0) continue; + for (const name of names) { + result.push({ name, index }); + } + index++; } - index++; + continue; } + const names = extractParamNames(child, rules); + for (const name of names) { + result.push({ name, index }); + } + index++; } return result; } diff --git a/tests/parsers/dataflow-cpp.test.ts b/tests/parsers/dataflow-cpp.test.ts index ce3a11047..15faebd29 100644 --- a/tests/parsers/dataflow-cpp.test.ts +++ b/tests/parsers/dataflow-cpp.test.ts @@ -73,6 +73,15 @@ describe('extractDataflow — C++', () => { ]), ); }); + + it('keeps a named parameter at its true position after a preceding unnamed parameter (#2358)', () => { + const data = parseAndExtract('void f(int, int value) { value; }'); + expect(data?.parameters).toEqual( + expect.arrayContaining([ + expect.objectContaining({ funcName: 'f', paramName: 'value', paramIndex: 1 }), + ]), + ); + }); }); // ── Return statements ─────────────────────────────────────────────────