import type { BashCommandContext } from "#src/types"; import type { TSNode } from "./parser"; /** * AST node types whose interior commands really execute when the shell runs the * program: command substitution (`$(…)`, backticks) and process substitution * (`<(…)`/`>(…)`). * * Subshells (`( … )`) are deliberately absent — a subshell is also a command * unit in its own right, so the command enumerator emits it whole and descends * it separately rather than treating it as a pure nesting wrapper. * * This map is the single vocabulary shared by the bash command surface and the * bash path surface, so the two cannot disagree about what counts as a nested * execution (#741). */ export const NESTED_EXECUTION_CONTEXTS: ReadonlyMap< string, BashCommandContext > = new Map([ ["command_substitution", "command_substitution"], ["process_substitution", "process_substitution"], ] satisfies [string, BashCommandContext][]); /** * AST node types that are neither commands nor argument values themselves, but * whose subtree can host a nested execution context that really runs. * * A redirect destination is the motivating case: tree-sitter-bash parses * `echo hi > $(rm x)` with the `file_redirect` as a *sibling* of the `command`, * so a consumer that abandons the redirect never sees the substitution inside * it — the bypass #741 fixed. * * An interpolating heredoc body is the second case: `cat < = new Set([ "file_redirect", "heredoc_redirect", "herestring_redirect", "heredoc_body", ]); /** * Visit every execution context `node` *is or contains*, in source order. * * The root-inclusive question, and the one nearly every consumer asks: a node * handed in can be a substitution outright (`> $(cmd)`) or merely host one * (`> ${DIR}/$(cmd)`), and both really execute. {@link forEachNestedExecution} * answers the strictly-within question instead, which is what a visitor needs * once it has already decided to treat a context's interior itself. */ export function forEachExecutionIn( node: TSNode, visit: (contextNode: TSNode, context: BashCommandContext) => void, ): void { const context = NESTED_EXECUTION_CONTEXTS.get(node.type); if (context) visit(node, context); else forEachNestedExecution(node, visit); } /** * Visit every nested execution context in `node`'s subtree, in source order. * * The walk does not descend *past* a context it finds: `visit` receives the * context node itself and decides how to treat its interior (the command * enumerator enumerates commands there; the path collector collects operand * tokens), which keeps recursion policy with the consumer that understands it. * * A substitution can nest under `command_name` (when the whole command is * `$(…)`), under an argument, inside a redirect destination, or inside an * interpolating heredoc body, so the entire subtree is searched. * * `node` itself is never visited, however it is typed — use * {@link forEachExecutionIn} when it may *be* a context rather than merely * contain one. */ export function forEachNestedExecution( node: TSNode, visit: (contextNode: TSNode, context: BashCommandContext) => void, ): void { for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (!child) continue; const context = NESTED_EXECUTION_CONTEXTS.get(child.type); if (context) { visit(child, context); } else { forEachNestedExecution(child, visit); } } }