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..0c52ee72e 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,6 +94,26 @@ export function extractParams( const result: ParamInfo[] = []; let index = 0; for (const child of paramsNode.namedChildren) { + 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++; + } + continue; + } const names = extractParamNames(child, rules); for (const name of names) { result.push({ name, index }); 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-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 ───────────────────────────────────────────────── 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(