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
17 changes: 8 additions & 9 deletions src/ast-analysis/rules/b2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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) —
Expand Down
1 change: 1 addition & 0 deletions src/ast-analysis/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions src/ast-analysis/visitor-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
}

/**
Expand Down Expand Up @@ -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 });
Expand Down
10 changes: 10 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
returnNode: string | null;
varDeclaratorNode: string | null;
varDeclaratorNodes: Set<string> | null;
Expand Down
9 changes: 9 additions & 0 deletions tests/parsers/dataflow-cpp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────────────────────────────────────────────────
Expand Down
24 changes: 24 additions & 0 deletions tests/parsers/dataflow-dart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
11 changes: 11 additions & 0 deletions tests/parsers/dataflow-javascript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading