Skip to content

Commit b283f44

Browse files
committed
fix(dataflow): guard exitFunction pop against early-return enterFunctionScope
enterFunctionScope returns early (no push) when nameExtractor rejects a node. The unconditional scopeStack.pop() in exitFunction caused asymmetric push/pop for languages like Elixir and Clojure whose functionNodes include generic node types (call/list_lit) that only sometimes represent function definitions. Fix: enterFunctionScope now returns boolean (true = pushed). A parallel pushRecord boolean stack in createDataflowVisitor records the result of each enterFunction call; exitFunction pops scopeStack only when pushRecord.pop() is true.
1 parent a5afa8a commit b283f44

1 file changed

Lines changed: 17 additions & 5 deletions

File tree

src/ast-analysis/visitors/dataflow-visitor.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -426,17 +426,23 @@ function handleReturn(
426426
}
427427
}
428428

429-
/** Collect parameter entries for a function and push a new scope onto the stack. */
429+
/**
430+
* Collect parameter entries for a function and push a new scope onto the stack.
431+
*
432+
* Returns `true` if a scope was pushed, `false` if the node was skipped (i.e.
433+
* `nameExtractor` rejected it). Callers must pop the stack only when this
434+
* returns `true` to keep push/pop symmetric.
435+
*/
430436
function enterFunctionScope(
431437
funcNode: TreeSitterNode,
432438
rules: AnyRules,
433439
scopeStack: ScopeEntry[],
434440
parameters: DataflowParam[],
435-
): void {
441+
): boolean {
436442
// When nameExtractor is set it acts as a gate: null means this node is not a function
437443
// definition. Needed for languages (Elixir, Clojure) where functionNodes includes generic
438444
// node types (call/list_lit) that are only sometimes function definitions.
439-
if (rules.nameExtractor && !rules.nameExtractor(funcNode)) return;
445+
if (rules.nameExtractor && !rules.nameExtractor(funcNode)) return false;
440446
const name = functionName(funcNode, rules);
441447
const paramsNode = rules.getParamListNode
442448
? rules.getParamListNode(funcNode)
@@ -455,6 +461,7 @@ function enterFunctionScope(
455461
}
456462
}
457463
scopeStack.push({ funcName: name, funcNode, params: paramMap, locals: new Map() });
464+
return true;
458465
}
459466

460467
interface DataflowDispatchCtx {
@@ -516,6 +523,11 @@ export function createDataflowVisitor(rules: AnyRules): Visitor {
516523
const argFlows: DataflowArgFlow[] = [];
517524
const mutations: DataflowMutation[] = [];
518525
const scopeStack: ScopeEntry[] = [];
526+
// Parallel stack that records whether each enterFunction call actually pushed a
527+
// scope frame. exitFunction pops scopeStack only when the matching entry is true,
528+
// keeping push/pop symmetric even for languages (Elixir, Clojure) where
529+
// enterFunctionScope may return early without pushing.
530+
const pushRecord: boolean[] = [];
519531

520532
const dispatchCtx: DataflowDispatchCtx = {
521533
rules,
@@ -536,15 +548,15 @@ export function createDataflowVisitor(rules: AnyRules): Visitor {
536548
_funcName: string | null,
537549
_context: VisitorContext,
538550
): void {
539-
enterFunctionScope(funcNode, rules, scopeStack, parameters);
551+
pushRecord.push(enterFunctionScope(funcNode, rules, scopeStack, parameters));
540552
},
541553

542554
exitFunction(
543555
_funcNode: TreeSitterNode,
544556
_funcName: string | null,
545557
_context: VisitorContext,
546558
): void {
547-
scopeStack.pop();
559+
if (pushRecord.pop()) scopeStack.pop();
548560
},
549561

550562
enterNode(node: TreeSitterNode, _context: VisitorContext): EnterNodeResult | undefined {

0 commit comments

Comments
 (0)