( rootNode: unknown, command: string, )
| 294 | * Replaces isUnsafeCompoundCommand() and splitCommand() for tree-sitter path. |
| 295 | */ |
| 296 | export function extractCompoundStructure( |
| 297 | rootNode: unknown, |
| 298 | command: string, |
| 299 | ): CompoundStructure { |
| 300 | const n = rootNode as TreeSitterNode |
| 301 | const operators: string[] = [] |
| 302 | const segments: string[] = [] |
| 303 | let hasSubshell = false |
| 304 | let hasCommandGroup = false |
| 305 | let hasPipeline = false |
| 306 | |
| 307 | // Walk top-level children of the program node |
| 308 | function walkTopLevel(node: TreeSitterNode): void { |
| 309 | for (const child of node.children) { |
| 310 | if (!child) continue |
| 311 | |
| 312 | if (child.type === 'list') { |
| 313 | // list nodes contain && and || operators |
| 314 | for (const listChild of child.children) { |
| 315 | if (!listChild) continue |
| 316 | if (listChild.type === '&&' || listChild.type === '||') { |
| 317 | operators.push(listChild.type) |
| 318 | } else if ( |
| 319 | listChild.type === 'list' || |
| 320 | listChild.type === 'redirected_statement' |
| 321 | ) { |
| 322 | // Nested list, or redirected_statement wrapping a list/pipeline — |
| 323 | // recurse so inner operators/pipelines are detected. For |
| 324 | // `cmd1 && cmd2 2>/dev/null && cmd3`, the redirected_statement |
| 325 | // wraps `list(cmd1 && cmd2)` — the inner `&&` would be missed |
| 326 | // without recursion. |
| 327 | walkTopLevel({ ...node, children: [listChild] } as TreeSitterNode) |
| 328 | } else if (listChild.type === 'pipeline') { |
| 329 | hasPipeline = true |
| 330 | segments.push(listChild.text) |
| 331 | } else if (listChild.type === 'subshell') { |
| 332 | hasSubshell = true |
| 333 | segments.push(listChild.text) |
| 334 | } else if (listChild.type === 'compound_statement') { |
| 335 | hasCommandGroup = true |
| 336 | segments.push(listChild.text) |
| 337 | } else { |
| 338 | segments.push(listChild.text) |
| 339 | } |
| 340 | } |
| 341 | } else if (child.type === ';') { |
| 342 | operators.push(';') |
| 343 | } else if (child.type === 'pipeline') { |
| 344 | hasPipeline = true |
| 345 | segments.push(child.text) |
| 346 | } else if (child.type === 'subshell') { |
| 347 | hasSubshell = true |
| 348 | segments.push(child.text) |
| 349 | } else if (child.type === 'compound_statement') { |
| 350 | hasCommandGroup = true |
| 351 | segments.push(child.text) |
| 352 | } else if ( |
| 353 | child.type === 'command' || |
no test coverage detected