( P: ParseState, nodeType: string, stopAtSlash: boolean, )
| 2805 | } |
| 2806 | |
| 2807 | function parseExpansionRest( |
| 2808 | P: ParseState, |
| 2809 | nodeType: string, |
| 2810 | stopAtSlash: boolean, |
| 2811 | ): TsNode | null { |
| 2812 | // Don't skipBlanks — `${var:- }` space IS the word. Stop at } or newline |
| 2813 | // (`${var:\n}` emits no word). stopAtSlash=true stops at `/` for pat/repl |
| 2814 | // split in ${var/pat/repl}. nodeType 'replword' is word-mode for the |
| 2815 | // replacement in `/` `//` — same as 'word' but `(` is NOT array. |
| 2816 | const start = P.L.b |
| 2817 | // Value-substitution RHS starting with `(` parses as array: ${var:-(x)} → |
| 2818 | // (expansion (variable_name) (array (word))). Only for 'word' context (not |
| 2819 | // pattern-matching operators which emit regex, and not 'replword' where `(` |
| 2820 | // is a regular char per grammar `_expansion_regex_replacement`). |
| 2821 | if (nodeType === 'word' && peek(P.L) === '(') { |
| 2822 | advance(P.L) |
| 2823 | const open = mk(P, '(', start, P.L.b, []) |
| 2824 | const elems: TsNode[] = [open] |
| 2825 | while (P.L.i < P.L.len) { |
| 2826 | skipBlanks(P.L) |
| 2827 | const c = peek(P.L) |
| 2828 | if (c === ')' || c === '}' || c === '\n' || c === '') break |
| 2829 | const wStart = P.L.b |
| 2830 | while (P.L.i < P.L.len) { |
| 2831 | const wc = peek(P.L) |
| 2832 | if ( |
| 2833 | wc === ')' || |
| 2834 | wc === '}' || |
| 2835 | wc === ' ' || |
| 2836 | wc === '\t' || |
| 2837 | wc === '\n' || |
| 2838 | wc === '' |
| 2839 | ) { |
| 2840 | break |
| 2841 | } |
| 2842 | advance(P.L) |
| 2843 | } |
| 2844 | if (P.L.b > wStart) elems.push(mk(P, 'word', wStart, P.L.b, [])) |
| 2845 | else break |
| 2846 | } |
| 2847 | if (peek(P.L) === ')') { |
| 2848 | const cStart = P.L.b |
| 2849 | advance(P.L) |
| 2850 | elems.push(mk(P, ')', cStart, P.L.b, [])) |
| 2851 | } |
| 2852 | while (peek(P.L) === '\n') advance(P.L) |
| 2853 | return mk(P, 'array', start, P.L.b, elems) |
| 2854 | } |
| 2855 | // REGEX mode: flat single-span scan. Quotes are opaque (skipped past so |
| 2856 | // `/` inside them doesn't break stopAtSlash), but NOT emitted as separate |
| 2857 | // nodes — the entire range becomes one regex node. |
| 2858 | if (nodeType === 'regex') { |
| 2859 | let braceDepth = 0 |
| 2860 | while (P.L.i < P.L.len) { |
| 2861 | const c = peek(P.L) |
| 2862 | if (c === '\n') break |
| 2863 | if (braceDepth === 0) { |
| 2864 | if (c === '}') break |
no test coverage detected