Download packages/tree-sitter-bash/test/parser-compound.test.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 59.8 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/tree-sitter-bash/test/parser-compound.test.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/tree-sitter-bash/test/parser-compound.test.ts
-
curl -L -o parser-compound.test.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/tree-sitter-bash/test/parser-compound.test.ts
59.8 kB
| import { describe, expect, it } from 'vitest'; | |
| import type { SyntaxNode } from '#/node'; | |
| import { descendantsOfType } from '#/node'; | |
| import { parse } from '#/parse'; | |
| /** Compact S-expression of the tree: named leaves are (type "text"), | |
| * anonymous leaves are just their quoted type, inner nodes nest. */ | |
| function sexp(node: SyntaxNode): string { | |
| if (node.children.length === 0) { | |
| return node.isNamed ? `(${node.type} ${JSON.stringify(node.text)})` : JSON.stringify(node.type); | |
| } | |
| const label = node.isNamed ? node.type : JSON.stringify(node.type); | |
| return `(${label} ${node.children.map(sexp).join(' ')})`; | |
| } | |
| function parseOk(source: string): { rootNode: SyntaxNode; hasError: boolean } { | |
| const result = parse(source); | |
| expect(result.ok).toBe(true); | |
| if (!result.ok) throw new Error('unreachable'); | |
| return result; | |
| } | |
| /** Parse and assert the exact tree shape plus the hasError flag. */ | |
| function expectTree(source: string, expected: string, hasError = false): SyntaxNode { | |
| const { rootNode, hasError: actual } = parseOk(source); | |
| expect(actual).toBe(hasError); | |
| expect(sexp(rootNode)).toBe(expected); | |
| return rootNode; | |
| } | |
| describe('if / while / until', () => { | |
| it('parses a one-line if statement', () => { | |
| expectTree( | |
| 'if true; then echo yes; fi', | |
| `(program (if_statement "if" (command (command_name (word "true"))) ";" "then" (command (command_name (word "echo")) (word "yes")) ";" "fi"))`, | |
| ); | |
| }); | |
| it('parses elif and else clauses', () => { | |
| expectTree( | |
| 'if a; then b; elif c; then d; else e; fi', | |
| `(program (if_statement "if" (command (command_name (word "a"))) ";" "then" (command (command_name (word "b"))) ";" (elif_clause "elif" (command (command_name (word "c"))) ";" "then" (command (command_name (word "d"))) ";") (else_clause "else" (command (command_name (word "e"))) ";") "fi"))`, | |
| ); | |
| }); | |
| it('parses a multiline if without emitting newline nodes', () => { | |
| expectTree( | |
| 'if a\nthen\n b\nfi', | |
| `(program (if_statement "if" (command (command_name (word "a"))) "then" (command (command_name (word "b"))) "fi"))`, | |
| ); | |
| }); | |
| it('parses a while loop with a trailing redirect', () => { | |
| expectTree( | |
| 'while read -r line; do echo "$line"; done < file', | |
| `(program (redirected_statement (while_statement "while" (command (command_name (word "read")) (word "-r") (word "line")) ";" (do_group "do" (command (command_name (word "echo")) (string "\\"" (simple_expansion "$" (variable_name "line")) "\\"")) ";" "done")) (file_redirect "<" (word "file"))))`, | |
| ); | |
| }); | |
| it('parses until as a while_statement', () => { | |
| expectTree( | |
| 'until x; do y; done', | |
| `(program (while_statement "until" (command (command_name (word "x"))) ";" (do_group "do" (command (command_name (word "y"))) ";" "done")))`, | |
| ); | |
| }); | |
| it('parses an empty do_group', () => { | |
| expectTree( | |
| 'while x; do done', | |
| `(program (while_statement "while" (command (command_name (word "x"))) ";" (do_group "do" "done")))`, | |
| ); | |
| }); | |
| it('only treats keywords as keywords in statement position', () => { | |
| expectTree( | |
| 'echo if for while', | |
| `(program (command (command_name (word "echo")) (word "if") (word "for") (word "while")))`, | |
| ); | |
| expectTree('fi', `(program (command (command_name (word "fi"))))`); | |
| expectTree('done', `(program (command (command_name (word "done"))))`); | |
| expectTree('}', `(program (command (command_name (word "}"))))`); | |
| expectTree('time ls -la', `(program (command (command_name (word "time")) (word "ls") (word "-la")))`); | |
| // Reserved words are plain arguments inside conditions and bodies. | |
| expectTree( | |
| 'if echo then; then x; fi', | |
| `(program (if_statement "if" (command (command_name (word "echo")) (word "then")) ";" "then" (command (command_name (word "x"))) ";" "fi"))`, | |
| ); | |
| // A prefix assignment disables the keyword reading. | |
| expectTree( | |
| 'x=1 if a; then b; fi', | |
| `(program (command (variable_assignment (variable_name "x") "=" (number "1")) (command_name (word "if")) (word "a")) ";" (command (command_name (word "then")) (word "b")) ";" (command (command_name (word "fi"))))`, | |
| ); | |
| }); | |
| }); | |
| describe('for / select', () => { | |
| it('parses a for-in loop', () => { | |
| expectTree( | |
| 'for f in a b c; do echo "$f"; done', | |
| `(program (for_statement "for" (variable_name "f") "in" (word "a") (word "b") (word "c") ";" (do_group "do" (command (command_name (word "echo")) (string "\\"" (simple_expansion "$" (variable_name "f")) "\\"")) ";" "done")))`, | |
| ); | |
| }); | |
| it('parses a for loop without in', () => { | |
| expectTree( | |
| 'for f; do x; done', | |
| `(program (for_statement "for" (variable_name "f") ";" (do_group "do" (command (command_name (word "x"))) ";" "done")))`, | |
| ); | |
| }); | |
| it('parses select as a for_statement', () => { | |
| expectTree( | |
| 'select opt in a b; do echo $opt; done', | |
| `(program (for_statement "select" (variable_name "opt") "in" (word "a") (word "b") ";" (do_group "do" (command (command_name (word "echo")) (simple_expansion "$" (variable_name "opt"))) ";" "done")))`, | |
| ); | |
| }); | |
| it('parses newline-separated for headers', () => { | |
| expectTree( | |
| 'for f in a b\ndo x\ndone', | |
| `(program (for_statement "for" (variable_name "f") "in" (word "a") (word "b") (do_group "do" (command (command_name (word "x"))) "done")))`, | |
| ); | |
| }); | |
| it('flags a bare `in` with no values as an error', () => { | |
| expectTree( | |
| 'select x in; do y; done', | |
| `(program (for_statement "select" (variable_name "x") (ERROR "in") ";" (do_group "do" (command (command_name (word "y"))) ";" "done")))`, | |
| true, | |
| ); | |
| }); | |
| it('parses a c-style for loop', () => { | |
| expectTree( | |
| 'for ((i=0;i<3;i++)); do echo $i; done', | |
| `(program (c_style_for_statement "for" "((" (variable_assignment (variable_name "i") "=" (number "0")) ";" (binary_expression (word "i") "<" (number "3")) ";" (postfix_expression (word "i") "++") "))" ";" (do_group "do" (command (command_name (word "echo")) (simple_expansion "$" (variable_name "i"))) ";" "done")))`, | |
| ); | |
| }); | |
| it('parses a spaced c-style for with an update assignment operator', () => { | |
| expectTree( | |
| 'for (( i = 0; i < 10; i += 2 )); do x; done', | |
| `(program (c_style_for_statement "for" "((" (variable_assignment (variable_name "i") "=" (number "0")) ";" (binary_expression (word "i") "<" (number "10")) ";" (binary_expression (word "i") "+=" (number "2")) "))" ";" (do_group "do" (command (command_name (word "x"))) ";" "done")))`, | |
| ); | |
| }); | |
| it('parses an empty c-style for header', () => { | |
| expectTree( | |
| 'for ((;;)); do x; done', | |
| `(program (c_style_for_statement "for" "((" ";" ";" "))" ";" (do_group "do" (command (command_name (word "x"))) ";" "done")))`, | |
| ); | |
| }); | |
| it('parses comma-separated c-style for parts', () => { | |
| expectTree( | |
| 'for ((i=0,j=1; i<3; i++,j--)); do x; done', | |
| `(program (c_style_for_statement "for" "((" (variable_assignment (variable_name "i") "=" (number "0")) "," (variable_assignment (variable_name "j") "=" (number "1")) ";" (binary_expression (word "i") "<" (number "3")) ";" (postfix_expression (word "i") "++") "," (postfix_expression (word "j") "--") "))" ";" (do_group "do" (command (command_name (word "x"))) ";" "done")))`, | |
| ); | |
| }); | |
| it('parses a compound_statement as the c-style for body', () => { | |
| expectTree( | |
| 'for ((i=0;i<3;i++)) { echo $i; }', | |
| `(program (c_style_for_statement "for" "((" (variable_assignment (variable_name "i") "=" (number "0")) ";" (binary_expression (word "i") "<" (number "3")) ";" (postfix_expression (word "i") "++") "))" (compound_statement "{" (command (command_name (word "echo")) (simple_expansion "$" (variable_name "i"))) ";" "}")))`, | |
| ); | |
| }); | |
| }); | |
| describe('case', () => { | |
| it('parses a multi-item case with all termination forms', () => { | |
| expectTree( | |
| 'case $x in\n a) echo A ;;\n b|c) echo BC ;&\n *) echo other ;;\nesac', | |
| `(program (case_statement "case" (simple_expansion "$" (variable_name "x")) "in" (case_item (word "a") ")" (command (command_name (word "echo")) (word "A")) ";;") (case_item (extglob_pattern "b") "|" (word "c") ")" (command (command_name (word "echo")) (word "BC")) ";&") (case_item (extglob_pattern "*") ")" (command (command_name (word "echo")) (word "other")) ";;") "esac"))`, | |
| ); | |
| }); | |
| it('parses optional parens and empty item bodies', () => { | |
| expectTree( | |
| 'case $x in (a) x ;; esac', | |
| `(program (case_statement "case" (simple_expansion "$" (variable_name "x")) "in" (case_item "(" (word "a") ")" (command (command_name (word "x"))) ";;") "esac"))`, | |
| ); | |
| expectTree( | |
| 'case x in a) ;; esac', | |
| `(program (case_statement "case" (word "x") "in" (case_item (word "a") ")" ";;") "esac"))`, | |
| ); | |
| expectTree( | |
| 'case $x in esac', | |
| `(program (case_statement "case" (simple_expansion "$" (variable_name "x")) "in" "esac"))`, | |
| ); | |
| }); | |
| it('parses pattern forms: globs, quotes and expansions', () => { | |
| expectTree( | |
| 'case $x in [a-z]) x ;; esac', | |
| `(program (case_statement "case" (simple_expansion "$" (variable_name "x")) "in" (case_item (extglob_pattern "[a-z]") ")" (command (command_name (word "x"))) ";;") "esac"))`, | |
| ); | |
| expectTree( | |
| 'case $x in "a") x ;; esac', | |
| `(program (case_statement "case" (simple_expansion "$" (variable_name "x")) "in" (case_item (string "\\"" (string_content "a") "\\"") ")" (command (command_name (word "x"))) ";;") "esac"))`, | |
| ); | |
| expectTree( | |
| 'case $x in $v|${w}) x ;; esac', | |
| `(program (case_statement "case" (simple_expansion "$" (variable_name "x")) "in" (case_item (simple_expansion "$" (variable_name "v")) "|" (expansion "\${" (variable_name "w") "}") ")" (command (command_name (word "x"))) ";;") "esac"))`, | |
| ); | |
| }); | |
| it('ends the last case_item at esac even without ;;', () => { | |
| expectTree( | |
| 'case $x in a) x esac', | |
| `(program (case_statement "case" (simple_expansion "$" (variable_name "x")) "in" (case_item (word "a") ")" (command (command_name (word "x")))) "esac"))`, | |
| ); | |
| }); | |
| it('flags a fallthrough terminator on the last case_item', () => { | |
| const { hasError } = parseOk('case $x in a) x ;;& esac'); | |
| expect(hasError).toBe(true); | |
| }); | |
| }); | |
| describe('functions and compound statements', () => { | |
| it('parses the name() form', () => { | |
| expectTree( | |
| 'foo() { echo hi; }', | |
| `(program (function_definition (word "foo") "(" ")" (compound_statement "{" (command (command_name (word "echo")) (word "hi")) ";" "}")))`, | |
| ); | |
| }); | |
| it('parses the function keyword forms', () => { | |
| expectTree( | |
| 'function bar { echo hi; }', | |
| `(program (function_definition "function" (word "bar") (compound_statement "{" (command (command_name (word "echo")) (word "hi")) ";" "}")))`, | |
| ); | |
| expectTree( | |
| 'function baz() { echo hi; return 0; }', | |
| `(program (function_definition "function" (word "baz") "(" ")" (compound_statement "{" (command (command_name (word "echo")) (word "hi")) ";" (command (command_name (word "return")) (number "0")) ";" "}")))`, | |
| ); | |
| expectTree( | |
| 'f () { x; }', | |
| `(program (function_definition (word "f") "(" ")" (compound_statement "{" (command (command_name (word "x"))) ";" "}")))`, | |
| ); | |
| }); | |
| it('parses subshell, test and if bodies', () => { | |
| expectTree( | |
| 'foo() ( echo sub )', | |
| `(program (function_definition (word "foo") "(" ")" (subshell "(" (command (command_name (word "echo")) (word "sub")) ")")))`, | |
| ); | |
| expectTree( | |
| 'foo() [[ -f x ]]', | |
| `(program (function_definition (word "foo") "(" ")" (test_command "[[" (unary_expression (test_operator "-f") (word "x")) "]]")))`, | |
| ); | |
| expectTree( | |
| 'foo() if a; then b; fi', | |
| `(program (function_definition (word "foo") "(" ")" (if_statement "if" (command (command_name (word "a"))) ";" "then" (command (command_name (word "b"))) ";" "fi")))`, | |
| ); | |
| }); | |
| it('keeps a trailing redirect inside the function_definition', () => { | |
| expectTree( | |
| 'foo() { x; } > out', | |
| `(program (function_definition (word "foo") "(" ")" (compound_statement "{" (command (command_name (word "x"))) ";" "}") (file_redirect ">" (word "out"))))`, | |
| ); | |
| }); | |
| it('parses a standalone compound statement', () => { | |
| expectTree( | |
| '{ echo a; echo b; }', | |
| `(program (compound_statement "{" (command (command_name (word "echo")) (word "a")) ";" (command (command_name (word "echo")) (word "b")) ";" "}"))`, | |
| ); | |
| expectTree('{ls;}', `(program (compound_statement "{" (command (command_name (word "ls"))) ";" "}"))`); | |
| }); | |
| it('parses coproc as an ordinary command (like the reference)', () => { | |
| expectTree( | |
| 'coproc myjob { echo hi; }', | |
| `(program (command (command_name (word "coproc")) (word "myjob") (word "{") (word "echo") (word "hi")) ";" (command (command_name (word "}"))))`, | |
| ); | |
| }); | |
| }); | |
| describe('test commands', () => { | |
| it('parses unary test operators', () => { | |
| expectTree( | |
| '[[ -f file.txt ]]', | |
| `(program (test_command "[[" (unary_expression (test_operator "-f") (word "file.txt")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ -z $s ]]', | |
| `(program (test_command "[[" (unary_expression (test_operator "-z") (simple_expansion "$" (variable_name "s"))) "]]"))`, | |
| ); | |
| }); | |
| it('parses string comparisons', () => { | |
| expectTree( | |
| '[[ $x == "foo" ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "x")) "==" (string "\\"" (string_content "foo") "\\"")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[ "$a" = "b" ]', | |
| `(program (test_command "[" (binary_expression (string "\\"" (simple_expansion "$" (variable_name "a")) "\\"") "=" (string "\\"" (string_content "b") "\\"")) "]"))`, | |
| ); | |
| }); | |
| it('parses =~ with a regex right side', () => { | |
| expectTree( | |
| '[[ $x =~ ^ab+c$ ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "x")) "=~" (regex "^ab+c$")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $x =~ a|b ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "x")) "=~" (regex "a|b")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $x =~ $re ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "x")) "=~" (simple_expansion "$" (variable_name "re"))) "]]"))`, | |
| ); | |
| }); | |
| it('parses pattern right sides: regex after =, extglob_pattern after ==/!=', () => { | |
| expectTree( | |
| '[[ a = b*c ]]', | |
| `(program (test_command "[[" (binary_expression (word "a") "=" (regex "b*c")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ a = bc ]]', | |
| `(program (test_command "[[" (binary_expression (word "a") "=" (word "bc")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $x == b*c ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "x")) "==" (extglob_pattern "b*c")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $x == 123 ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "x")) "==" (number "123")) "]]"))`, | |
| ); | |
| }); | |
| it('parses && / || / ! combinations', () => { | |
| expectTree( | |
| '[[ -n $s && -d /tmp ]]', | |
| `(program (test_command "[[" (binary_expression (unary_expression (test_operator "-n") (simple_expansion "$" (variable_name "s"))) "&&" (unary_expression (test_operator "-d") (word "/tmp"))) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ a < b || ! -e f ]]', | |
| `(program (test_command "[[" (binary_expression (binary_expression (word "a") "<" (word "b")) "||" (unary_expression "!" (unary_expression (test_operator "-e") (word "f")))) "]]"))`, | |
| ); | |
| }); | |
| it('parses binary test operators and parentheses', () => { | |
| expectTree( | |
| '[[ $a -eq 3 ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) (test_operator "-eq") (number "3")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $x -eq 1 && $y -ne 2 ]]', | |
| `(program (test_command "[[" (binary_expression (binary_expression (simple_expansion "$" (variable_name "x")) (test_operator "-eq") (number "1")) "&&" (binary_expression (simple_expansion "$" (variable_name "y")) (test_operator "-ne") (number "2"))) "]]"))`, | |
| ); | |
| // Inside parentheses the == right side is a word, not extglob_pattern. | |
| expectTree( | |
| '[[ (a == b) && c ]]', | |
| `(program (test_command "[[" (binary_expression (parenthesized_expression "(" (binary_expression (word "a") "==" (word "b")) ")") "&&" (word "c")) "]]"))`, | |
| ); | |
| }); | |
| it('parses the single-bracket form and adjacent =', () => { | |
| expectTree('[ -f file ]', `(program (test_command "[" (unary_expression (test_operator "-f") (word "file")) "]"))`); | |
| expectTree('[ x ]', `(program (test_command "[" (word "x") "]"))`); | |
| expectTree('[[ a=b ]]', `(program (test_command "[[" (word "a=b") "]]"))`); | |
| }); | |
| it('parses negated test commands', () => { | |
| expectTree( | |
| '! [[ -f x ]]', | |
| `(program (negated_command "!" (test_command "[[" (unary_expression (test_operator "-f") (word "x")) "]]")))`, | |
| ); | |
| expectTree( | |
| '! [ -f x ]', | |
| `(program (negated_command "!" (test_command "[" (unary_expression (test_operator "-f") (word "x")) "]")))`, | |
| ); | |
| }); | |
| it('keeps [ in argument position as plain words', () => { | |
| expectTree('echo [ x ]', `(program (command (command_name (word "echo")) (word "[") (word "x") (word "]")))`); | |
| }); | |
| it('recovers an unterminated test command with a zero-width closer', () => { | |
| expectTree('[ -f file', `(program (test_command "[" (unary_expression (test_operator "-f") (word "file")) "]"))`, true); | |
| }); | |
| }); | |
| describe('arithmetic', () => { | |
| it('parses operator precedence levels', () => { | |
| expectTree( | |
| 'echo $((1 + 2 * 3))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (binary_expression (number "1") "+" (binary_expression (number "2") "*" (number "3"))) "))")))`, | |
| ); | |
| expectTree( | |
| 'echo $((x << 2 | 1))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (binary_expression (binary_expression (variable_name "x") "<<" (number "2")) "|" (number "1")) "))")))`, | |
| ); | |
| expectTree( | |
| 'echo $((n % 2 == 0))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (binary_expression (binary_expression (variable_name "n") "%" (number "2")) "==" (number "0")) "))")))`, | |
| ); | |
| }); | |
| it('parses ternary, postfix, prefix and unary expressions', () => { | |
| expectTree( | |
| 'echo $((a > b ? a : b))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (ternary_expression (binary_expression (variable_name "a") ">" (variable_name "b")) "?" (variable_name "a") ":" (variable_name "b")) "))")))`, | |
| ); | |
| expectTree( | |
| 'echo $((i++ + --j))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (binary_expression (postfix_expression (variable_name "i") "++") "+" (unary_expression "--" (variable_name "j"))) "))")))`, | |
| ); | |
| // Prefix operators grab the whole tighter-precedence expression, like | |
| // the reference: -(x + ~y). | |
| expectTree( | |
| 'echo $((-x + ~y))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (unary_expression "-" (binary_expression (variable_name "x") "+" (unary_expression "~" (variable_name "y")))) "))")))`, | |
| ); | |
| }); | |
| it('parses parentheses and exponent (left-associative in arithmetic)', () => { | |
| expectTree( | |
| 'echo $(( (1+2) ** 3 ))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (binary_expression (parenthesized_expression "(" (binary_expression (number "1") "+" (number "2")) ")") "**" (number "3")) "))")))`, | |
| ); | |
| expectTree( | |
| 'echo $((2 ** 3 ** 2))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (binary_expression (binary_expression (number "2") "**" (number "3")) "**" (number "2")) "))")))`, | |
| ); | |
| }); | |
| it('parses assignment operators, subscripts and comma lists', () => { | |
| expectTree( | |
| 'echo $((x += 5))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (binary_expression (variable_name "x") "+=" (number "5")) "))")))`, | |
| ); | |
| expectTree( | |
| 'echo $((x = y = 1))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (binary_expression (binary_expression (variable_name "x") "=" (variable_name "y")) "=" (number "1")) "))")))`, | |
| ); | |
| expectTree( | |
| 'echo $((arr[0] + 1))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (binary_expression (subscript (variable_name "arr") "[" (number "0") "]") "+" (number "1")) "))")))`, | |
| ); | |
| expectTree( | |
| 'echo $((1, 2, 3))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (number "1") "," (number "2") "," (number "3") "))")))`, | |
| ); | |
| }); | |
| it('parses the ((…)) command and legacy $[…] form', () => { | |
| expectTree( | |
| '((x = y + 1))', | |
| `(program (command (command_name (arithmetic_expansion "((" (binary_expression (variable_name "x") "=" (binary_expression (variable_name "y") "+" (number "1"))) "))"))))`, | |
| ); | |
| expectTree( | |
| '(( ls ))', | |
| `(program (command (command_name (arithmetic_expansion "((" (variable_name "ls") "))"))))`, | |
| ); | |
| expectTree( | |
| 'echo $[1+2]', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$[" (binary_expression (number "1") "+" (number "2")) "]")))`, | |
| ); | |
| }); | |
| it('parses arithmetic across newlines and nested arithmetic', () => { | |
| expectTree( | |
| 'echo $((\n1 +\n2\n))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (binary_expression (number "1") "+" (number "2")) "))")))`, | |
| ); | |
| expectTree( | |
| 'for ((i = $((1 + 1)); i < 3; i++)); do x; done', | |
| `(program (c_style_for_statement "for" "((" (variable_assignment (variable_name "i") "=" (arithmetic_expansion "$((" (binary_expression (number "1") "+" (number "1")) "))")) ";" (binary_expression (word "i") "<" (number "3")) ";" (postfix_expression (word "i") "++") "))" ";" (do_group "do" (command (command_name (word "x"))) ";" "done")))`, | |
| ); | |
| }); | |
| }); | |
| describe('arrays and subscripts', () => { | |
| it('parses array assignments', () => { | |
| expectTree( | |
| 'arr=(a b "c d")', | |
| `(program (variable_assignment (variable_name "arr") "=" (array "(" (word "a") (word "b") (string "\\"" (string_content "c d") "\\"") ")")))`, | |
| ); | |
| expectTree( | |
| 'x+=(c d)', | |
| `(program (variable_assignment (variable_name "x") "+=" (array "(" (word "c") (word "d") ")")))`, | |
| ); | |
| }); | |
| it('parses subscript assignments', () => { | |
| expectTree( | |
| 'arr[0]=x', | |
| `(program (variable_assignment (subscript (variable_name "arr") "[" (number "0") "]") "=" (word "x")))`, | |
| ); | |
| expectTree( | |
| 'a[i+1]=$y', | |
| `(program (variable_assignment (subscript (variable_name "a") "[" (word "i+1") "]") "=" (simple_expansion "$" (variable_name "y"))))`, | |
| ); | |
| expectTree( | |
| 'x[1]+=2', | |
| `(program (variable_assignment (subscript (variable_name "x") "[" (number "1") "]") "+=" (number "2")))`, | |
| ); | |
| }); | |
| it('parses subscript expansions including @ and $ indexes', () => { | |
| expectTree( | |
| 'echo ${arr[@]}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (subscript (variable_name "arr") "[" (word "@") "]") "}")))`, | |
| ); | |
| expectTree( | |
| 'echo ${a[$i]}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (subscript (variable_name "a") "[" (simple_expansion "$" (variable_name "i")) "]") "}")))`, | |
| ); | |
| }); | |
| }); | |
| describe('declaration and unset commands', () => { | |
| it('parses export/declare/local/readonly', () => { | |
| expectTree( | |
| 'export FOO=bar BAZ', | |
| `(program (declaration_command "export" (variable_assignment (variable_name "FOO") "=" (word "bar")) (variable_name "BAZ")))`, | |
| ); | |
| expectTree( | |
| 'declare -r x=1', | |
| `(program (declaration_command "declare" (word "-r") (variable_assignment (variable_name "x") "=" (number "1"))))`, | |
| ); | |
| expectTree('local y', `(program (declaration_command "local" (variable_name "y")))`); | |
| expectTree( | |
| 'readonly z=2', | |
| `(program (declaration_command "readonly" (variable_assignment (variable_name "z") "=" (number "2"))))`, | |
| ); | |
| }); | |
| it('parses unset', () => { | |
| expectTree('unset a b', `(program (unset_command "unset" (variable_name "a") (variable_name "b")))`); | |
| }); | |
| }); | |
| describe('strings and brace expressions', () => { | |
| it('parses ANSI-C strings as a single node', () => { | |
| expectTree( | |
| `echo $'a\\nb\\t'`, | |
| `(program (command (command_name (word "echo")) (ansi_c_string "$'a\\\\nb\\\\t'")))`, | |
| ); | |
| }); | |
| it('parses $"…" as an anonymous $ plus a string (no translated_string)', () => { | |
| expectTree( | |
| 'echo $"hello $USER"', | |
| `(program (command (command_name (word "echo")) "$" (string "\\"" (string_content "hello ") (simple_expansion "$" (variable_name "USER")) "\\"")))`, | |
| ); | |
| }); | |
| it('parses $"…" as translated_string outside argument position', () => { | |
| expectTree( | |
| 'x=$"v"', | |
| `(program (variable_assignment (variable_name "x") "=" (translated_string "$" (string "\\"" (string_content "v") "\\""))))`, | |
| ); | |
| expectTree( | |
| 'cat > $"out"', | |
| `(program (redirected_statement (command (command_name (word "cat"))) (file_redirect ">" (translated_string "$" (string "\\"" (string_content "out") "\\"")))))`, | |
| ); | |
| expectTree( | |
| 'for f in $"a"; do x; done', | |
| `(program (for_statement "for" (variable_name "f") "in" (translated_string "$" (string "\\"" (string_content "a") "\\"")) ";" (do_group "do" (command (command_name (word "x"))) ";" "done")))`, | |
| ); | |
| // Mid-concatenation the bare $ and the string stay separate pieces. | |
| expectTree( | |
| 'echo a$"b"', | |
| `(program (command (command_name (word "echo")) (concatenation (word "a") "$" (string "\\"" (string_content "b") "\\""))))`, | |
| ); | |
| }); | |
| it('parses {N..M} brace expressions', () => { | |
| expectTree( | |
| 'echo {1..10}', | |
| `(program (command (command_name (word "echo")) (brace_expression "{" (number "1") ".." (number "10") "}")))`, | |
| ); | |
| expectTree( | |
| 'echo a{1..3}b', | |
| `(program (command (command_name (word "echo")) (concatenation (word "a") (brace_expression "{" (number "1") ".." (number "3") "}") (word "b"))))`, | |
| ); | |
| expectTree( | |
| 'x={1..5}', | |
| `(program (variable_assignment (variable_name "x") "=" (brace_expression "{" (number "1") ".." (number "5") "}")))`, | |
| ); | |
| expectTree( | |
| '{1..3}', | |
| `(program (command (command_name (brace_expression "{" (number "1") ".." (number "3") "}"))))`, | |
| ); | |
| }); | |
| it('parses other brace forms as plain concatenations (like the reference)', () => { | |
| expectTree( | |
| 'echo {1..10..2}', | |
| `(program (command (command_name (word "echo")) (concatenation (word "{") (word "1..10..2") (word "}"))))`, | |
| ); | |
| expectTree( | |
| 'echo {a..z}', | |
| `(program (command (command_name (word "echo")) (concatenation (word "{") (word "a..z") (word "}"))))`, | |
| ); | |
| expectTree( | |
| 'echo {-5..5}', | |
| `(program (command (command_name (word "echo")) (concatenation (word "{") (word "-5..5") (word "}"))))`, | |
| ); | |
| expectTree( | |
| 'echo a{b,c}d', | |
| `(program (command (command_name (word "echo")) (concatenation (word "a") (word "{") (word "b,c") (word "}") (word "d"))))`, | |
| ); | |
| }); | |
| }); | |
| describe('expansion operators', () => { | |
| it('parses removal patterns as regex nodes', () => { | |
| expectTree( | |
| 'echo ${x##*/} ${x%%.*}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "x") "##" (regex "*/") "}") (expansion "\${" (variable_name "x") "%%" (regex ".*") "}")))`, | |
| ); | |
| expectTree( | |
| 'echo ${x##a b}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "x") "##" (regex "a b") "}")))`, | |
| ); | |
| expectTree( | |
| 'echo ${x#"pat"}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "x") "#" (string "\\"" (string_content "pat") "\\"") "}")))`, | |
| ); | |
| }); | |
| it('parses replacements with regex pattern and literal replacement', () => { | |
| expectTree( | |
| 'echo ${x/y/z} ${x//a/b}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "x") "/" (regex "y") "/" (word "z") "}") (expansion "\${" (variable_name "x") "//" (regex "a") "/" (word "b") "}")))`, | |
| ); | |
| expectTree( | |
| 'echo ${x/a b/c}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "x") "/" (regex "a b") "/" (word "c") "}")))`, | |
| ); | |
| expectTree( | |
| 'echo ${x/a/$v}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "x") "/" (regex "a") "/" (simple_expansion "$" (variable_name "v")) "}")))`, | |
| ); | |
| expectTree( | |
| 'echo ${x/pat/rep/extra}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "x") "/" (regex "pat") "/" (word "rep/extra") "}")))`, | |
| ); | |
| }); | |
| it('parses case modification and transformation operators', () => { | |
| expectTree( | |
| 'echo ${x^} ${x,,}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "x") "^" "}") (expansion "\${" (variable_name "x") ",," "}")))`, | |
| ); | |
| expectTree( | |
| 'echo ${x@Q}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "x") "@" "Q" "}")))`, | |
| ); | |
| }); | |
| it('parses max-length expansions with arithmetic values', () => { | |
| expectTree( | |
| 'echo ${v:1:2}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "v") ":" (number "1") ":" (number "2") "}")))`, | |
| ); | |
| expectTree( | |
| 'echo ${arr[@]:1:2}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (subscript (variable_name "arr") "[" (word "@") "]") ":" (number "1") ":" (number "2") "}")))`, | |
| ); | |
| }); | |
| it('parses default values as words, splitting bare values with spaces', () => { | |
| expectTree( | |
| 'echo ${v:-1} ${v:=2x}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "v") ":-" (word "1") "}") (expansion "\${" (variable_name "v") ":=" (word "2x") "}")))`, | |
| ); | |
| expectTree( | |
| 'echo ${x:-d e f}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "x") ":-" (concatenation (word "d") (word " e f")) "}")))`, | |
| ); | |
| expectTree( | |
| 'echo ${v:-"d e"}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "v") ":-" (string "\\"" (string_content "d e") "\\"") "}")))`, | |
| ); | |
| }); | |
| it('parses indirect expansions with trailing * and @', () => { | |
| expectTree( | |
| 'echo ${!prefix*} ${!name@}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" "!" (variable_name "prefix") "*" "}") (expansion "\${" "!" (variable_name "name") "@" "}")))`, | |
| ); | |
| }); | |
| }); | |
| describe('combinations', () => { | |
| it('nests if inside for inside case', () => { | |
| expectTree( | |
| 'case $1 in start) for s in a b; do if [[ -n $s ]]; then echo "$s"; fi; done ;; *) echo usage ;; esac', | |
| `(program (case_statement "case" (simple_expansion "$" (variable_name "1")) "in" (case_item (word "start") ")" (for_statement "for" (variable_name "s") "in" (word "a") (word "b") ";" (do_group "do" (if_statement "if" (test_command "[[" (unary_expression (test_operator "-n") (simple_expansion "$" (variable_name "s"))) "]]") ";" "then" (command (command_name (word "echo")) (string "\\"" (simple_expansion "$" (variable_name "s")) "\\"")) ";" "fi") ";" "done")) ";;") (case_item (extglob_pattern "*") ")" (command (command_name (word "echo")) (word "usage")) ";;") "esac"))`, | |
| ); | |
| }); | |
| it('parses a heredoc inside a function body', () => { | |
| expectTree( | |
| 'foo() { cat <<EOF\nbody $x\nEOF\n}', | |
| `(program (function_definition (word "foo") "(" ")" (compound_statement "{" (redirected_statement (command (command_name (word "cat"))) (heredoc_redirect "<<" (heredoc_start "EOF") (heredoc_body (heredoc_content "body ") (simple_expansion "$" (variable_name "x")) (heredoc_content "\\n")) (heredoc_end "EOF"))) "}")))`, | |
| ); | |
| }); | |
| it('parses compound commands in pipelines and lists', () => { | |
| expectTree( | |
| 'if a; then b; fi | grep x', | |
| `(program (pipeline (if_statement "if" (command (command_name (word "a"))) ";" "then" (command (command_name (word "b"))) ";" "fi") "|" (command (command_name (word "grep")) (word "x"))))`, | |
| ); | |
| expectTree( | |
| 'if a; then b; fi && c', | |
| `(program (list (if_statement "if" (command (command_name (word "a"))) ";" "then" (command (command_name (word "b"))) ";" "fi") "&&" (command (command_name (word "c")))))`, | |
| ); | |
| expectTree( | |
| 'echo hi | if a; then b; fi', | |
| `(program (pipeline (command (command_name (word "echo")) (word "hi")) "|" (if_statement "if" (command (command_name (word "a"))) ";" "then" (command (command_name (word "b"))) ";" "fi")))`, | |
| ); | |
| }); | |
| it('parses redirects on compound statements', () => { | |
| expectTree( | |
| 'if a; then b; fi > out', | |
| `(program (redirected_statement (if_statement "if" (command (command_name (word "a"))) ";" "then" (command (command_name (word "b"))) ";" "fi") (file_redirect ">" (word "out"))))`, | |
| ); | |
| }); | |
| it('parses test commands as loop conditions', () => { | |
| expectTree( | |
| 'while [[ -n $s ]]; do s=; done', | |
| `(program (while_statement "while" (test_command "[[" (unary_expression (test_operator "-n") (simple_expansion "$" (variable_name "s"))) "]]") ";" (do_group "do" (variable_assignment (variable_name "s") "=") ";" "done")))`, | |
| ); | |
| expectTree( | |
| 'if [ -f f ]; then x; fi', | |
| `(program (if_statement "if" (test_command "[" (unary_expression (test_operator "-f") (word "f")) "]") ";" "then" (command (command_name (word "x"))) ";" "fi"))`, | |
| ); | |
| }); | |
| it('parses negation, lists and nested ifs in conditions', () => { | |
| expectTree( | |
| 'if ! grep -q x f; then echo missing; fi', | |
| `(program (if_statement "if" (negated_command "!" (command (command_name (word "grep")) (word "-q") (word "x") (word "f"))) ";" "then" (command (command_name (word "echo")) (word "missing")) ";" "fi"))`, | |
| ); | |
| expectTree( | |
| 'if a && b; then c; fi', | |
| `(program (if_statement "if" (list (command (command_name (word "a"))) "&&" (command (command_name (word "b")))) ";" "then" (command (command_name (word "c"))) ";" "fi"))`, | |
| ); | |
| expectTree( | |
| 'if if a; then b; fi; then c; fi', | |
| `(program (if_statement "if" (if_statement "if" (command (command_name (word "a"))) ";" "then" (command (command_name (word "b"))) ";" "fi") ";" "then" (command (command_name (word "c"))) ";" "fi"))`, | |
| ); | |
| }); | |
| it('parses arrays and subscript expansions inside functions', () => { | |
| expectTree( | |
| 'f() { arr=(1 2); echo ${arr[0]}; }', | |
| `(program (function_definition (word "f") "(" ")" (compound_statement "{" (variable_assignment (variable_name "arr") "=" (array "(" (number "1") (number "2") ")")) ";" (command (command_name (word "echo")) (expansion "\${" (subscript (variable_name "arr") "[" (number "0") "]") "}")) ";" "}")))`, | |
| ); | |
| }); | |
| it('parses a command subshell argument and command substitutions in for values', () => { | |
| expectTree( | |
| 'foo (ls)', | |
| `(program (command (command_name (word "foo")) (subshell "(" (command (command_name (word "ls"))) ")")))`, | |
| ); | |
| expectTree( | |
| 'for i in $(seq 3); do echo $i; done', | |
| `(program (for_statement "for" (variable_name "i") "in" (command_substitution "$(" (command (command_name (word "seq")) (number "3")) ")") ";" (do_group "do" (command (command_name (word "echo")) (simple_expansion "$" (variable_name "i"))) ";" "done")))`, | |
| ); | |
| }); | |
| }); | |
| describe('heredocs in substitutions', () => { | |
| it('parses a quoted heredoc with a single quote in its body inside "$( )"', () => { | |
| expectTree( | |
| 'echo "$(cat <<\'EOF\'\nit\'s\nEOF\n)"', | |
| `(program (command (command_name (word "echo")) (string "\\"" (command_substitution "$(" (redirected_statement (command (command_name (word "cat"))) (heredoc_redirect "<<" (heredoc_start "'EOF'") (heredoc_body "it's\\n") (heredoc_end "EOF"))) ")") "\\"")))`, | |
| ); | |
| }); | |
| it('parses the gh pr create shape that triggered the unanalyzable verdict', () => { | |
| expectTree( | |
| 'gh --body "$(cat <<\'EOF\'\nit\'s\nEOF\n)"', | |
| `(program (command (command_name (word "gh")) (word "--body") (string "\\"" (command_substitution "$(" (redirected_statement (command (command_name (word "cat"))) (heredoc_redirect "<<" (heredoc_start "'EOF'") (heredoc_body "it's\\n") (heredoc_end "EOF"))) ")") "\\"")))`, | |
| ); | |
| }); | |
| it('parses a quoted heredoc with a single quote in its body inside bare $( )', () => { | |
| expectTree( | |
| 'echo $(cat <<\'EOF\'\nit\'s\nEOF\n)', | |
| `(program (command (command_name (word "echo")) (command_substitution "$(" (redirected_statement (command (command_name (word "cat"))) (heredoc_redirect "<<" (heredoc_start "'EOF'") (heredoc_body "it's\\n") (heredoc_end "EOF"))) ")")))`, | |
| ); | |
| }); | |
| it('parses a heredoc body containing a paren inside $( )', () => { | |
| expectTree( | |
| 'echo $(cat <<EOF\n)a\nEOF\n)', | |
| `(program (command (command_name (word "echo")) (command_substitution "$(" (redirected_statement (command (command_name (word "cat"))) (heredoc_redirect "<<" (heredoc_start "EOF") (heredoc_body (heredoc_content ")a\\n")) (heredoc_end "EOF"))) ")")))`, | |
| ); | |
| }); | |
| it('parses a heredoc with a double-quoted body line inside "$( )"', () => { | |
| expectTree( | |
| 'echo "$(cat <<\'EOF\'\nsay "hi"\nEOF\n)"', | |
| `(program (command (command_name (word "echo")) (string "\\"" (command_substitution "$(" (redirected_statement (command (command_name (word "cat"))) (heredoc_redirect "<<" (heredoc_start "'EOF'") (heredoc_body "say \\"hi\\"\\n") (heredoc_end "EOF"))) ")") "\\"")))`, | |
| ); | |
| }); | |
| it('parses a heredoc inside a process substitution', () => { | |
| expectTree( | |
| 'cat <(cat <<\'EOF\'\nit\'s\nEOF\n)', | |
| `(program (command (command_name (word "cat")) (process_substitution "<(" (redirected_statement (command (command_name (word "cat"))) (heredoc_redirect "<<" (heredoc_start "'EOF'") (heredoc_body "it's\\n") (heredoc_end "EOF"))) ")")))`, | |
| ); | |
| }); | |
| it('handles tricky << contexts inside substitutions (non-redirection contexts and substitution delimiters)', () => { | |
| expectTree( | |
| 'echo $(echo $((x << 2)))', | |
| `(program (command (command_name (word "echo")) (command_substitution "$(" (command (command_name (word "echo")) (arithmetic_expansion "$((" (binary_expression (variable_name "x") "<<" (number "2")) "))")) ")")))`, | |
| ); | |
| expectTree( | |
| 'echo $(echo $[x << 2]\n)', | |
| `(program (command (command_name (word "echo")) (command_substitution "$(" (command (command_name (word "echo")) (arithmetic_expansion "$[" (binary_expression (variable_name "x") "<<" (number "2")) "]")) ")")))`, | |
| ); | |
| expectTree( | |
| 'echo $(printf x # <<EOF\n)', | |
| `(program (command (command_name (word "echo")) (command_substitution "$(" (command (command_name (word "printf")) (word "x")) (comment "# <<EOF") ")")))`, | |
| ); | |
| expectTree( | |
| 'echo $(a[x<<2]=3\n)', | |
| `(program (command (command_name (word "echo")) (command_substitution "$(" (variable_assignment (subscript (variable_name "a") "[" (word "x<<2") "]") "=" (number "3")) ")")))`, | |
| ); | |
| expectTree( | |
| 'echo $( [[ x == @(<<EOF) ]]\n)', | |
| `(program (command (command_name (word "echo")) (command_substitution "$(" (test_command "[[" (binary_expression (word "x") "==" (extglob_pattern "@(<<EOF)")) "]]") ")")))`, | |
| ); | |
| expectTree( | |
| 'echo $(cat <<$(foo)\nbody\n$(foo)\n)', | |
| `(program (command (command_name (word "echo")) (command_substitution "$(" (redirected_statement (command (command_name (word "cat"))) (heredoc_redirect "<<" (heredoc_start "$(foo)") (heredoc_body (heredoc_content "body\\n")) (heredoc_end "$(foo)"))) ")")))`, | |
| ); | |
| expectTree( | |
| 'echo $(printf foo\\\n#bar)', | |
| `(program (command (command_name (word "echo")) (command_substitution "$(" (command (command_name (word "printf")) (word "foo")) (comment "#bar") ")")))`, | |
| ); | |
| }); | |
| }); | |
| describe('recovery and adversarial input', () => { | |
| it('recovers unterminated compound commands without throwing', () => { | |
| for (const source of [ | |
| 'if a; then b', | |
| 'if a', | |
| 'while x; do y', | |
| 'for f in a b; do x', | |
| 'case $x in a) x', | |
| 'case $x', | |
| 'foo() { x;', | |
| '{ x;', | |
| 'function f', | |
| 'for ((i=0;i<3', | |
| 'echo $((1 + ', | |
| '[[ -f x', | |
| 'arr=(1 2', | |
| ]) { | |
| const result = parse(source); | |
| expect(result.ok).toBe(true); | |
| if (result.ok) expect(result.hasError).toBe(true); | |
| } | |
| }); | |
| it('recovers stray case terminators at top level', () => { | |
| for (const source of ['a ;& b', 'a ;;& b', ';;&']) { | |
| const result = parse(source); | |
| expect(result.ok).toBe(true); | |
| if (result.ok) expect(result.hasError).toBe(true); | |
| } | |
| }); | |
| it('flags an empty subshell', () => { | |
| const result = parse('()'); | |
| expect(result.ok).toBe(true); | |
| if (result.ok) expect(result.hasError).toBe(true); | |
| }); | |
| it('handles deeply nested compound commands within the depth cap', () => { | |
| const source = 'if a; then '.repeat(600) + 'b' + ' fi'.repeat(600); | |
| const result = parse(source, { timeoutMs: 10_000 }); | |
| expect(result.ok).toBe(true); | |
| if (result.ok) expect(result.hasError).toBe(true); | |
| }); | |
| it('handles deeply nested arithmetic parentheses within the depth cap', () => { | |
| const source = 'echo $(( ' + '('.repeat(600) + '1' + ')'.repeat(600) + ' ))'; | |
| const result = parse(source, { timeoutMs: 10_000 }); | |
| expect(result.ok).toBe(true); | |
| if (result.ok) expect(result.hasError).toBe(true); | |
| }); | |
| it('handles deeply nested case statements within the depth cap', () => { | |
| const source = 'case x in a) '.repeat(600) + 'y' + ' ;; esac'.repeat(600); | |
| const result = parse(source, { timeoutMs: 10_000 }); | |
| expect(result.ok).toBe(true); | |
| if (result.ok) expect(result.hasError).toBe(true); | |
| }); | |
| it('parses a pathological test command under budget', () => { | |
| const result = parse('[[ ' + 'a && '.repeat(2000) + 'a ]]'); | |
| // Either parses (with bounded work) or the budget cuts it off; never throws. | |
| if (result.ok) { | |
| expect(result.rootNode.type).toBe('program'); | |
| } else { | |
| expect(result.reason).toBe('aborted'); | |
| } | |
| }); | |
| }); | |
| describe('M2 review regressions', () => { | |
| it('keeps a full case_statement inside $() despite pattern parens', () => { | |
| expectTree( | |
| 'x=$(case y in a) 1;; esac)', | |
| `(program (variable_assignment (variable_name "x") "=" (command_substitution "$(" (case_statement "case" (word "y") "in" (case_item (word "a") ")" (command (command_name (number "1"))) ";;") "esac") ")")))`, | |
| ); | |
| // Optional-paren item form stays balanced too. | |
| expectTree( | |
| 'x=$(case y in (b) 2;; esac)', | |
| `(program (variable_assignment (variable_name "x") "=" (command_substitution "$(" (case_statement "case" (word "y") "in" (case_item "(" (word "b") ")" (command (command_name (number "2"))) ";;") "esac") ")")))`, | |
| ); | |
| expectTree( | |
| 'diff <(case y in a) 1;; esac) other', | |
| `(program (command (command_name (word "diff")) (process_substitution "<(" (case_statement "case" (word "y") "in" (case_item (word "a") ")" (command (command_name (number "1"))) ";;") "esac") ")") (word "other")))`, | |
| ); | |
| }); | |
| it('does not mistake an argument named case for a case_statement in $()', () => { | |
| expectTree( | |
| '$(echo case; ls)', | |
| `(program (command (command_name (command_substitution "$(" (command (command_name (word "echo")) (word "case")) ";" (command (command_name (word "ls"))) ")"))))`, | |
| ); | |
| }); | |
| it('converts test right-hand sides by operator, glob and paren depth', () => { | |
| expectTree( | |
| '[[ ( $a == x* ) ]]', | |
| `(program (test_command "[[" (parenthesized_expression "(" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (extglob_pattern "x*")) ")") "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ ( $a = x* ) ]]', | |
| `(program (test_command "[[" (parenthesized_expression "(" (binary_expression (simple_expansion "$" (variable_name "a")) "=" (word "x*")) ")") "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $x == b=c ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "x")) "==" (word "b=c")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ a = b=c ]]', | |
| `(program (test_command "[[" (binary_expression (word "a") "=" (regex "b=c")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ a = =b ]]', | |
| `(program (test_command "[[" (binary_expression (word "a") "=" (regex "=b")) "]]"))`, | |
| ); | |
| }); | |
| it('parses statement-position ((word++…)) as a test_command', () => { | |
| expectTree('((a++))', `(program (test_command "((" (word "a++") "))"))`); | |
| expectTree('((a--))', `(program (test_command "((" (word "a--") "))"))`); | |
| expectTree( | |
| '((a++ + b))', | |
| `(program (test_command "((" (binary_expression (word "a++") "+" (word "b")) "))"))`, | |
| ); | |
| // Other ((…)) forms and non-statement positions stay arithmetic. | |
| expectTree( | |
| '((b + a++))', | |
| `(program (command (command_name (arithmetic_expansion "((" (binary_expression (variable_name "b") "+" (postfix_expression (variable_name "a") "++")) "))"))))`, | |
| ); | |
| expectTree( | |
| 'echo ((a++))', | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "((" (postfix_expression (variable_name "a") "++") "))")))`, | |
| ); | |
| expectTree( | |
| '! ((a++))', | |
| `(program (negated_command "!" (test_command "((" (word "a++") "))")))`, | |
| ); | |
| }); | |
| it('keeps unconsumed expression input in ERROR nodes', () => { | |
| const arithmetic = parseOk('echo $((x[1][2]))'); | |
| expect(arithmetic.hasError).toBe(true); | |
| expect(sexp(arithmetic.rootNode)).toBe( | |
| `(program (command (command_name (word "echo")) (arithmetic_expansion "$((" (subscript (variable_name "x") "[" (number "1") "]") (ERROR "[2]") "))")))`, | |
| ); | |
| const test = parseOk('[[ $a =~ a b ]]'); | |
| expect(test.hasError).toBe(true); | |
| const errors = descendantsOfType(test.rootNode, 'ERROR'); | |
| expect(errors).toHaveLength(1); | |
| expect(errors[0]!.text).toContain('b'); | |
| }); | |
| it('parses negative offsets in max-length expansions as numbers', () => { | |
| expectTree( | |
| 'echo ${x: -5}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "x") ":" (number "-5") "}")))`, | |
| ); | |
| expectTree( | |
| 'echo ${x: -5:2}', | |
| `(program (command (command_name (word "echo")) (expansion "\${" (variable_name "x") ":" (number "-5") ":" (number "2") "}")))`, | |
| ); | |
| }); | |
| it('parses $0 and ${0} as special_variable_name', () => { | |
| expectTree( | |
| 'echo $0 ${0} $1 ${10}', | |
| `(program (command (command_name (word "echo")) (simple_expansion "$" (special_variable_name "0")) (expansion "\${" (special_variable_name "0") "}") (simple_expansion "$" (variable_name "1")) (expansion "\${" (variable_name "10") "}")))`, | |
| ); | |
| }); | |
| it('degrades locally on deep pattern→string→expansion nesting', () => { | |
| let inner = '${a#""}'; | |
| for (let k = 0; k < 650; k++) inner = `\${a#"${inner}"}`; | |
| const result = parse(`echo ${inner}`, { timeoutMs: 10_000 }); | |
| expect(result.ok).toBe(true); | |
| if (!result.ok) return; | |
| expect(result.hasError).toBe(true); | |
| // Local degradation: the tree keeps the outer expansions instead of | |
| // collapsing into a single ERROR child under program. | |
| expect(result.rootNode.children[0]!.type).not.toBe('ERROR'); | |
| expect(descendantsOfType(result.rootNode, 'expansion').length).toBeGreaterThan(100); | |
| }); | |
| it('treats = and ! as word characters at operand position in tests', () => { | |
| expectTree('[[ a!=b ]]', `(program (test_command "[[" (word "a!=b") "]]"))`); | |
| expectTree('[[ a=b ]]', `(program (test_command "[[" (word "a=b") "]]"))`); | |
| expectTree( | |
| '[[ !x = y ]]', | |
| `(program (test_command "[[" (binary_expression (word "!x") "=" (word "y")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $a==b* ]]', | |
| `(program (test_command "[[" (concatenation (simple_expansion "$" (variable_name "a")) (word "==b*")) "]]"))`, | |
| ); | |
| // But they are operators at operator position, even when attached. | |
| expectTree( | |
| '[[ a ==b ]]', | |
| `(program (test_command "[[" (binary_expression (word "a") "==" (extglob_pattern "b")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ a =b ]]', | |
| `(program (test_command "[[" (binary_expression (word "a") "=" (word "b")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $a =~^x ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "=~" (regex "^x")) "]]"))`, | |
| ); | |
| }); | |
| it('parses [[ ]] as a concatenation plus a zero-width closer (reference quirk)', () => { | |
| expectTree( | |
| '[[ ]]', | |
| `(program (test_command "[[" (concatenation (word "]") (word "]")) "]]"))`, | |
| true, | |
| ); | |
| }); | |
| it('parses bare numbers inside square brackets as number pieces', () => { | |
| expectTree( | |
| 'echo [1] a[2]b', | |
| `(program (command (command_name (word "echo")) (concatenation (word "[") (number "1") (word "]")) (concatenation (word "a") (word "[") (number "2") (word "]") (word "b"))))`, | |
| ); | |
| expectTree( | |
| 'echo a[-5]b', | |
| `(program (command (command_name (word "echo")) (concatenation (word "a") (word "[") (number "-5") (word "]") (word "b"))))`, | |
| ); | |
| }); | |
| }); | |
| describe('M2 review round 2 regressions', () => { | |
| it('applies the scanner.c extglob_pattern rule to ==/!= right sides', () => { | |
| // Single letter followed by whitespace → glob; followed by `)` → word. | |
| expectTree( | |
| '[[ $a == b ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (extglob_pattern "b")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ (a == b) && c ]]', | |
| `(program (test_command "[[" (binary_expression (parenthesized_expression "(" (binary_expression (word "a") "==" (word "b")) ")") "&&" (word "c")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ ( $a == x ) ]]', | |
| `(program (test_command "[[" (parenthesized_expression "(" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (extglob_pattern "x")) ")") "]]"))`, | |
| ); | |
| // Multi-character all-letter words stay words, dots don't count. | |
| expectTree( | |
| '[[ $a == foo ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (word "foo")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $a == x.y ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (word "x.y")) "]]"))`, | |
| ); | |
| // Any non-letter non-dot character makes it a glob. | |
| for (const word of ['x1', 'foo-', 'x_', 'b*c=d', 'bc=d', 'b?=e', '.x', 'a-b']) { | |
| const result = parseOk(`[[ $a == ${word} ]]`); | |
| const globs = descendantsOfType(result.rootNode, 'extglob_pattern'); | |
| expect(globs.map((g) => g.text)).toEqual([word]); | |
| } | |
| // `=` vetoes only as the second character; a leading `=` vetoes too. | |
| expectTree( | |
| '[[ $x == b=c ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "x")) "==" (word "b=c")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ a == =b ]]', | |
| `(program (test_command "[[" (binary_expression (word "a") "==" (word "=b")) "]]"))`, | |
| ); | |
| // Round-1 fixed behavior must not regress. | |
| expectTree( | |
| '[[ $x == b*c ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "x")) "==" (extglob_pattern "b*c")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $x == 123 ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "x")) "==" (number "123")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ a ==b ]]', | |
| `(program (test_command "[[" (binary_expression (word "a") "==" (extglob_pattern "b")) "]]"))`, | |
| ); | |
| }); | |
| it('keeps the = right-side regex rule independent of the glob rule', () => { | |
| expectTree( | |
| '[[ a = b*c ]]', | |
| `(program (test_command "[[" (binary_expression (word "a") "=" (regex "b*c")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ a = bc ]]', | |
| `(program (test_command "[[" (binary_expression (word "a") "=" (word "bc")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ a = b=c ]]', | |
| `(program (test_command "[[" (binary_expression (word "a") "=" (regex "b=c")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ ( $a = x* ) ]]', | |
| `(program (test_command "[[" (parenthesized_expression "(" (binary_expression (simple_expansion "$" (variable_name "a")) "=" (word "x*")) ")") "]]"))`, | |
| ); | |
| }); | |
| it('survives 2500 levels of lexer-side pattern nesting', () => { | |
| let inner = '${a#""}'; | |
| for (let k = 0; k < 2500; k++) inner = `\${a#"${inner}"}`; | |
| const result = parse(`echo ${inner}`, { timeoutMs: 30_000 }); | |
| // No RangeError, no abort: the scan depth cap degrades locally. | |
| expect(result.ok).toBe(true); | |
| if (!result.ok) return; | |
| expect(result.hasError).toBe(true); | |
| expect(result.rootNode.children[0]!.type).not.toBe('ERROR'); | |
| }); | |
| it('parses ((…)) inside a test as an arithmetic_expansion', () => { | |
| expectTree( | |
| '[[ ((a)) == x ]]', | |
| `(program (test_command "[[" (binary_expression (arithmetic_expansion "((" (variable_name "a") "))") "==" (extglob_pattern "x")) "]]"))`, | |
| ); | |
| // Without the closing )), parens still nest as parenthesized_expressions. | |
| expectTree( | |
| '[[ ((a) == x) && y ]]', | |
| `(program (test_command "[[" (binary_expression (parenthesized_expression "(" (binary_expression (parenthesized_expression "(" (word "a") ")") "==" (word "x")) ")") "&&" (word "y")) "]]"))`, | |
| ); | |
| }); | |
| it('parses ((ab-cd++)) as a test_command', () => { | |
| expectTree('((ab-cd++))', `(program (test_command "((" (word "ab-cd++") "))"))`); | |
| }); | |
| it('flags a missing separator before a compound keyword', () => { | |
| for (const source of [ | |
| 'if a; then case x in b) 1;; esac elif c; then d; fi', | |
| 'if a; then b fi', | |
| '{ ls }', | |
| 'while x do y done', | |
| 'for f in a b; do x done', | |
| ]) { | |
| const result = parse(source); | |
| expect(result.ok).toBe(true); | |
| if (result.ok) expect(result.hasError).toBe(true); | |
| } | |
| // Valid inputs stay clean: case items need no terminator before esac, | |
| // subshells none before ), compounds with proper terminators. | |
| for (const source of ['if a; then b; fi', '{ ls; }', '(ls)', 'case $x in a) x esac', 'while x; do y; done']) { | |
| const result = parse(source); | |
| expect(result.ok).toBe(true); | |
| if (result.ok) expect(result.hasError).toBe(false); | |
| } | |
| }); | |
| }); | |
| describe('M2 review round 3 regressions', () => { | |
| it('converts […]-containing glob right sides as a whole', () => { | |
| expectTree( | |
| '[[ $ver == [0-9]* ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "ver")) "==" (extglob_pattern "[0-9]*")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $a == [abc] ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (extglob_pattern "[abc]")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ ( $a == [xyz] ) ]]', | |
| `(program (test_command "[[" (parenthesized_expression "(" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (extglob_pattern "[xyz]")) ")") "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $a == [!a]* ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (extglob_pattern "[!a]*")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $a = [abc] ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "=" (regex "[abc]")) "]]"))`, | |
| ); | |
| // Bracket fragments outside test commands are untouched. | |
| expectTree( | |
| 'echo a[2]b', | |
| `(program (command (command_name (word "echo")) (concatenation (word "a") (word "[") (number "2") (word "]") (word "b"))))`, | |
| ); | |
| }); | |
| it('keeps -word as a word when no operand follows it', () => { | |
| expectTree( | |
| '[[ $a == -foo ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (word "-foo")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ -foo == x ]]', | |
| `(program (test_command "[[" (binary_expression (word "-foo") "==" (extglob_pattern "x")) "]]"))`, | |
| ); | |
| expectTree('[[ -f ]]', `(program (test_command "[[" (word "-f") "]]"))`); | |
| // A real operand still makes it a test_operator. | |
| expectTree( | |
| '[[ -f file.txt ]]', | |
| `(program (test_command "[[" (unary_expression (test_operator "-f") (word "file.txt")) "]]"))`, | |
| ); | |
| }); | |
| it('parses extglob group patterns after ==/!=', () => { | |
| expectTree( | |
| '[[ $a == +(!a) ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (extglob_pattern "+(!a)")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $a == ?(a|b) ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (extglob_pattern "?(a|b)")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $a == !(x) ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (extglob_pattern "!(x)")) "]]"))`, | |
| ); | |
| }); | |
| it('parses a negative number operand in tests as unary minus', () => { | |
| expectTree( | |
| '[[ $a == -1 ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (unary_expression "-" (number "1"))) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $a -eq -1 ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) (test_operator "-eq") (unary_expression "-" (number "1"))) "]]"))`, | |
| ); | |
| }); | |
| it('handles escaped characters in glob right sides', () => { | |
| expectTree( | |
| '[[ $a == foo\\ bar ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (word "foo\\\\ bar")) "]]"))`, | |
| ); | |
| expectTree( | |
| '[[ $a == b\\*c ]]', | |
| `(program (test_command "[[" (binary_expression (simple_expansion "$" (variable_name "a")) "==" (extglob_pattern "b\\\\*c")) "]]"))`, | |
| ); | |
| }); | |
| it('extends elif/else clause ranges over the trailing newline', () => { | |
| const { rootNode, hasError } = parseOk('if a; then b; elif c; then d\nelse e\nfi'); | |
| expect(hasError).toBe(false); | |
| const elifClause = descendantsOfType(rootNode, 'elif_clause')[0]!; | |
| const elseClause = descendantsOfType(rootNode, 'else_clause')[0]!; | |
| expect(elifClause.text).toBe('elif c; then d\n'); | |
| expect(elseClause.text).toBe('else e\n'); | |
| }); | |
| it('degrades 400-level command substitution nesting locally', () => { | |
| const source = 'echo ' + '$('.repeat(400) + 'x' + ')'.repeat(400); | |
| const result = parse(source, { timeoutMs: 10_000 }); | |
| expect(result.ok).toBe(true); | |
| if (!result.ok) return; | |
| expect(result.hasError).toBe(true); | |
| // Local degradation (MAX_SUBSTITUTION_DEPTH), not a last-resort | |
| // single-ERROR root. | |
| expect(result.rootNode.children[0]!.type).not.toBe('ERROR'); | |
| expect(descendantsOfType(result.rootNode, 'command_substitution').length).toBeGreaterThan(100); | |
| }); | |
| }); | |