Skip to main content

shuck_parser/parser/
mod.rs

1//! Parser entrypoints, lexical types, and shell-profile configuration.
2//!
3//! The parser is recursive descent and produces `shuck-ast` syntax trees while also collecting
4//! recovery diagnostics and lightweight syntax facts needed by downstream tooling.
5#![cfg_attr(not(test), warn(clippy::unwrap_used))]
6
7mod arithmetic;
8mod commands;
9mod heredocs;
10mod keywords;
11mod lexer;
12mod parser_state;
13mod profile;
14mod redirects;
15mod result;
16mod words;
17mod zsh_options;
18mod zsh_prescan;
19
20use std::{
21    borrow::Cow,
22    collections::{HashMap, HashSet, VecDeque},
23    sync::Arc,
24};
25
26pub use lexer::{LexedToken, Lexer};
27pub(crate) use lexer::{LexedWordSegment, LexedWordSegmentKind};
28pub use profile::{ShellDialect, ShellProfile};
29pub use result::{ParseDiagnostic, ParseResult, ParseStatus, SyntaxFacts, ZshCaseGroupPart};
30pub use zsh_options::{OptionValue, ZshEmulationMode, ZshOptionState};
31
32use keywords::*;
33use memchr::{memchr, memchr2, memchr3};
34pub use parser_state::Parser;
35#[cfg(feature = "benchmarking")]
36pub use parser_state::ParserBenchmarkCounters;
37use parser_state::*;
38use smallvec::SmallVec;
39use zsh_prescan::ZshOptionTimeline;
40
41use shuck_ast::{
42    AlwaysCommand, AnonymousFunctionCommand, AnonymousFunctionSurface, ArithmeticCommand,
43    ArithmeticExpansionSyntax, ArithmeticExpr, ArithmeticExprNode, ArithmeticForCommand,
44    ArithmeticLvalue, ArrayElem, ArrayExpr, ArrayKind, Assignment, AssignmentValue,
45    BackgroundOperator, BinaryCommand, BinaryOp, BourneParameterExpansion, BraceExpansionKind,
46    BraceQuoteContext, BraceSyntax, BraceSyntaxKind, BreakCommand as AstBreakCommand,
47    BuiltinCommand as AstBuiltinCommand, CaseCommand, CaseItem, CaseTerminator,
48    Command as AstCommand, CommandSubstitutionSyntax, Comment, CompoundCommand,
49    ConditionalBinaryExpr, ConditionalBinaryOp, ConditionalCommand, ConditionalExpr,
50    ConditionalParenExpr, ConditionalUnaryExpr, ConditionalUnaryOp,
51    ContinueCommand as AstContinueCommand, CoprocCommand, DeclClause as AstDeclClause, DeclOperand,
52    ExitCommand as AstExitCommand, File, ForCommand, ForSyntax, ForTarget, ForeachCommand,
53    ForeachSyntax, FunctionDef, FunctionHeader, FunctionHeaderEntry, Heredoc, HeredocBody,
54    HeredocBodyMode, HeredocBodyPart, HeredocBodyPartNode, HeredocDelimiter, IfCommand, IfSyntax,
55    LiteralText, Name, ParameterExpansion, ParameterExpansionSyntax, ParameterOp, Pattern,
56    PatternGroupKind, PatternPart, PatternPartNode, Position, PrefixMatchKind, Redirect,
57    RedirectKind, RedirectTarget, RepeatCommand, RepeatSyntax, ReturnCommand as AstReturnCommand,
58    SelectCommand, SimpleCommand as AstSimpleCommand, SourceText, Span, Stmt, StmtSeq,
59    StmtTerminator, Subscript, SubscriptInterpretation, SubscriptKind, SubscriptSelector, TextSize,
60    TimeCommand, TokenKind, UntilCommand, VarRef, WhileCommand, Word, WordPart, WordPartNode,
61    ZshDefaultingOp, ZshExpansionOperation, ZshExpansionTarget, ZshGlobQualifier,
62    ZshGlobQualifierGroup, ZshGlobQualifierKind, ZshGlobSegment, ZshInlineGlobControl, ZshModifier,
63    ZshParameterExpansion, ZshPatternOp, ZshQualifiedGlob, ZshReplacementOp, ZshTrimOp,
64};
65
66use crate::error::{Error, Result};
67
68type WordPartBuffer = SmallVec<[WordPartNode; 2]>;
69
70/// Default maximum AST depth (matches ExecutionLimits default)
71const DEFAULT_MAX_AST_DEPTH: usize = 100;
72
73/// Hard cap on AST depth to prevent stack overflow even if caller misconfigures limits.
74/// Protects against deeply nested input attacks where
75/// a large max_depth setting allows recursion deep enough to overflow the native stack.
76/// This cap cannot be overridden by the caller.
77///
78/// Set conservatively to avoid stack overflow on tokio's blocking threads (default 2MB
79/// stack in debug builds). Each parser recursion level uses ~4-8KB of stack in debug
80/// mode. 100 levels × ~8KB = ~800KB, well within 2MB.
81/// In release builds this could safely be higher, but we use one value for consistency.
82const HARD_MAX_AST_DEPTH: usize = 100;
83
84/// Auxiliary word reparsing happens while the main parser is already on the stack.
85/// Keep its synthetic parser shallower than the main AST limit.
86const SOURCE_TEXT_WORD_REPARSE_MAX_DEPTH: usize = 8;
87
88/// Pattern operands can themselves contain parameter expansions with pattern operands.
89/// Keep that source-text reparsing shallow and preserve deeper text literally.
90const SOURCE_TEXT_PATTERN_REPARSE_MAX_DEPTH: usize = 4;
91
92/// Default maximum parser operations (matches ExecutionLimits default)
93const DEFAULT_MAX_PARSER_OPERATIONS: usize = 100_000;
94
95/// Returns whether `text` parses as a nontrivial arithmetic expression.
96///
97/// Plain numbers and plain variable names are considered trivial. The helper
98/// returns `false` for empty text and for text that cannot be parsed inside a
99/// shell arithmetic command.
100pub fn text_looks_like_nontrivial_arithmetic_expression(text: &str) -> bool {
101    let text = text.trim();
102    if text.is_empty() {
103        return false;
104    }
105
106    let source = format!("(( {text} ))");
107    let file = Parser::new(&source).parse();
108    if file.is_err() {
109        return false;
110    }
111
112    let Some(statement) = file.file.body.first() else {
113        return false;
114    };
115
116    let AstCommand::Compound(CompoundCommand::Arithmetic(command)) = &statement.command else {
117        return false;
118    };
119
120    command.expr_ast.as_ref().is_some_and(|expr| {
121        !matches!(
122            expr.kind,
123            ArithmeticExpr::Number(_) | ArithmeticExpr::Variable(_)
124        )
125    })
126}
127
128/// Returns whether `text` parses as an arithmetic expression without variable
129/// references, subscripts, shell words, or assignments.
130///
131/// This is useful when a caller needs a purely self-contained arithmetic value.
132/// Invalid or empty text returns `false`.
133pub fn text_is_self_contained_arithmetic_expression(text: &str) -> bool {
134    let text = text.trim();
135    if text.is_empty() {
136        return false;
137    }
138
139    let source = format!("(( {text} ))");
140    let file = Parser::new(&source).parse();
141    if file.is_err() {
142        return false;
143    }
144
145    let Some(statement) = file.file.body.first() else {
146        return false;
147    };
148
149    let AstCommand::Compound(CompoundCommand::Arithmetic(command)) = &statement.command else {
150        return false;
151    };
152
153    command
154        .expr_ast
155        .as_ref()
156        .is_some_and(arithmetic_expr_is_self_contained)
157}
158
159fn arithmetic_expr_is_self_contained(expr: &ArithmeticExprNode) -> bool {
160    match &expr.kind {
161        ArithmeticExpr::Number(_) => true,
162        ArithmeticExpr::Variable(_)
163        | ArithmeticExpr::Indexed { .. }
164        | ArithmeticExpr::ShellWord(_)
165        | ArithmeticExpr::Assignment { .. } => false,
166        ArithmeticExpr::Parenthesized { expression } => {
167            arithmetic_expr_is_self_contained(expression)
168        }
169        ArithmeticExpr::Unary { expr, .. } | ArithmeticExpr::Postfix { expr, .. } => {
170            arithmetic_expr_is_self_contained(expr)
171        }
172        ArithmeticExpr::Binary { left, right, .. } => {
173            arithmetic_expr_is_self_contained(left) && arithmetic_expr_is_self_contained(right)
174        }
175        ArithmeticExpr::Conditional {
176            condition,
177            then_expr,
178            else_expr,
179        } => {
180            arithmetic_expr_is_self_contained(condition)
181                && arithmetic_expr_is_self_contained(then_expr)
182                && arithmetic_expr_is_self_contained(else_expr)
183        }
184    }
185}
186
187#[cfg(test)]
188mod arithmetic_text_helper_tests {
189    use super::{
190        text_is_self_contained_arithmetic_expression,
191        text_looks_like_nontrivial_arithmetic_expression,
192    };
193
194    #[test]
195    fn requires_nontrivial_expressions() {
196        assert!(text_looks_like_nontrivial_arithmetic_expression("1 + 2"));
197        assert!(text_looks_like_nontrivial_arithmetic_expression("arr[1]"));
198        assert!(text_looks_like_nontrivial_arithmetic_expression("++count"));
199        assert!(!text_looks_like_nontrivial_arithmetic_expression("123"));
200        assert!(!text_looks_like_nontrivial_arithmetic_expression("name"));
201        assert!(!text_looks_like_nontrivial_arithmetic_expression(
202            "latest value"
203        ));
204    }
205
206    #[test]
207    fn distinguishes_self_contained_expressions() {
208        assert!(text_is_self_contained_arithmetic_expression("1 + 2"));
209        assert!(text_is_self_contained_arithmetic_expression("(1 + 2)"));
210        assert!(!text_is_self_contained_arithmetic_expression("name"));
211        assert!(!text_is_self_contained_arithmetic_expression("arr[1]"));
212        assert!(!text_is_self_contained_arithmetic_expression("foo + 1"));
213        assert!(!text_is_self_contained_arithmetic_expression(
214            "latest value"
215        ));
216    }
217}
218
219impl<'a> Parser<'a> {
220    /// Create a new parser for the given input.
221    pub fn new(input: &'a str) -> Self {
222        Self::with_limits_and_profile(
223            input,
224            DEFAULT_MAX_AST_DEPTH,
225            DEFAULT_MAX_PARSER_OPERATIONS,
226            ShellProfile::native(ShellDialect::Bash),
227        )
228    }
229
230    /// Create a new parser for the given input and shell dialect.
231    ///
232    /// This uses [`ShellProfile::native`] for the selected dialect. Use
233    /// [`Parser::with_profile`] when zsh option state is known.
234    pub fn with_dialect(input: &'a str, dialect: ShellDialect) -> Self {
235        Self::with_profile(input, ShellProfile::native(dialect))
236    }
237
238    /// Create a new parser for the given input and full shell profile.
239    ///
240    /// Profiles allow callers to provide parser-visible zsh option state in
241    /// addition to the broad shell dialect.
242    pub fn with_profile(input: &'a str, shell_profile: ShellProfile) -> Self {
243        Self::with_limits_and_profile(
244            input,
245            DEFAULT_MAX_AST_DEPTH,
246            DEFAULT_MAX_PARSER_OPERATIONS,
247            shell_profile,
248        )
249    }
250
251    /// Create a new bash parser with a custom maximum AST depth.
252    ///
253    /// The requested depth is clamped to the parser's hard safety cap. Hitting
254    /// the limit produces a non-clean [`ParseResult`] rather than panicking.
255    pub fn with_max_depth(input: &'a str, max_depth: usize) -> Self {
256        Self::with_limits_and_profile(
257            input,
258            max_depth,
259            DEFAULT_MAX_PARSER_OPERATIONS,
260            ShellProfile::native(ShellDialect::Bash),
261        )
262    }
263
264    /// Create a new bash parser with a custom fuel limit.
265    ///
266    /// Fuel bounds the number of parser operations. Exhaustion produces a
267    /// terminal parse error in the returned [`ParseResult`].
268    pub fn with_fuel(input: &'a str, max_fuel: usize) -> Self {
269        Self::with_limits_and_profile(
270            input,
271            DEFAULT_MAX_AST_DEPTH,
272            max_fuel,
273            ShellProfile::native(ShellDialect::Bash),
274        )
275    }
276
277    /// Create a new bash parser with custom depth and fuel limits.
278    ///
279    /// `max_depth` is clamped to the parser's hard safety cap to prevent stack
280    /// overflow from misconfiguration. `max_fuel` bounds parser operations.
281    /// Either limit can produce a non-clean [`ParseResult`].
282    pub fn with_limits(input: &'a str, max_depth: usize, max_fuel: usize) -> Self {
283        Self::with_limits_and_profile(
284            input,
285            max_depth,
286            max_fuel,
287            ShellProfile::native(ShellDialect::Bash),
288        )
289    }
290
291    /// Create a new parser with custom depth, fuel, and dialect settings.
292    ///
293    /// This uses [`ShellProfile::native`] for `dialect`; use
294    /// [`Parser::with_limits_and_profile`] when explicit zsh option state is
295    /// available.
296    pub fn with_limits_and_dialect(
297        input: &'a str,
298        max_depth: usize,
299        max_fuel: usize,
300        dialect: ShellDialect,
301    ) -> Self {
302        Self::with_limits_and_profile(input, max_depth, max_fuel, ShellProfile::native(dialect))
303    }
304
305    /// Create a new parser with custom depth, fuel, and shell-profile settings.
306    ///
307    /// This is the most explicit constructor for embedders that need both
308    /// resource limits and parser-visible shell option state.
309    pub fn with_limits_and_profile(
310        input: &'a str,
311        max_depth: usize,
312        max_fuel: usize,
313        shell_profile: ShellProfile,
314    ) -> Self {
315        Self::with_limits_and_profile_and_benchmarking(
316            input,
317            max_depth,
318            max_fuel,
319            shell_profile,
320            false,
321        )
322    }
323
324    fn with_limits_and_profile_and_benchmarking(
325        input: &'a str,
326        max_depth: usize,
327        max_fuel: usize,
328        shell_profile: ShellProfile,
329        benchmark_counters_enabled: bool,
330    ) -> Self {
331        #[cfg(not(feature = "benchmarking"))]
332        let _ = benchmark_counters_enabled;
333
334        let zsh_timeline = (shell_profile.dialect == ShellDialect::Zsh)
335            .then(|| ZshOptionTimeline::build(input, &shell_profile))
336            .flatten()
337            .map(Arc::new);
338        let mut lexer = Lexer::with_max_subst_depth_and_profile(
339            input,
340            max_depth.min(HARD_MAX_AST_DEPTH),
341            &shell_profile,
342            zsh_timeline.clone(),
343        );
344        #[cfg(feature = "benchmarking")]
345        if benchmark_counters_enabled {
346            lexer.enable_benchmark_counters();
347        }
348        let mut comments = Vec::new();
349        let (current_token, current_token_kind, current_keyword, current_span) = loop {
350            match lexer.next_lexed_token_with_comments() {
351                Some(st) if st.kind == TokenKind::Comment => {
352                    comments.push(Comment {
353                        range: st.span.to_range(),
354                    });
355                }
356                Some(st) => {
357                    break (
358                        Some(st.clone()),
359                        Some(st.kind),
360                        Self::keyword_from_token(&st),
361                        st.span,
362                    );
363                }
364                None => break (None, None, None, Span::new()),
365            }
366        };
367        Self {
368            input,
369            lexer,
370            synthetic_tokens: VecDeque::new(),
371            alias_replays: Vec::new(),
372            current_token,
373            current_word_cache: None,
374            current_token_kind,
375            current_keyword,
376            current_span,
377            peeked_token: None,
378            max_depth: max_depth.min(HARD_MAX_AST_DEPTH),
379            current_depth: 0,
380            fuel: max_fuel,
381            max_fuel,
382            source_text_pattern_depth: 0,
383            comments,
384            aliases: HashMap::new(),
385            expand_aliases: false,
386            expand_next_word: false,
387            brace_group_depth: 0,
388            brace_body_stack: Vec::new(),
389            syntax_facts: SyntaxFacts::default(),
390            dialect: shell_profile.dialect,
391            shell_profile,
392            zsh_timeline,
393            #[cfg(feature = "benchmarking")]
394            benchmark_counters: benchmark_counters_enabled.then(ParserBenchmarkCounters::default),
395        }
396    }
397
398    #[cfg(feature = "benchmarking")]
399    fn rebuild_with_benchmark_counters(&self) -> Self {
400        Self::with_limits_and_profile_and_benchmarking(
401            self.input,
402            self.max_depth,
403            self.max_fuel,
404            self.shell_profile.clone(),
405            true,
406        )
407    }
408
409    fn zsh_options_at_offset(&self, offset: usize) -> Option<&ZshOptionState> {
410        self.zsh_timeline
411            .as_ref()
412            .map(|timeline| timeline.options_at(offset))
413            .or_else(|| self.shell_profile.zsh_options())
414    }
415
416    fn current_zsh_options(&self) -> Option<&ZshOptionState> {
417        self.zsh_options_at_offset(self.current_span.start.offset)
418    }
419
420    fn zsh_short_loops_enabled(&self) -> bool {
421        self.dialect.features().zsh_foreach_loop
422            && !self
423                .current_zsh_options()
424                .is_some_and(|options| options.short_loops.is_definitely_off())
425    }
426
427    fn zsh_short_repeat_enabled(&self) -> bool {
428        self.dialect.features().zsh_repeat_loop
429            && !self
430                .current_zsh_options()
431                .is_some_and(|options| options.short_repeat.is_definitely_off())
432    }
433
434    fn zsh_brace_bodies_enabled(&self) -> bool {
435        self.dialect.features().zsh_brace_if
436            && !self
437                .current_zsh_options()
438                .is_some_and(|options| options.ignore_braces.is_definitely_on())
439    }
440
441    fn zsh_brace_if_enabled(&self) -> bool {
442        self.zsh_brace_bodies_enabled()
443    }
444
445    fn zsh_glob_qualifiers_enabled_at(&self, offset: usize) -> bool {
446        self.dialect.features().zsh_glob_qualifiers
447            && !self.zsh_options_at_offset(offset).is_some_and(|options| {
448                options.ignore_braces.is_definitely_on()
449                    || options.bare_glob_qual.is_definitely_off()
450            })
451    }
452
453    fn brace_syntax_enabled_at(&self, offset: usize) -> bool {
454        !self.zsh_options_at_offset(offset).is_some_and(|options| {
455            options.ignore_braces.is_definitely_on()
456                || options.ignore_close_braces.is_definitely_on()
457        })
458    }
459
460    #[cfg(test)]
461    fn current_span(&self) -> Span {
462        self.current_span
463    }
464
465    /// Parse a standalone shell word string.
466    ///
467    /// This handles shell word constructs such as parameter expansion, command
468    /// substitution, arithmetic expansion, and quoting. The returned word is
469    /// positioned as if `input` started at the beginning of a file.
470    pub fn parse_word_string(input: &str) -> Word {
471        let mut parser = Parser::new(input);
472        let start = Position::new();
473        parser.parse_word_with_context(
474            input,
475            Span::from_positions(start, start.advanced_by(input)),
476            start,
477            true,
478        )
479    }
480
481    /// Classify a contiguous group of already-parsed words as a shell assignment.
482    ///
483    /// Some shell syntax, such as process substitution inside an array subscript,
484    /// can produce multiple AST words while still occupying one contiguous
485    /// assignment operand in the source.
486    pub fn parse_assignment_word_group(
487        source: &str,
488        words: &[&Word],
489        explicit_array_kind: Option<ArrayKind>,
490        subscript_interpretation: SubscriptInterpretation,
491    ) -> Option<Assignment> {
492        let first = words.first()?;
493        let last = words.last()?;
494        let span = Span::from_positions(first.span.start, last.span.end);
495        let raw = span.slice(source);
496        let mut parser = Parser::new(source);
497        parser.parse_assignment_from_text(raw, span, explicit_array_kind, subscript_interpretation)
498    }
499
500    /// Parse a word string with caller-configured limits and shell dialect.
501    fn parse_word_string_with_limits_and_dialect(
502        input: &str,
503        max_depth: usize,
504        max_fuel: usize,
505        dialect: ShellDialect,
506    ) -> Word {
507        let mut parser = Parser::with_limits_and_profile(
508            input,
509            max_depth,
510            max_fuel,
511            ShellProfile::native(dialect),
512        );
513        let start = Position::new();
514        parser.parse_word_with_context(
515            input,
516            Span::from_positions(start, start.advanced_by(input)),
517            start,
518            true,
519        )
520    }
521
522    /// Parse a fragment against the original source span so part offsets stay
523    /// aligned with the surrounding script.
524    #[cfg(test)]
525    fn parse_word_fragment(source: &str, text: &str, span: Span) -> Word {
526        Self::parse_word_fragment_with_limits(
527            source,
528            text,
529            span,
530            DEFAULT_MAX_AST_DEPTH,
531            DEFAULT_MAX_PARSER_OPERATIONS,
532            ShellProfile::native(ShellDialect::Bash),
533        )
534    }
535
536    fn parse_word_fragment_with_limits(
537        source: &str,
538        text: &str,
539        span: Span,
540        max_depth: usize,
541        max_fuel: usize,
542        shell_profile: ShellProfile,
543    ) -> Word {
544        let mut parser = Parser::with_limits_and_profile(text, max_depth, max_fuel, shell_profile);
545        let source_backed = span.end.offset <= source.len() && span.slice(source) == text;
546        let start = Position::new();
547        let fragment_span = Span::from_positions(start, start.advanced_by(text));
548        let mut word = parser.parse_word_with_context(text, fragment_span, start, source_backed);
549        if !source_backed {
550            Self::materialize_word_source_backing(&mut word, text);
551        }
552        Self::rebase_word(&mut word, span.start);
553        word.span = span;
554        word
555    }
556
557    fn maybe_record_comment(&mut self, token: &LexedToken<'_>) {
558        if token.kind == TokenKind::Comment && !token.flags.is_synthetic() {
559            self.comments.push(Comment {
560                range: token.span.to_range(),
561            });
562        }
563    }
564
565    fn record_zsh_brace_if_span(&mut self, span: Span) {
566        if !self.syntax_facts.zsh_brace_if_spans.contains(&span) {
567            self.syntax_facts.zsh_brace_if_spans.push(span);
568        }
569    }
570
571    fn record_zsh_always_span(&mut self, span: Span) {
572        if !self.syntax_facts.zsh_always_spans.contains(&span) {
573            self.syntax_facts.zsh_always_spans.push(span);
574        }
575    }
576
577    fn record_zsh_case_group_part(&mut self, pattern_part_index: usize, span: Span) {
578        if !self
579            .syntax_facts
580            .zsh_case_group_parts
581            .iter()
582            .any(|fact| fact.pattern_part_index == pattern_part_index && fact.span == span)
583        {
584            self.syntax_facts
585                .zsh_case_group_parts
586                .push(ZshCaseGroupPart {
587                    pattern_part_index,
588                    span,
589                });
590        }
591    }
592
593    fn word_text_needs_parse(text: &str) -> bool {
594        memchr3(b'$', b'`', b'\0', text.as_bytes()).is_some()
595    }
596
597    fn word_with_parts(&self, parts: Vec<WordPartNode>, span: Span) -> Word {
598        let brace_syntax = self.brace_syntax_from_parts(&parts, span.start.offset);
599        Word {
600            parts,
601            span,
602            brace_syntax,
603        }
604    }
605
606    fn word_with_part_buffer(&self, parts: WordPartBuffer, span: Span) -> Word {
607        let brace_syntax = self.brace_syntax_from_parts(&parts, span.start.offset);
608        let parts = if parts.spilled() {
609            parts.into_vec()
610        } else {
611            let mut vec = Vec::with_capacity(parts.len());
612            vec.extend(parts);
613            vec
614        };
615        Word {
616            parts,
617            span,
618            brace_syntax,
619        }
620    }
621
622    fn word_with_single_part(&self, part: WordPartNode, span: Span) -> Word {
623        let mut parts = WordPartBuffer::new();
624        parts.push(part);
625        self.word_with_part_buffer(parts, span)
626    }
627
628    fn heredoc_body_with_parts(
629        &self,
630        parts: Vec<HeredocBodyPartNode>,
631        span: Span,
632        mode: HeredocBodyMode,
633        source_backed: bool,
634    ) -> HeredocBody {
635        HeredocBody {
636            mode,
637            source_backed,
638            parts,
639            span,
640        }
641    }
642
643    fn heredoc_body_part_from_word_part_node(part: WordPartNode) -> HeredocBodyPartNode {
644        let kind = match part.kind {
645            WordPart::Literal(text) => HeredocBodyPart::Literal(text),
646            WordPart::Variable(name) => HeredocBodyPart::Variable(name),
647            WordPart::CommandSubstitution { body, syntax } => {
648                HeredocBodyPart::CommandSubstitution { body, syntax }
649            }
650            WordPart::ArithmeticExpansion {
651                expression,
652                expression_ast,
653                expression_word_ast,
654                syntax,
655            } => HeredocBodyPart::ArithmeticExpansion {
656                expression,
657                expression_ast,
658                expression_word_ast,
659                syntax,
660            },
661            WordPart::Parameter(parameter) => HeredocBodyPart::Parameter(Box::new(parameter)),
662            other => panic!("unsupported heredoc body part: {other:?}"),
663        };
664
665        HeredocBodyPartNode::new(kind, part.span)
666    }
667
668    fn brace_syntax_from_parts(&self, parts: &[WordPartNode], offset: usize) -> Vec<BraceSyntax> {
669        if !self.brace_syntax_enabled_at(offset) {
670            return Vec::new();
671        }
672        let mut brace_syntax = Vec::new();
673        self.collect_brace_syntax_from_parts(parts, BraceQuoteContext::Unquoted, &mut brace_syntax);
674        brace_syntax.sort_by_key(|brace| (brace.span.start.offset, brace.span.end.offset));
675        brace_syntax.dedup_by_key(|brace| {
676            (
677                brace.span.start.offset,
678                brace.span.end.offset,
679                brace.quote_context,
680                brace.kind,
681            )
682        });
683        brace_syntax
684    }
685
686    fn collect_brace_syntax_from_parts(
687        &self,
688        parts: &[WordPartNode],
689        quote_context: BraceQuoteContext,
690        out: &mut Vec<BraceSyntax>,
691    ) {
692        for part in parts {
693            match &part.kind {
694                WordPart::Literal(text) => Self::scan_brace_syntax_text(
695                    text.syntax_str(self.input, part.span),
696                    part.span.start,
697                    quote_context,
698                    out,
699                ),
700                WordPart::SingleQuoted { .. } => Self::scan_brace_syntax_text(
701                    part.span.slice(self.input),
702                    part.span.start,
703                    BraceQuoteContext::SingleQuoted,
704                    out,
705                ),
706                WordPart::DoubleQuoted { parts, .. } => self.collect_brace_syntax_from_parts(
707                    parts,
708                    BraceQuoteContext::DoubleQuoted,
709                    out,
710                ),
711                WordPart::ZshQualifiedGlob(glob) => {
712                    self.collect_brace_syntax_from_zsh_qualified_glob(glob, quote_context, out)
713                }
714                WordPart::Variable(_)
715                | WordPart::CommandSubstitution { .. }
716                | WordPart::ArithmeticExpansion { .. }
717                | WordPart::Parameter(_)
718                | WordPart::ParameterExpansion { .. }
719                | WordPart::Length(_)
720                | WordPart::ArrayAccess(_)
721                | WordPart::ArrayLength(_)
722                | WordPart::ArrayIndices(_)
723                | WordPart::Substring { .. }
724                | WordPart::ArraySlice { .. }
725                | WordPart::IndirectExpansion { .. }
726                | WordPart::PrefixMatch { .. }
727                | WordPart::ProcessSubstitution { .. }
728                | WordPart::Transformation { .. } => {}
729            }
730        }
731
732        if self.needs_cross_part_brace_scan(parts) {
733            let mut chars = Vec::new();
734            self.collect_brace_scan_chars_from_parts(parts, &mut chars);
735            Self::scan_brace_syntax_chars(&chars, quote_context, out);
736        }
737    }
738
739    fn needs_cross_part_brace_scan(&self, parts: &[WordPartNode]) -> bool {
740        if parts.len() < 2 {
741            return false;
742        }
743
744        let mut cursor = parts[0].span.start.offset;
745        for part in parts {
746            if part.span.start.offset > cursor
747                && self
748                    .input
749                    .get(cursor..part.span.start.offset)
750                    .is_some_and(|raw| raw.contains(['{', '}']))
751            {
752                return true;
753            }
754
755            let has_brace_text = match &part.kind {
756                WordPart::Literal(text) => {
757                    text.syntax_str(self.input, part.span).contains(['{', '}'])
758                }
759                WordPart::SingleQuoted { .. }
760                | WordPart::DoubleQuoted { .. }
761                | WordPart::ZshQualifiedGlob(_) => self
762                    .input
763                    .get(part.span.start.offset..part.span.end.offset)
764                    .is_some_and(|raw| raw.contains(['{', '}'])),
765                WordPart::Variable(_)
766                | WordPart::CommandSubstitution { .. }
767                | WordPart::ArithmeticExpansion { .. }
768                | WordPart::Parameter(_)
769                | WordPart::ParameterExpansion { .. }
770                | WordPart::Length(_)
771                | WordPart::ArrayAccess(_)
772                | WordPart::ArrayLength(_)
773                | WordPart::ArrayIndices(_)
774                | WordPart::Substring { .. }
775                | WordPart::ArraySlice { .. }
776                | WordPart::IndirectExpansion { .. }
777                | WordPart::PrefixMatch { .. }
778                | WordPart::ProcessSubstitution { .. }
779                | WordPart::Transformation { .. } => false,
780            };
781            if has_brace_text {
782                return true;
783            }
784
785            cursor = part.span.end.offset;
786        }
787
788        false
789    }
790
791    fn collect_brace_scan_chars_from_parts(
792        &self,
793        parts: &[WordPartNode],
794        out: &mut Vec<(char, Position)>,
795    ) {
796        for part in parts {
797            match &part.kind {
798                WordPart::Literal(text) => {
799                    Self::push_brace_scan_text(
800                        text.syntax_str(self.input, part.span),
801                        part.span.start,
802                        out,
803                    );
804                }
805                WordPart::SingleQuoted { .. } => {
806                    if let Some(raw) = self.input.get(part.span.start.offset..part.span.end.offset)
807                    {
808                        Self::push_brace_scan_text(raw, part.span.start, out);
809                    }
810                }
811                WordPart::DoubleQuoted { parts, .. } => {
812                    self.collect_brace_scan_chars_from_double_quoted_part(part.span, parts, out);
813                }
814                WordPart::ZshQualifiedGlob(_) => {}
815                WordPart::Variable(_)
816                | WordPart::CommandSubstitution { .. }
817                | WordPart::ArithmeticExpansion { .. }
818                | WordPart::Parameter(_)
819                | WordPart::ParameterExpansion { .. }
820                | WordPart::Length(_)
821                | WordPart::ArrayAccess(_)
822                | WordPart::ArrayLength(_)
823                | WordPart::ArrayIndices(_)
824                | WordPart::Substring { .. }
825                | WordPart::ArraySlice { .. }
826                | WordPart::IndirectExpansion { .. }
827                | WordPart::PrefixMatch { .. }
828                | WordPart::ProcessSubstitution { .. }
829                | WordPart::Transformation { .. } => {
830                    Self::push_brace_scan_boundary(part.span.start, out);
831                }
832            }
833        }
834    }
835
836    fn collect_brace_scan_chars_from_double_quoted_part(
837        &self,
838        span: Span,
839        parts: &[WordPartNode],
840        out: &mut Vec<(char, Position)>,
841    ) {
842        let mut cursor_offset = span.start.offset;
843        let mut cursor_position = span.start;
844
845        for part in parts {
846            if part.span.start.offset > cursor_offset
847                && let Some(raw) = self.input.get(cursor_offset..part.span.start.offset)
848            {
849                Self::push_brace_scan_text(raw, cursor_position, out);
850            }
851
852            match &part.kind {
853                WordPart::Literal(text) => {
854                    Self::push_brace_scan_text(
855                        text.syntax_str(self.input, part.span),
856                        part.span.start,
857                        out,
858                    );
859                }
860                WordPart::SingleQuoted { .. } => {
861                    if let Some(raw) = self.input.get(part.span.start.offset..part.span.end.offset)
862                    {
863                        Self::push_brace_scan_text(raw, part.span.start, out);
864                    }
865                }
866                WordPart::DoubleQuoted { parts, .. } => {
867                    self.collect_brace_scan_chars_from_double_quoted_part(part.span, parts, out);
868                }
869                WordPart::ZshQualifiedGlob(_) => {}
870                WordPart::Variable(_)
871                | WordPart::CommandSubstitution { .. }
872                | WordPart::ArithmeticExpansion { .. }
873                | WordPart::Parameter(_)
874                | WordPart::ParameterExpansion { .. }
875                | WordPart::Length(_)
876                | WordPart::ArrayAccess(_)
877                | WordPart::ArrayLength(_)
878                | WordPart::ArrayIndices(_)
879                | WordPart::Substring { .. }
880                | WordPart::ArraySlice { .. }
881                | WordPart::IndirectExpansion { .. }
882                | WordPart::PrefixMatch { .. }
883                | WordPart::ProcessSubstitution { .. }
884                | WordPart::Transformation { .. } => {
885                    Self::push_brace_scan_boundary(part.span.start, out);
886                }
887            }
888
889            cursor_offset = part.span.end.offset;
890            cursor_position = part.span.end;
891        }
892
893        if cursor_offset < span.end.offset
894            && let Some(raw) = self.input.get(cursor_offset..span.end.offset)
895        {
896            Self::push_brace_scan_text(raw, cursor_position, out);
897        }
898    }
899
900    fn push_brace_scan_text(text: &str, start: Position, out: &mut Vec<(char, Position)>) {
901        let mut position = start;
902        for ch in text.chars() {
903            out.push((ch, position));
904            position.advance(ch);
905        }
906    }
907
908    fn push_brace_scan_boundary(position: Position, out: &mut Vec<(char, Position)>) {
909        out.push(('\0', position));
910    }
911
912    fn scan_brace_syntax_chars(
913        chars: &[(char, Position)],
914        initial_quote_context: BraceQuoteContext,
915        out: &mut Vec<BraceSyntax>,
916    ) {
917        #[derive(Clone, Copy, PartialEq, Eq)]
918        enum QuoteState {
919            Single,
920            AnsiSingle,
921            Double,
922        }
923
924        #[derive(Clone, Copy)]
925        struct Candidate {
926            start: Position,
927            quote_context: BraceQuoteContext,
928            has_comma: bool,
929            has_dot_dot: bool,
930            saw_unquoted_whitespace: bool,
931            saw_quote_boundary: bool,
932            prev_char: Option<char>,
933        }
934
935        fn quote_state(context: BraceQuoteContext) -> Option<QuoteState> {
936            match context {
937                BraceQuoteContext::Unquoted => None,
938                BraceQuoteContext::SingleQuoted => Some(QuoteState::Single),
939                BraceQuoteContext::DoubleQuoted => Some(QuoteState::Double),
940            }
941        }
942
943        fn quote_context(state: Option<QuoteState>) -> BraceQuoteContext {
944            match state {
945                None => BraceQuoteContext::Unquoted,
946                Some(QuoteState::Single | QuoteState::AnsiSingle) => {
947                    BraceQuoteContext::SingleQuoted
948                }
949                Some(QuoteState::Double) => BraceQuoteContext::DoubleQuoted,
950            }
951        }
952
953        fn quote_context_is_active(context: BraceQuoteContext, state: Option<QuoteState>) -> bool {
954            match context {
955                BraceQuoteContext::Unquoted => state.is_none(),
956                BraceQuoteContext::SingleQuoted => {
957                    matches!(state, Some(QuoteState::Single | QuoteState::AnsiSingle))
958                }
959                BraceQuoteContext::DoubleQuoted => matches!(state, Some(QuoteState::Double)),
960            }
961        }
962
963        fn last_active_candidate_index(
964            stack: &[Candidate],
965            state: Option<QuoteState>,
966        ) -> Option<usize> {
967            stack
968                .iter()
969                .rposition(|candidate| quote_context_is_active(candidate.quote_context, state))
970        }
971
972        fn template_placeholder_end(
973            chars: &[(char, Position)],
974            start: usize,
975            quote_context: BraceQuoteContext,
976        ) -> Option<usize> {
977            if chars.get(start)?.0 != '{' || chars.get(start + 1)?.0 != '{' {
978                return None;
979            }
980
981            let mut index = start + 2;
982            let mut depth = 1usize;
983
984            while index < chars.len() {
985                if matches!(quote_context, BraceQuoteContext::Unquoted) && chars[index].0 == '\\' {
986                    index += 1;
987                    if index < chars.len() {
988                        index += 1;
989                    }
990                    continue;
991                }
992
993                if chars.get(index).is_some_and(|(ch, _)| *ch == '{')
994                    && chars.get(index + 1).is_some_and(|(ch, _)| *ch == '{')
995                {
996                    depth += 1;
997                    index += 2;
998                    continue;
999                }
1000
1001                if chars.get(index).is_some_and(|(ch, _)| *ch == '}')
1002                    && chars.get(index + 1).is_some_and(|(ch, _)| *ch == '}')
1003                {
1004                    depth -= 1;
1005                    index += 2;
1006                    if depth == 0 {
1007                        return Some(index);
1008                    }
1009                    continue;
1010                }
1011
1012                index += 1;
1013            }
1014
1015            None
1016        }
1017
1018        fn brace_span(start: Position, end: (char, Position)) -> Span {
1019            let mut end_position = end.1;
1020            end_position.advance(end.0);
1021            Span::from_positions(start, end_position)
1022        }
1023
1024        let mut index = 0usize;
1025        let mut quote_state = quote_state(initial_quote_context);
1026        let mut stack = Vec::<Candidate>::new();
1027
1028        while index < chars.len() {
1029            let ch = chars[index].0;
1030
1031            if matches!(
1032                quote_state,
1033                None | Some(QuoteState::AnsiSingle) | Some(QuoteState::Double)
1034            ) && ch == '\\'
1035            {
1036                if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state) {
1037                    stack[candidate_index].prev_char = None;
1038                }
1039                index += 1;
1040                if index < chars.len() {
1041                    index += 1;
1042                }
1043                continue;
1044            }
1045
1046            let current_quote_context = quote_context(quote_state);
1047            if ch == '{'
1048                && let Some(end_index) =
1049                    template_placeholder_end(chars, index, current_quote_context)
1050            {
1051                out.push(BraceSyntax {
1052                    kind: BraceSyntaxKind::TemplatePlaceholder,
1053                    span: brace_span(chars[index].1, chars[end_index - 1]),
1054                    quote_context: current_quote_context,
1055                });
1056                if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state) {
1057                    stack[candidate_index].prev_char = None;
1058                }
1059                index = end_index;
1060                continue;
1061            }
1062
1063            match quote_state {
1064                None => match ch {
1065                    '\'' => {
1066                        quote_state = if index > 0 && chars[index - 1].0 == '$' {
1067                            Some(QuoteState::AnsiSingle)
1068                        } else {
1069                            Some(QuoteState::Single)
1070                        };
1071                        for candidate in &mut stack {
1072                            if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
1073                                candidate.saw_quote_boundary = true;
1074                            }
1075                        }
1076                    }
1077                    '"' => {
1078                        quote_state = Some(QuoteState::Double);
1079                        for candidate in &mut stack {
1080                            if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
1081                                candidate.saw_quote_boundary = true;
1082                            }
1083                        }
1084                    }
1085                    _ => {}
1086                },
1087                Some(QuoteState::Single) => {
1088                    if ch == '\'' {
1089                        quote_state = None;
1090                        for candidate in &mut stack {
1091                            if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
1092                                candidate.saw_quote_boundary = true;
1093                            }
1094                        }
1095                    }
1096                }
1097                Some(QuoteState::AnsiSingle) => {
1098                    if ch == '\'' {
1099                        quote_state = None;
1100                        for candidate in &mut stack {
1101                            if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
1102                                candidate.saw_quote_boundary = true;
1103                            }
1104                        }
1105                    }
1106                }
1107                Some(QuoteState::Double) => {
1108                    if ch == '"' {
1109                        quote_state = None;
1110                        for candidate in &mut stack {
1111                            if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
1112                                candidate.saw_quote_boundary = true;
1113                            }
1114                        }
1115                    }
1116                }
1117            }
1118
1119            match ch {
1120                '{' => {
1121                    stack.push(Candidate {
1122                        start: chars[index].1,
1123                        quote_context: current_quote_context,
1124                        has_comma: false,
1125                        has_dot_dot: false,
1126                        saw_unquoted_whitespace: false,
1127                        saw_quote_boundary: false,
1128                        prev_char: Some(ch),
1129                    });
1130                    index += 1;
1131                    continue;
1132                }
1133                '}' => {
1134                    if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state)
1135                    {
1136                        let candidate = stack.remove(candidate_index);
1137                        let kind = if matches!(candidate.quote_context, BraceQuoteContext::Unquoted)
1138                            && candidate.saw_unquoted_whitespace
1139                        {
1140                            BraceSyntaxKind::Literal
1141                        } else if candidate.has_comma {
1142                            BraceSyntaxKind::Expansion(BraceExpansionKind::CommaList)
1143                        } else if candidate.has_dot_dot {
1144                            BraceSyntaxKind::Expansion(BraceExpansionKind::Sequence)
1145                        } else {
1146                            BraceSyntaxKind::Literal
1147                        };
1148                        if !matches!(kind, BraceSyntaxKind::Literal)
1149                            || !candidate.saw_quote_boundary
1150                        {
1151                            out.push(BraceSyntax {
1152                                kind,
1153                                span: brace_span(candidate.start, chars[index]),
1154                                quote_context: candidate.quote_context,
1155                            });
1156                        }
1157                    }
1158                }
1159                _ => {
1160                    if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state)
1161                    {
1162                        let candidate = &mut stack[candidate_index];
1163                        let counts_as_top_level =
1164                            quote_context_is_active(candidate.quote_context, quote_state);
1165
1166                        if counts_as_top_level {
1167                            match ch {
1168                                ',' => candidate.has_comma = true,
1169                                '.' if candidate.prev_char == Some('.') => {
1170                                    candidate.has_dot_dot = true;
1171                                }
1172                                c if matches!(
1173                                    candidate.quote_context,
1174                                    BraceQuoteContext::Unquoted
1175                                ) && quote_state.is_none()
1176                                    && c.is_whitespace() =>
1177                                {
1178                                    candidate.saw_unquoted_whitespace = true;
1179                                }
1180                                _ => {}
1181                            }
1182                        }
1183                    }
1184                }
1185            }
1186
1187            if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state) {
1188                stack[candidate_index].prev_char = Some(ch);
1189            }
1190
1191            index += 1;
1192        }
1193    }
1194
1195    fn collect_brace_syntax_from_pattern(
1196        &self,
1197        pattern: &Pattern,
1198        quote_context: BraceQuoteContext,
1199        out: &mut Vec<BraceSyntax>,
1200    ) {
1201        for (part, span) in pattern.parts_with_spans() {
1202            match part {
1203                PatternPart::Literal(text) => Self::scan_brace_syntax_text(
1204                    text.as_str(self.input, span),
1205                    span.start,
1206                    quote_context,
1207                    out,
1208                ),
1209                PatternPart::CharClass(text) => Self::scan_brace_syntax_text(
1210                    text.slice(self.input),
1211                    text.span().start,
1212                    quote_context,
1213                    out,
1214                ),
1215                PatternPart::Group { patterns, .. } => {
1216                    for pattern in patterns {
1217                        self.collect_brace_syntax_from_pattern(pattern, quote_context, out);
1218                    }
1219                }
1220                PatternPart::Word(word) => {
1221                    self.collect_brace_syntax_from_parts(&word.parts, quote_context, out)
1222                }
1223                PatternPart::AnyString | PatternPart::AnyChar => {}
1224            }
1225        }
1226    }
1227
1228    fn collect_brace_syntax_from_zsh_qualified_glob(
1229        &self,
1230        glob: &ZshQualifiedGlob,
1231        quote_context: BraceQuoteContext,
1232        out: &mut Vec<BraceSyntax>,
1233    ) {
1234        for segment in &glob.segments {
1235            if let ZshGlobSegment::Pattern(pattern) = segment {
1236                self.collect_brace_syntax_from_pattern(pattern, quote_context, out);
1237            }
1238        }
1239    }
1240
1241    fn scan_brace_syntax_text(
1242        text: &str,
1243        base: Position,
1244        quote_context: BraceQuoteContext,
1245        out: &mut Vec<BraceSyntax>,
1246    ) {
1247        if memchr(b'{', text.as_bytes()).is_none() {
1248            return;
1249        }
1250
1251        #[derive(Clone, Copy)]
1252        struct ScanFrame<'a> {
1253            text: &'a str,
1254            index: usize,
1255            position: Position,
1256        }
1257
1258        let mut work = SmallVec::<[ScanFrame<'_>; 2]>::new();
1259        work.push(ScanFrame {
1260            text,
1261            index: 0,
1262            position: base,
1263        });
1264
1265        while let Some(mut frame) = work.pop() {
1266            let bytes = frame.text.as_bytes();
1267
1268            while frame.index < bytes.len() {
1269                let next_special = if matches!(quote_context, BraceQuoteContext::Unquoted) {
1270                    memchr2(b'{', b'\\', &bytes[frame.index..])
1271                        .map(|relative| frame.index + relative)
1272                } else {
1273                    memchr(b'{', &bytes[frame.index..]).map(|relative| frame.index + relative)
1274                };
1275
1276                let Some(next_index) = next_special else {
1277                    break;
1278                };
1279
1280                if next_index > frame.index {
1281                    frame.position = frame
1282                        .position
1283                        .advanced_by(&frame.text[frame.index..next_index]);
1284                    frame.index = next_index;
1285                }
1286
1287                if matches!(quote_context, BraceQuoteContext::Unquoted)
1288                    && bytes[frame.index] == b'\\'
1289                {
1290                    let escaped_start = frame.index;
1291                    frame.index += 1;
1292                    if let Some(next) = frame.text[frame.index..].chars().next() {
1293                        frame.index += next.len_utf8();
1294                    }
1295                    frame.position = frame
1296                        .position
1297                        .advanced_by(&frame.text[escaped_start..frame.index]);
1298                    continue;
1299                }
1300
1301                let brace_start = frame.position;
1302                if let Some(len) =
1303                    Self::template_placeholder_len(frame.text, frame.index, quote_context)
1304                {
1305                    let brace_end =
1306                        brace_start.advanced_by(&frame.text[frame.index..frame.index + len]);
1307                    out.push(BraceSyntax {
1308                        kind: BraceSyntaxKind::TemplatePlaceholder,
1309                        span: Span::from_positions(brace_start, brace_end),
1310                        quote_context,
1311                    });
1312                    frame.position = brace_end;
1313                    frame.index += len;
1314                    continue;
1315                }
1316
1317                if let Some((len, kind)) =
1318                    Self::brace_construct_len(frame.text, frame.index, quote_context)
1319                {
1320                    let brace_end =
1321                        brace_start.advanced_by(&frame.text[frame.index..frame.index + len]);
1322                    out.push(BraceSyntax {
1323                        kind,
1324                        span: Span::from_positions(brace_start, brace_end),
1325                        quote_context,
1326                    });
1327
1328                    frame.position = brace_end;
1329                    frame.index += len;
1330
1331                    if len > 2 {
1332                        let inner_start = frame.index - len + '{'.len_utf8();
1333                        let inner_end = frame.index - '}'.len_utf8();
1334                        if inner_start < inner_end {
1335                            let inner_base = brace_start.advanced_by("{");
1336                            work.push(frame);
1337                            work.push(ScanFrame {
1338                                text: &frame.text[inner_start..inner_end],
1339                                index: 0,
1340                                position: inner_base,
1341                            });
1342                            break;
1343                        }
1344                    }
1345
1346                    continue;
1347                }
1348
1349                frame.position.advance('{');
1350                frame.index += '{'.len_utf8();
1351            }
1352        }
1353    }
1354
1355    fn text_position(base: Position, text: &str, offset: usize) -> Position {
1356        base.advanced_by(&text[..offset])
1357    }
1358
1359    fn template_placeholder_len(
1360        text: &str,
1361        start: usize,
1362        quote_context: BraceQuoteContext,
1363    ) -> Option<usize> {
1364        text.get(start..).filter(|rest| rest.starts_with("{{"))?;
1365
1366        let mut index = start + "{{".len();
1367        let mut depth = 1usize;
1368
1369        while index < text.len() {
1370            if matches!(quote_context, BraceQuoteContext::Unquoted)
1371                && text[index..].starts_with('\\')
1372            {
1373                index += 1;
1374                if let Some(next) = text[index..].chars().next() {
1375                    index += next.len_utf8();
1376                }
1377                continue;
1378            }
1379
1380            if text[index..].starts_with("{{") {
1381                depth += 1;
1382                index += "{{".len();
1383                continue;
1384            }
1385
1386            if text[index..].starts_with("}}") {
1387                depth -= 1;
1388                index += "}}".len();
1389                if depth == 0 {
1390                    return Some(index - start);
1391                }
1392                continue;
1393            }
1394
1395            index += text[index..].chars().next()?.len_utf8();
1396        }
1397
1398        None
1399    }
1400
1401    fn brace_construct_len(
1402        text: &str,
1403        start: usize,
1404        quote_context: BraceQuoteContext,
1405    ) -> Option<(usize, BraceSyntaxKind)> {
1406        text.get(start..).filter(|rest| rest.starts_with('{'))?;
1407
1408        #[derive(Clone, Copy, PartialEq, Eq)]
1409        enum QuoteState {
1410            Single,
1411            Double,
1412        }
1413
1414        let mut index = start + '{'.len_utf8();
1415        let mut depth = 1usize;
1416        let mut has_comma = false;
1417        let mut has_dot_dot = false;
1418        let mut saw_unquoted_whitespace = false;
1419        let mut prev_char = None;
1420        let mut quote_state = None;
1421
1422        while index < text.len() {
1423            if matches!(quote_context, BraceQuoteContext::Unquoted)
1424                && quote_state.is_none()
1425                && text[index..].starts_with('\\')
1426            {
1427                index += 1;
1428                if let Some(next) = text[index..].chars().next() {
1429                    index += next.len_utf8();
1430                }
1431                prev_char = None;
1432                continue;
1433            }
1434
1435            let ch = text[index..].chars().next()?;
1436            index += ch.len_utf8();
1437
1438            if matches!(quote_context, BraceQuoteContext::Unquoted) {
1439                match quote_state {
1440                    None => match ch {
1441                        '\'' => {
1442                            quote_state = Some(QuoteState::Single);
1443                            prev_char = None;
1444                            continue;
1445                        }
1446                        '"' => {
1447                            quote_state = Some(QuoteState::Double);
1448                            prev_char = None;
1449                            continue;
1450                        }
1451                        '{' => depth += 1,
1452                        '}' => {
1453                            depth -= 1;
1454                            if depth == 0 {
1455                                let kind = if saw_unquoted_whitespace {
1456                                    BraceSyntaxKind::Literal
1457                                } else if has_comma {
1458                                    BraceSyntaxKind::Expansion(BraceExpansionKind::CommaList)
1459                                } else if has_dot_dot {
1460                                    BraceSyntaxKind::Expansion(BraceExpansionKind::Sequence)
1461                                } else {
1462                                    BraceSyntaxKind::Literal
1463                                };
1464                                return Some((index - start, kind));
1465                            }
1466                        }
1467                        ',' if depth == 1 => has_comma = true,
1468                        '.' if depth == 1 && prev_char == Some('.') => has_dot_dot = true,
1469                        c if c.is_whitespace() => saw_unquoted_whitespace = true,
1470                        _ => {}
1471                    },
1472                    Some(QuoteState::Single) => {
1473                        if ch == '\'' {
1474                            quote_state = None;
1475                        }
1476                    }
1477                    Some(QuoteState::Double) => match ch {
1478                        '\\' => {
1479                            if let Some(next) = text[index..].chars().next() {
1480                                index += next.len_utf8();
1481                            }
1482                            prev_char = None;
1483                            continue;
1484                        }
1485                        '"' => quote_state = None,
1486                        _ => {}
1487                    },
1488                }
1489            } else {
1490                match ch {
1491                    '{' => depth += 1,
1492                    '}' => {
1493                        depth -= 1;
1494                        if depth == 0 {
1495                            let kind = if has_comma {
1496                                BraceSyntaxKind::Expansion(BraceExpansionKind::CommaList)
1497                            } else if has_dot_dot {
1498                                BraceSyntaxKind::Expansion(BraceExpansionKind::Sequence)
1499                            } else {
1500                                BraceSyntaxKind::Literal
1501                            };
1502                            return Some((index - start, kind));
1503                        }
1504                    }
1505                    ',' if depth == 1 => has_comma = true,
1506                    '.' if depth == 1 && prev_char == Some('.') => has_dot_dot = true,
1507                    _ => {}
1508                }
1509            }
1510
1511            prev_char = Some(ch);
1512        }
1513
1514        None
1515    }
1516
1517    fn maybe_parse_zsh_qualified_glob_word(
1518        &mut self,
1519        text: &str,
1520        span: Span,
1521        source_backed: bool,
1522    ) -> Option<Word> {
1523        if !self.zsh_glob_qualifiers_enabled_at(span.start.offset)
1524            || text.is_empty()
1525            || text.contains('=')
1526            || text.contains(['\x00', '\\', '\'', '"', '$', '`'])
1527            || text.chars().any(char::is_whitespace)
1528        {
1529            return None;
1530        }
1531
1532        let (segments, qualifiers) =
1533            self.parse_zsh_qualified_glob_segments(text, span, source_backed)?;
1534        if !segments.iter().any(|segment| {
1535            matches!(
1536                segment,
1537                ZshGlobSegment::Pattern(pattern) if Self::pattern_has_glob_syntax(pattern)
1538            )
1539        }) {
1540            return None;
1541        }
1542
1543        Some(self.word_with_parts(
1544            vec![WordPartNode::new(
1545                WordPart::ZshQualifiedGlob(ZshQualifiedGlob {
1546                    span,
1547                    segments,
1548                    qualifiers,
1549                }),
1550                span,
1551            )],
1552            span,
1553        ))
1554    }
1555
1556    fn parse_zsh_qualified_glob_segments(
1557        &mut self,
1558        text: &str,
1559        span: Span,
1560        source_backed: bool,
1561    ) -> Option<(Vec<ZshGlobSegment>, Option<ZshGlobQualifierGroup>)> {
1562        let mut segments = Vec::new();
1563        let mut qualifiers = None;
1564        let mut pattern_start = 0usize;
1565        let mut index = 0usize;
1566
1567        while index < text.len() {
1568            if text[index..].starts_with("(#") {
1569                if let Some((len, control)) =
1570                    self.parse_zsh_inline_glob_control(text, span.start, index)
1571                {
1572                    self.push_zsh_pattern_segment(
1573                        &mut segments,
1574                        text,
1575                        span.start,
1576                        pattern_start,
1577                        index,
1578                        source_backed,
1579                    );
1580                    segments.push(ZshGlobSegment::InlineControl(control));
1581                    index += len;
1582                    pattern_start = index;
1583                    continue;
1584                }
1585
1586                let suffix_start = Self::text_position(span.start, text, index);
1587                if let Some(group) = self.parse_zsh_terminal_glob_qualifier_group(
1588                    &text[index..],
1589                    suffix_start,
1590                    source_backed,
1591                ) {
1592                    self.push_zsh_pattern_segment(
1593                        &mut segments,
1594                        text,
1595                        span.start,
1596                        pattern_start,
1597                        index,
1598                        source_backed,
1599                    );
1600                    qualifiers = Some(group);
1601                    index = text.len();
1602                    pattern_start = index;
1603                    break;
1604                }
1605
1606                return None;
1607            }
1608
1609            if text[index..].starts_with('(') {
1610                let suffix_start = Self::text_position(span.start, text, index);
1611                if let Some(group) = self.parse_zsh_terminal_glob_qualifier_group(
1612                    &text[index..],
1613                    suffix_start,
1614                    source_backed,
1615                ) && matches!(group.kind, ZshGlobQualifierKind::Classic)
1616                {
1617                    self.push_zsh_pattern_segment(
1618                        &mut segments,
1619                        text,
1620                        span.start,
1621                        pattern_start,
1622                        index,
1623                        source_backed,
1624                    );
1625                    qualifiers = Some(group);
1626                    index = text.len();
1627                    pattern_start = index;
1628                    break;
1629                }
1630            }
1631
1632            index += text[index..].chars().next()?.len_utf8();
1633        }
1634
1635        self.push_zsh_pattern_segment(
1636            &mut segments,
1637            text,
1638            span.start,
1639            pattern_start,
1640            text.len(),
1641            source_backed,
1642        );
1643
1644        segments
1645            .iter()
1646            .any(|segment| matches!(segment, ZshGlobSegment::Pattern(_)))
1647            .then_some((segments, qualifiers))
1648    }
1649
1650    fn push_zsh_pattern_segment(
1651        &mut self,
1652        segments: &mut Vec<ZshGlobSegment>,
1653        text: &str,
1654        base: Position,
1655        start: usize,
1656        end: usize,
1657        source_backed: bool,
1658    ) {
1659        if start >= end {
1660            return;
1661        }
1662
1663        let start_position = Self::text_position(base, text, start);
1664        let end_position = Self::text_position(base, text, end);
1665        let span = Span::from_positions(start_position, end_position);
1666        let pattern_word =
1667            self.decode_word_text(&text[start..end], span, span.start, source_backed);
1668        segments.push(ZshGlobSegment::Pattern(
1669            self.pattern_from_word(&pattern_word),
1670        ));
1671    }
1672
1673    fn parse_zsh_inline_glob_control(
1674        &self,
1675        text: &str,
1676        base: Position,
1677        start: usize,
1678    ) -> Option<(usize, ZshInlineGlobControl)> {
1679        let (len, control) = if text[start..].starts_with("(#i)") {
1680            (
1681                "(#i)".len(),
1682                ZshInlineGlobControl::CaseInsensitive {
1683                    span: Span::from_positions(
1684                        Self::text_position(base, text, start),
1685                        Self::text_position(base, text, start + "(#i)".len()),
1686                    ),
1687                },
1688            )
1689        } else if text[start..].starts_with("(#b)") {
1690            (
1691                "(#b)".len(),
1692                ZshInlineGlobControl::Backreferences {
1693                    span: Span::from_positions(
1694                        Self::text_position(base, text, start),
1695                        Self::text_position(base, text, start + "(#b)".len()),
1696                    ),
1697                },
1698            )
1699        } else if text[start..].starts_with("(#s)") {
1700            (
1701                "(#s)".len(),
1702                ZshInlineGlobControl::StartAnchor {
1703                    span: Span::from_positions(
1704                        Self::text_position(base, text, start),
1705                        Self::text_position(base, text, start + "(#s)".len()),
1706                    ),
1707                },
1708            )
1709        } else if text[start..].starts_with("(#e)") {
1710            (
1711                "(#e)".len(),
1712                ZshInlineGlobControl::EndAnchor {
1713                    span: Span::from_positions(
1714                        Self::text_position(base, text, start),
1715                        Self::text_position(base, text, start + "(#e)".len()),
1716                    ),
1717                },
1718            )
1719        } else {
1720            return None;
1721        };
1722
1723        Some((len, control))
1724    }
1725
1726    fn parse_zsh_terminal_glob_qualifier_group(
1727        &self,
1728        text: &str,
1729        base: Position,
1730        source_backed: bool,
1731    ) -> Option<ZshGlobQualifierGroup> {
1732        let (kind, prefix_len, inner) = if let Some(inner) = text
1733            .strip_prefix("(#q")
1734            .and_then(|rest| rest.strip_suffix(')'))
1735        {
1736            (ZshGlobQualifierKind::HashQ, "(#q".len(), inner)
1737        } else {
1738            let inner = text.strip_prefix('(')?.strip_suffix(')')?;
1739            (ZshGlobQualifierKind::Classic, "(".len(), inner)
1740        };
1741
1742        let fragments = self.parse_zsh_glob_qualifier_fragments(
1743            inner,
1744            Self::text_position(base, text, prefix_len),
1745            source_backed,
1746        )?;
1747
1748        Some(ZshGlobQualifierGroup {
1749            span: Span::from_positions(base, Self::text_position(base, text, text.len())),
1750            kind,
1751            fragments,
1752        })
1753    }
1754
1755    fn parse_zsh_glob_qualifier_fragments(
1756        &self,
1757        text: &str,
1758        base: Position,
1759        source_backed: bool,
1760    ) -> Option<Vec<ZshGlobQualifier>> {
1761        let mut fragments = Vec::new();
1762        let mut index = 0;
1763        let mut saw_non_letter_fragment = false;
1764
1765        while index < text.len() {
1766            let start = index;
1767            let ch = text[index..].chars().next()?;
1768
1769            match ch {
1770                '^' => {
1771                    index += ch.len_utf8();
1772                    fragments.push(ZshGlobQualifier::Negation {
1773                        span: Span::from_positions(
1774                            Self::text_position(base, text, start),
1775                            Self::text_position(base, text, index),
1776                        ),
1777                    });
1778                    saw_non_letter_fragment = true;
1779                }
1780                '.' | '/' | '-' | 'A'..='Z' => {
1781                    index += ch.len_utf8();
1782                    fragments.push(ZshGlobQualifier::Flag {
1783                        name: ch,
1784                        span: Span::from_positions(
1785                            Self::text_position(base, text, start),
1786                            Self::text_position(base, text, index),
1787                        ),
1788                    });
1789                    saw_non_letter_fragment = true;
1790                }
1791                '[' => {
1792                    index += ch.len_utf8();
1793                    let number_start = index;
1794                    while matches!(text[index..].chars().next(), Some('0'..='9')) {
1795                        index += 1;
1796                    }
1797                    if number_start == index {
1798                        return None;
1799                    }
1800
1801                    let start_text = self.zsh_glob_qualifier_source_text(
1802                        text,
1803                        base,
1804                        number_start,
1805                        index,
1806                        source_backed,
1807                    );
1808                    let end_text = if text[index..].starts_with(',') {
1809                        index += 1;
1810                        let range_start = index;
1811                        while matches!(text[index..].chars().next(), Some('0'..='9')) {
1812                            index += 1;
1813                        }
1814                        if range_start == index {
1815                            return None;
1816                        }
1817                        Some(self.zsh_glob_qualifier_source_text(
1818                            text,
1819                            base,
1820                            range_start,
1821                            index,
1822                            source_backed,
1823                        ))
1824                    } else {
1825                        None
1826                    };
1827
1828                    if !text[index..].starts_with(']') {
1829                        return None;
1830                    }
1831                    index += "]".len();
1832                    fragments.push(ZshGlobQualifier::NumericArgument {
1833                        span: Span::from_positions(
1834                            Self::text_position(base, text, start),
1835                            Self::text_position(base, text, index),
1836                        ),
1837                        start: start_text,
1838                        end: end_text,
1839                    });
1840                    saw_non_letter_fragment = true;
1841                }
1842                'a'..='z' => {
1843                    index += ch.len_utf8();
1844                    while matches!(text[index..].chars().next(), Some('a'..='z' | 'A'..='Z')) {
1845                        index += 1;
1846                    }
1847                    if index - start <= 1 {
1848                        return None;
1849                    }
1850                    fragments.push(ZshGlobQualifier::LetterSequence {
1851                        text: self.zsh_glob_qualifier_source_text(
1852                            text,
1853                            base,
1854                            start,
1855                            index,
1856                            source_backed,
1857                        ),
1858                        span: Span::from_positions(
1859                            Self::text_position(base, text, start),
1860                            Self::text_position(base, text, index),
1861                        ),
1862                    });
1863                }
1864                _ => return None,
1865            }
1866        }
1867
1868        (!fragments.is_empty() && saw_non_letter_fragment).then_some(fragments)
1869    }
1870
1871    fn zsh_glob_qualifier_source_text(
1872        &self,
1873        text: &str,
1874        base: Position,
1875        start: usize,
1876        end: usize,
1877        source_backed: bool,
1878    ) -> SourceText {
1879        let span = Span::from_positions(
1880            Self::text_position(base, text, start),
1881            Self::text_position(base, text, end),
1882        );
1883        if source_backed {
1884            SourceText::source(span)
1885        } else {
1886            SourceText::cooked(span, text[start..end].to_string())
1887        }
1888    }
1889
1890    fn pattern_has_glob_syntax(pattern: &Pattern) -> bool {
1891        pattern.parts.iter().any(|part| match &part.kind {
1892            PatternPart::AnyString | PatternPart::AnyChar | PatternPart::CharClass(_) => true,
1893            PatternPart::Group { .. } => true,
1894            PatternPart::Word(word) => Self::pattern_has_glob_word(word),
1895            PatternPart::Literal(_) => false,
1896        })
1897    }
1898
1899    fn pattern_has_glob_word(word: &Word) -> bool {
1900        word.parts
1901            .iter()
1902            .any(|part| !matches!(part.kind, WordPart::Literal(_)))
1903    }
1904
1905    fn simple_word_from_token(&mut self, token: &LexedToken<'_>, span: Span) -> Option<Word> {
1906        let word = token.word()?;
1907        let source_backed = !token.flags.is_synthetic();
1908
1909        if self.zsh_glob_qualifiers_enabled_at(span.start.offset)
1910            && let Some(segment) = word.single_segment()
1911            && segment.kind() == LexedWordSegmentKind::Plain
1912            && let Some(word) = self.maybe_parse_zsh_qualified_glob_word(
1913                segment.as_str(),
1914                span,
1915                segment.span().is_some() && source_backed && segment.text_is_source_backed(),
1916            )
1917        {
1918            return Some(word);
1919        }
1920        let mut parts = Self::word_part_buffer_with_capacity(word.segments().size_hint().0);
1921
1922        for segment in word.segments() {
1923            let source_backed = segment.span().is_some() && !token.flags.is_synthetic();
1924            let content_span = Self::segment_content_span(segment, span);
1925            let raw_text = segment.as_str();
1926            let use_source_slice = source_backed
1927                && match segment.kind() {
1928                    LexedWordSegmentKind::Plain => {
1929                        segment.text_is_source_backed()
1930                            || raw_text.contains("${") && raw_text.contains('/')
1931                            || !raw_text.contains("$(")
1932                    }
1933                    _ => segment.text_is_source_backed(),
1934                };
1935            let text = if use_source_slice {
1936                content_span.slice(self.input)
1937            } else {
1938                raw_text
1939            };
1940            match segment.kind() {
1941                LexedWordSegmentKind::Plain
1942                | LexedWordSegmentKind::DoubleQuoted
1943                | LexedWordSegmentKind::DollarDoubleQuoted
1944                    if Self::word_text_needs_parse(text) =>
1945                {
1946                    return None;
1947                }
1948                LexedWordSegmentKind::Plain
1949                | LexedWordSegmentKind::SingleQuoted
1950                | LexedWordSegmentKind::DollarSingleQuoted
1951                | LexedWordSegmentKind::DoubleQuoted
1952                | LexedWordSegmentKind::DollarDoubleQuoted => {}
1953                LexedWordSegmentKind::Composite => return None,
1954            }
1955
1956            let wrapper_span = Self::segment_wrapper_span(segment, span);
1957            let part = match segment.kind() {
1958                LexedWordSegmentKind::Plain => {
1959                    self.literal_part_from_text(text, content_span, source_backed)
1960                }
1961                LexedWordSegmentKind::SingleQuoted => {
1962                    self.single_quoted_part_from_text(text, content_span, wrapper_span, false)
1963                }
1964                LexedWordSegmentKind::DollarSingleQuoted => {
1965                    self.single_quoted_part_from_text(text, content_span, wrapper_span, true)
1966                }
1967                LexedWordSegmentKind::DoubleQuoted => self.double_quoted_literal_part_from_text(
1968                    text,
1969                    content_span,
1970                    wrapper_span,
1971                    source_backed,
1972                    false,
1973                ),
1974                LexedWordSegmentKind::DollarDoubleQuoted => self
1975                    .double_quoted_literal_part_from_text(
1976                        text,
1977                        content_span,
1978                        wrapper_span,
1979                        source_backed,
1980                        true,
1981                    ),
1982                LexedWordSegmentKind::Composite => unreachable!(),
1983            };
1984            Self::push_word_part_node(&mut parts, part);
1985        }
1986
1987        Some(self.word_with_part_buffer(parts, span))
1988    }
1989
1990    fn segment_content_span(segment: &LexedWordSegment<'_>, fallback: Span) -> Span {
1991        segment
1992            .span()
1993            .or_else(|| segment.wrapper_span())
1994            .unwrap_or(fallback)
1995    }
1996
1997    fn segment_wrapper_span(segment: &LexedWordSegment<'_>, fallback: Span) -> Span {
1998        segment
1999            .wrapper_span()
2000            .or_else(|| segment.span())
2001            .unwrap_or(fallback)
2002    }
2003
2004    fn literal_part_from_text(&self, text: &str, span: Span, source_backed: bool) -> WordPartNode {
2005        WordPartNode::new(
2006            WordPart::Literal(self.literal_text_from_str(
2007                text,
2008                span.start,
2009                span.end,
2010                source_backed,
2011            )),
2012            span,
2013        )
2014    }
2015
2016    fn single_quoted_part_from_text(
2017        &self,
2018        text: &str,
2019        content_span: Span,
2020        wrapper_span: Span,
2021        dollar: bool,
2022    ) -> WordPartNode {
2023        WordPartNode::new(
2024            WordPart::SingleQuoted {
2025                value: self.source_text_from_str(text, content_span.start, content_span.end),
2026                dollar,
2027            },
2028            wrapper_span,
2029        )
2030    }
2031
2032    fn double_quoted_literal_part_from_text(
2033        &self,
2034        text: &str,
2035        content_span: Span,
2036        wrapper_span: Span,
2037        source_backed: bool,
2038        dollar: bool,
2039    ) -> WordPartNode {
2040        WordPartNode::new(
2041            WordPart::DoubleQuoted {
2042                parts: vec![self.literal_part_from_text(text, content_span, source_backed)],
2043                dollar,
2044            },
2045            wrapper_span,
2046        )
2047    }
2048
2049    fn decode_word_from_token(&mut self, token: &LexedToken<'_>, span: Span) -> Option<Word> {
2050        let word = token.word()?;
2051
2052        if word.single_segment().is_none()
2053            && !token.flags.is_synthetic()
2054            && let Some(source_text) = token.source_slice(self.input)
2055        {
2056            return Some(self.parse_word_with_context(source_text, span, span.start, true));
2057        }
2058
2059        if let Some(segment) = word.single_segment() {
2060            let content_span = Self::segment_content_span(segment, span);
2061            let wrapper_span = Self::segment_wrapper_span(segment, span);
2062            let source_backed = segment.span().is_some() && !token.flags.is_synthetic();
2063            let raw_text = segment.as_str();
2064            let use_source_slice = source_backed
2065                && match segment.kind() {
2066                    LexedWordSegmentKind::Plain => {
2067                        segment.text_is_source_backed()
2068                            || raw_text.contains("${") && raw_text.contains('/')
2069                            || !raw_text.contains("$(")
2070                    }
2071                    _ => segment.text_is_source_backed(),
2072                };
2073            let text = if use_source_slice {
2074                content_span.slice(self.input)
2075            } else {
2076                raw_text
2077            };
2078            let decode_text = if source_backed
2079                && !self.source_matches(content_span, text)
2080                && matches!(
2081                    segment.kind(),
2082                    LexedWordSegmentKind::DoubleQuoted | LexedWordSegmentKind::DollarDoubleQuoted
2083                )
2084                && (!text.contains("$(") || text.contains("$(("))
2085            {
2086                content_span.slice(self.input)
2087            } else {
2088                text
2089            };
2090            let preserve_escaped_expansion_literals =
2091                source_backed && self.source_matches(content_span, decode_text);
2092
2093            return match segment.kind() {
2094                LexedWordSegmentKind::SingleQuoted => Some(self.word_with_single_part(
2095                    self.single_quoted_part_from_text(text, content_span, wrapper_span, false),
2096                    span,
2097                )),
2098                LexedWordSegmentKind::DollarSingleQuoted => Some(self.word_with_single_part(
2099                    self.single_quoted_part_from_text(text, content_span, wrapper_span, true),
2100                    span,
2101                )),
2102                LexedWordSegmentKind::Plain if Self::word_text_needs_parse(text) => Some(
2103                    self.decode_word_text_preserving_quotes_if_needed_with_escape_mode(
2104                        text,
2105                        span,
2106                        content_span.start,
2107                        source_backed,
2108                        preserve_escaped_expansion_literals,
2109                    ),
2110                ),
2111                LexedWordSegmentKind::DoubleQuoted | LexedWordSegmentKind::DollarDoubleQuoted
2112                    if Self::word_text_needs_parse(text) =>
2113                {
2114                    let inner = self.decode_quoted_segment_text(
2115                        decode_text,
2116                        content_span,
2117                        content_span.start,
2118                        source_backed,
2119                    );
2120                    Some(self.word_with_single_part(
2121                        WordPartNode::new(
2122                            WordPart::DoubleQuoted {
2123                                parts: inner.parts,
2124                                dollar: matches!(
2125                                    segment.kind(),
2126                                    LexedWordSegmentKind::DollarDoubleQuoted
2127                                ),
2128                            },
2129                            wrapper_span,
2130                        ),
2131                        span,
2132                    ))
2133                }
2134                LexedWordSegmentKind::Plain => Some(self.word_with_single_part(
2135                    self.literal_part_from_text(text, content_span, source_backed),
2136                    span,
2137                )),
2138                LexedWordSegmentKind::DoubleQuoted => Some(self.word_with_single_part(
2139                    self.double_quoted_literal_part_from_text(
2140                        text,
2141                        content_span,
2142                        wrapper_span,
2143                        source_backed,
2144                        false,
2145                    ),
2146                    span,
2147                )),
2148                LexedWordSegmentKind::DollarDoubleQuoted => Some(self.word_with_single_part(
2149                    self.double_quoted_literal_part_from_text(
2150                        text,
2151                        content_span,
2152                        wrapper_span,
2153                        source_backed,
2154                        true,
2155                    ),
2156                    span,
2157                )),
2158                LexedWordSegmentKind::Composite => None,
2159            };
2160        }
2161
2162        let mut parts = Self::word_part_buffer_with_capacity(word.segments().size_hint().0);
2163        let mut cursor = span.start;
2164
2165        for segment in word.segments() {
2166            let raw_text = segment.as_str();
2167            let content_span = if let Some(segment_span) = segment.span() {
2168                cursor = segment_span.end;
2169                segment_span
2170            } else {
2171                let start = cursor;
2172                let end = start.advanced_by(raw_text);
2173                cursor = end;
2174                Span::from_positions(start, end)
2175            };
2176            let wrapper_span = segment.wrapper_span().unwrap_or(content_span);
2177            let source_backed = segment.span().is_some() && !token.flags.is_synthetic();
2178            let use_source_slice = source_backed
2179                && match segment.kind() {
2180                    LexedWordSegmentKind::Plain => {
2181                        segment.text_is_source_backed()
2182                            || raw_text.contains("${") && raw_text.contains('/')
2183                            || !raw_text.contains("$(")
2184                    }
2185                    _ => segment.text_is_source_backed(),
2186                };
2187            let text = if use_source_slice {
2188                content_span.slice(self.input)
2189            } else {
2190                raw_text
2191            };
2192            let preserve_escaped_expansion_literals = source_backed;
2193
2194            match segment.kind() {
2195                LexedWordSegmentKind::SingleQuoted => Self::push_word_part_node(
2196                    &mut parts,
2197                    self.single_quoted_part_from_text(text, content_span, wrapper_span, false),
2198                ),
2199                LexedWordSegmentKind::DollarSingleQuoted => Self::push_word_part_node(
2200                    &mut parts,
2201                    self.single_quoted_part_from_text(text, content_span, wrapper_span, true),
2202                ),
2203                LexedWordSegmentKind::Plain => {
2204                    if Self::word_text_needs_parse(text) {
2205                        parts.extend(
2206                            self.decode_word_text_preserving_quotes_if_needed_with_escape_mode(
2207                                text,
2208                                content_span,
2209                                content_span.start,
2210                                source_backed,
2211                                preserve_escaped_expansion_literals,
2212                            )
2213                            .parts,
2214                        );
2215                    } else {
2216                        Self::push_word_part_node(
2217                            &mut parts,
2218                            self.literal_part_from_text(text, content_span, source_backed),
2219                        );
2220                    }
2221                }
2222                LexedWordSegmentKind::DoubleQuoted | LexedWordSegmentKind::DollarDoubleQuoted => {
2223                    if Self::word_text_needs_parse(text) {
2224                        let inner = self.decode_quoted_segment_text(
2225                            text,
2226                            content_span,
2227                            content_span.start,
2228                            source_backed,
2229                        );
2230                        Self::push_word_part_node(
2231                            &mut parts,
2232                            WordPartNode::new(
2233                                WordPart::DoubleQuoted {
2234                                    parts: inner.parts,
2235                                    dollar: matches!(
2236                                        segment.kind(),
2237                                        LexedWordSegmentKind::DollarDoubleQuoted
2238                                    ),
2239                                },
2240                                wrapper_span,
2241                            ),
2242                        );
2243                    } else {
2244                        Self::push_word_part_node(
2245                            &mut parts,
2246                            self.double_quoted_literal_part_from_text(
2247                                text,
2248                                content_span,
2249                                wrapper_span,
2250                                source_backed,
2251                                matches!(segment.kind(), LexedWordSegmentKind::DollarDoubleQuoted),
2252                            ),
2253                        );
2254                    }
2255                }
2256                LexedWordSegmentKind::Composite => return None,
2257            }
2258        }
2259
2260        Some(self.word_with_part_buffer(parts, span))
2261    }
2262
2263    fn current_word_ref(&mut self) -> Option<&Word> {
2264        if self.current_word_cache.is_none() {
2265            self.current_word_cache = self.current_word();
2266        }
2267
2268        self.current_word_cache.as_ref()
2269    }
2270
2271    fn current_word(&mut self) -> Option<Word> {
2272        if let Some(word) = self.current_word_cache.as_ref() {
2273            return Some(word.clone());
2274        }
2275
2276        if let Some(word) = self.current_zsh_glob_word_from_source() {
2277            self.current_word_cache = Some(word.clone());
2278            return Some(word);
2279        }
2280
2281        let span = self.current_span;
2282        if let Some(token) = self.current_token.clone()
2283            && let Some(word) = self.simple_word_from_token(&token, span)
2284        {
2285            return Some(word);
2286        }
2287
2288        let token = self.current_token.take()?;
2289        let word = self.decode_word_from_token(&token, span);
2290        self.current_token = Some(token);
2291        if let Some(word) = word.as_ref() {
2292            self.current_word_cache = Some(word.clone());
2293        }
2294        word
2295    }
2296
2297    fn take_current_word(&mut self) -> Option<Word> {
2298        if let Some(word) = self.current_word_cache.take() {
2299            return Some(word);
2300        }
2301
2302        if let Some(word) = self.current_zsh_glob_word_from_source() {
2303            return Some(word);
2304        }
2305
2306        let span = self.current_span;
2307        if let Some(token) = self.current_token.clone()
2308            && let Some(word) = self.simple_word_from_token(&token, span)
2309        {
2310            return Some(word);
2311        }
2312
2313        let token = self.current_token.take()?;
2314        let word = self.decode_word_from_token(&token, span);
2315        self.current_token = Some(token);
2316        word
2317    }
2318
2319    fn take_current_word_and_advance(&mut self) -> Option<Word> {
2320        let word = self.take_current_word()?;
2321        self.advance_past_word(&word);
2322        Some(word)
2323    }
2324
2325    fn current_zsh_glob_word_from_source(&mut self) -> Option<Word> {
2326        if !matches!(
2327            self.current_token_kind,
2328            Some(TokenKind::LeftParen | TokenKind::Word)
2329        ) {
2330            return None;
2331        }
2332
2333        let start = self.current_span.start;
2334        if !self.source_word_contains_zsh_glob_control(start) {
2335            return None;
2336        }
2337        let (text, end) = self.scan_source_word(start)?;
2338        if !text.contains("(#") {
2339            return None;
2340        }
2341        let span = Span::from_positions(start, end);
2342        if self.zsh_glob_qualifiers_enabled_at(span.start.offset)
2343            && let Some(word) = self.maybe_parse_zsh_qualified_glob_word(&text, span, true)
2344        {
2345            return Some(word);
2346        }
2347
2348        Some(self.parse_word_with_context(&text, span, start, true))
2349    }
2350
2351    fn source_word_contains_zsh_glob_control(&self, start: Position) -> bool {
2352        if start.offset >= self.input.len() {
2353            return false;
2354        }
2355
2356        let source = &self.input[start.offset..];
2357        let mut chars = source.chars().peekable();
2358        let mut cursor = start;
2359        let mut paren_depth = 0_i32;
2360        let mut brace_depth = 0_i32;
2361        let mut in_single = false;
2362        let mut in_double = false;
2363        let mut in_backtick = false;
2364        let mut escaped = false;
2365        let mut prev_char = None;
2366
2367        while let Some(&ch) = chars.peek() {
2368            if !in_single
2369                && !in_double
2370                && !in_backtick
2371                && paren_depth == 0
2372                && brace_depth == 0
2373                && matches!(ch, ' ' | '\t' | '\n' | ';' | '|' | '&' | '>' | '<' | ')')
2374            {
2375                break;
2376            }
2377
2378            let ch = Self::next_word_char_unwrap(&mut chars, &mut cursor);
2379            if !in_single && !in_double && !in_backtick && prev_char == Some('(') && ch == '#' {
2380                return true;
2381            }
2382
2383            if escaped {
2384                escaped = false;
2385                prev_char = Some(ch);
2386                continue;
2387            }
2388
2389            match ch {
2390                '\\' if !in_single => escaped = true,
2391                '\'' if !in_double => in_single = !in_single,
2392                '"' if !in_single => in_double = !in_double,
2393                '`' if !in_single => in_backtick = !in_backtick,
2394                '(' if !in_single && !in_double => paren_depth += 1,
2395                ')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
2396                '{' if !in_single && !in_double => brace_depth += 1,
2397                '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
2398                _ => {}
2399            }
2400
2401            prev_char = Some(ch);
2402        }
2403
2404        false
2405    }
2406
2407    fn scan_source_word(&self, start: Position) -> Option<(String, Position)> {
2408        if start.offset >= self.input.len() {
2409            return None;
2410        }
2411
2412        let source = &self.input[start.offset..];
2413        let mut chars = source.chars().peekable();
2414        let mut cursor = start;
2415        let mut text = String::new();
2416        let mut paren_depth = 0_i32;
2417        let mut brace_depth = 0_i32;
2418        let mut in_single = false;
2419        let mut in_double = false;
2420        let mut in_backtick = false;
2421        let mut escaped = false;
2422
2423        while let Some(&ch) = chars.peek() {
2424            if !in_single
2425                && !in_double
2426                && !in_backtick
2427                && paren_depth == 0
2428                && brace_depth == 0
2429                && matches!(ch, ' ' | '\t' | '\n' | ';' | '|' | '&' | '>' | '<' | ')')
2430            {
2431                break;
2432            }
2433
2434            let ch = Self::next_word_char_unwrap(&mut chars, &mut cursor);
2435            text.push(ch);
2436
2437            if escaped {
2438                escaped = false;
2439                continue;
2440            }
2441
2442            match ch {
2443                '\\' if !in_single => escaped = true,
2444                '\'' if !in_double => in_single = !in_single,
2445                '"' if !in_single => in_double = !in_double,
2446                '`' if !in_single => in_backtick = !in_backtick,
2447                '(' if !in_single && !in_double => paren_depth += 1,
2448                ')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
2449                '{' if !in_single && !in_double => brace_depth += 1,
2450                '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
2451                _ => {}
2452            }
2453        }
2454
2455        (!text.is_empty()).then_some((text, cursor))
2456    }
2457
2458    fn advance_past_word(&mut self, word: &Word) {
2459        let stop_after_synthetic = self
2460            .current_token
2461            .as_ref()
2462            .is_some_and(|token| token.flags.is_synthetic());
2463        while self.current_token.is_some() && self.current_span.start.offset < word.span.end.offset
2464        {
2465            self.advance();
2466            if stop_after_synthetic
2467                && self
2468                    .current_token
2469                    .as_ref()
2470                    .is_none_or(|token| !token.flags.is_synthetic())
2471            {
2472                break;
2473            }
2474        }
2475    }
2476
2477    fn token_source_like_word_text(&self, token: &LexedToken<'a>) -> Option<Cow<'a, str>> {
2478        token
2479            .source_slice(self.input)
2480            .map(Cow::Borrowed)
2481            .or_else(|| {
2482                (token.span.start.offset <= token.span.end.offset
2483                    && token.span.end.offset <= self.input.len())
2484                .then(|| Cow::Borrowed(&self.input[token.span.start.offset..token.span.end.offset]))
2485            })
2486            .or_else(|| token.word_string().map(Cow::Owned))
2487    }
2488
2489    fn current_source_like_word_text(&self) -> Option<Cow<'a, str>> {
2490        self.current_token_kind
2491            .filter(|kind| kind.is_word_like())
2492            .and(self.current_token.as_ref())
2493            .and_then(|token| self.token_source_like_word_text(token))
2494    }
2495
2496    fn current_source_like_word_text_or_error(
2497        &self,
2498        context: &'static str,
2499    ) -> Result<Cow<'a, str>> {
2500        self.current_source_like_word_text().ok_or_else(|| {
2501            self.error(format!(
2502                "internal parser error: missing source text for {context}"
2503            ))
2504        })
2505    }
2506
2507    fn keyword_from_token(token: &LexedToken<'_>) -> Option<Keyword> {
2508        (token.kind == TokenKind::Word)
2509            .then(|| token.word_text())
2510            .flatten()
2511            .and_then(Self::classify_keyword)
2512    }
2513
2514    fn current_conditional_literal_word(&self) -> Option<Word> {
2515        match self.current_token_kind? {
2516            TokenKind::LeftBrace | TokenKind::RightBrace => Some(Word::literal_with_span(
2517                self.input[self.current_span.start.offset..self.current_span.end.offset]
2518                    .to_string(),
2519                self.current_span,
2520            )),
2521            _ => None,
2522        }
2523    }
2524
2525    fn current_name_token(&self) -> Option<(Name, Span)> {
2526        self.current_token_kind
2527            .filter(|kind| kind.is_word_like())
2528            .and_then(|_| self.current_word_str())
2529            .map(|word| (Name::from(word), self.current_span))
2530    }
2531
2532    fn current_static_token_text(&self) -> Option<(String, bool)> {
2533        let token = self.current_token.as_ref()?;
2534        let raw_text = token.word_string()?;
2535        let text_had_escape_markers = raw_text.contains('\x00');
2536        let text = if text_had_escape_markers {
2537            raw_text.replace('\x00', "")
2538        } else {
2539            raw_text
2540        };
2541
2542        match token.kind {
2543            TokenKind::LiteralWord => Some((text, true)),
2544            TokenKind::QuotedWord if !Self::word_text_needs_parse(&text) => Some((text, true)),
2545            TokenKind::Word if !Self::word_text_needs_parse(&text) => Some((
2546                text,
2547                token.flags.has_cooked_text() || text_had_escape_markers,
2548            )),
2549            _ => None,
2550        }
2551    }
2552
2553    fn nested_stmt_seq_from_source(&mut self, source: &str, base: Position) -> StmtSeq {
2554        let remaining_depth = self.max_depth.saturating_sub(self.current_depth);
2555        let nested_profile = self
2556            .current_zsh_options()
2557            .cloned()
2558            .map(|options| ShellProfile::with_zsh_options(self.dialect, options))
2559            .unwrap_or_else(|| self.shell_profile.clone());
2560        let inner_parser =
2561            Parser::with_limits_and_profile(source, remaining_depth, self.fuel, nested_profile);
2562        let mut output = inner_parser.parse();
2563        if output.is_ok() {
2564            Self::materialize_stmt_seq_source_backing(&mut output.file.body, source);
2565            Self::rebase_file(&mut output.file, base);
2566            output.file.body
2567        } else {
2568            StmtSeq {
2569                leading_comments: Vec::new(),
2570                stmts: Vec::new(),
2571                trailing_comments: Vec::new(),
2572                span: Span::from_positions(base, base),
2573            }
2574        }
2575    }
2576
2577    fn nested_stmt_seq_from_current_input(&mut self, start: Position, end: Position) -> StmtSeq {
2578        if start.offset > end.offset || end.offset > self.input.len() {
2579            return StmtSeq {
2580                leading_comments: Vec::new(),
2581                stmts: Vec::new(),
2582                trailing_comments: Vec::new(),
2583                span: Span::from_positions(start, start),
2584            };
2585        }
2586        let source = &self.input[start.offset..end.offset];
2587        self.nested_stmt_seq_from_source(source, start)
2588    }
2589
2590    fn merge_optional_span(primary: Span, other: Span) -> Span {
2591        if other == Span::new() {
2592            primary
2593        } else {
2594            primary.merge(other)
2595        }
2596    }
2597
2598    fn redirect_span(operator_span: Span, target: &Word) -> Span {
2599        Self::merge_optional_span(operator_span, target.span)
2600    }
2601
2602    fn optional_span(start: Position, end: Position) -> Option<Span> {
2603        (start.offset < end.offset).then(|| Span::from_positions(start, end))
2604    }
2605
2606    fn split_nested_arithmetic_close(&mut self, context: &'static str) -> Result<Span> {
2607        let right_paren_start = self.current_span.start.advanced_by(")");
2608        self.advance();
2609
2610        if self.at(TokenKind::RightParen) {
2611            let right_paren_span = Span::from_positions(right_paren_start, self.current_span.end);
2612            self.advance();
2613            Ok(right_paren_span)
2614        } else {
2615            Err(Error::parse(format!(
2616                "expected ')' after '))' in {context}"
2617            )))
2618        }
2619    }
2620
2621    fn split_double_semicolon(span: Span) -> (Span, Span) {
2622        let middle = span.start.advanced_by(";");
2623        (
2624            Span::from_positions(span.start, middle),
2625            Span::from_positions(middle, span.end),
2626        )
2627    }
2628
2629    fn split_double_left_paren(span: Span) -> (Span, Span) {
2630        let middle = span.start.advanced_by("(");
2631        (
2632            Span::from_positions(span.start, middle),
2633            Span::from_positions(middle, span.end),
2634        )
2635    }
2636
2637    fn split_double_right_paren(span: Span) -> (Span, Span) {
2638        let middle = span.start.advanced_by(")");
2639        (
2640            Span::from_positions(span.start, middle),
2641            Span::from_positions(middle, span.end),
2642        )
2643    }
2644
2645    fn record_arithmetic_for_separator(
2646        semicolon_span: Span,
2647        segment_start: &mut Position,
2648        init_span: &mut Option<Span>,
2649        first_semicolon_span: &mut Option<Span>,
2650        condition_span: &mut Option<Span>,
2651        second_semicolon_span: &mut Option<Span>,
2652    ) -> Result<()> {
2653        if first_semicolon_span.is_none() {
2654            *init_span = Self::optional_span(*segment_start, semicolon_span.start);
2655            *first_semicolon_span = Some(semicolon_span);
2656            *segment_start = semicolon_span.end;
2657            return Ok(());
2658        }
2659
2660        if second_semicolon_span.is_none() {
2661            *condition_span = Self::optional_span(*segment_start, semicolon_span.start);
2662            *second_semicolon_span = Some(semicolon_span);
2663            *segment_start = semicolon_span.end;
2664            return Ok(());
2665        }
2666
2667        Err(Error::parse(
2668            "unexpected ';' in arithmetic for header".to_string(),
2669        ))
2670    }
2671
2672    fn rebase_file(file: &mut File, base: Position) {
2673        file.span = file.span.rebased(base);
2674        Self::rebase_stmt_seq(&mut file.body, base);
2675    }
2676
2677    fn rebase_comments(comments: &mut [Comment], base: Position) {
2678        let base_offset = TextSize::new(base.offset as u32);
2679        for comment in comments {
2680            comment.range = comment.range.offset_by(base_offset);
2681        }
2682    }
2683
2684    fn rebase_stmt_seq(sequence: &mut StmtSeq, base: Position) {
2685        sequence.span = sequence.span.rebased(base);
2686        Self::rebase_comments(&mut sequence.leading_comments, base);
2687        for stmt in &mut sequence.stmts {
2688            Self::rebase_stmt(stmt, base);
2689        }
2690        Self::rebase_comments(&mut sequence.trailing_comments, base);
2691    }
2692
2693    fn rebase_stmt(stmt: &mut Stmt, base: Position) {
2694        stmt.span = stmt.span.rebased(base);
2695        Self::rebase_comments(&mut stmt.leading_comments, base);
2696        stmt.terminator_span = stmt.terminator_span.map(|span| span.rebased(base));
2697        if let Some(comment) = &mut stmt.inline_comment {
2698            let base_offset = TextSize::new(base.offset as u32);
2699            comment.range = comment.range.offset_by(base_offset);
2700        }
2701        Self::rebase_redirects(&mut stmt.redirects, base);
2702        Self::rebase_ast_command(&mut stmt.command, base);
2703    }
2704
2705    fn rebase_ast_command(command: &mut AstCommand, base: Position) {
2706        match command {
2707            AstCommand::Simple(simple) => {
2708                simple.span = simple.span.rebased(base);
2709                Self::rebase_word(&mut simple.name, base);
2710                Self::rebase_words(&mut simple.args, base);
2711                Self::rebase_assignments(&mut simple.assignments, base);
2712            }
2713            AstCommand::Builtin(builtin) => match builtin {
2714                AstBuiltinCommand::Break(command) => {
2715                    command.span = command.span.rebased(base);
2716                    if let Some(depth) = &mut command.depth {
2717                        Self::rebase_word(depth, base);
2718                    }
2719                    Self::rebase_words(&mut command.extra_args, base);
2720                    Self::rebase_assignments(&mut command.assignments, base);
2721                }
2722                AstBuiltinCommand::Continue(command) => {
2723                    command.span = command.span.rebased(base);
2724                    if let Some(depth) = &mut command.depth {
2725                        Self::rebase_word(depth, base);
2726                    }
2727                    Self::rebase_words(&mut command.extra_args, base);
2728                    Self::rebase_assignments(&mut command.assignments, base);
2729                }
2730                AstBuiltinCommand::Return(command) => {
2731                    command.span = command.span.rebased(base);
2732                    if let Some(code) = &mut command.code {
2733                        Self::rebase_word(code, base);
2734                    }
2735                    Self::rebase_words(&mut command.extra_args, base);
2736                    Self::rebase_assignments(&mut command.assignments, base);
2737                }
2738                AstBuiltinCommand::Exit(command) => {
2739                    command.span = command.span.rebased(base);
2740                    if let Some(code) = &mut command.code {
2741                        Self::rebase_word(code, base);
2742                    }
2743                    Self::rebase_words(&mut command.extra_args, base);
2744                    Self::rebase_assignments(&mut command.assignments, base);
2745                }
2746            },
2747            AstCommand::Decl(decl) => {
2748                decl.span = decl.span.rebased(base);
2749                decl.variant_span = decl.variant_span.rebased(base);
2750                Self::rebase_assignments(&mut decl.assignments, base);
2751                for operand in &mut decl.operands {
2752                    match operand {
2753                        DeclOperand::Flag(word) | DeclOperand::Dynamic(word) => {
2754                            Self::rebase_word(word, base);
2755                        }
2756                        DeclOperand::Name(name) => Self::rebase_var_ref(name, base),
2757                        DeclOperand::Assignment(assignment) => {
2758                            Self::rebase_assignments(std::slice::from_mut(assignment), base);
2759                        }
2760                    }
2761                }
2762            }
2763            AstCommand::Binary(binary) => {
2764                binary.span = binary.span.rebased(base);
2765                binary.op_span = binary.op_span.rebased(base);
2766                Self::rebase_stmt(binary.left.as_mut(), base);
2767                Self::rebase_stmt(binary.right.as_mut(), base);
2768            }
2769            AstCommand::Compound(compound) => Self::rebase_compound(compound, base),
2770            AstCommand::Function(function) => {
2771                function.span = function.span.rebased(base);
2772                if let Some(span) = &mut function.header.function_keyword_span {
2773                    *span = span.rebased(base);
2774                }
2775                if let Some(span) = &mut function.header.trailing_parens_span {
2776                    *span = span.rebased(base);
2777                }
2778                for entry in &mut function.header.entries {
2779                    Self::rebase_word(&mut entry.word, base);
2780                }
2781                Self::rebase_stmt(function.body.as_mut(), base);
2782            }
2783            AstCommand::AnonymousFunction(function) => {
2784                function.span = function.span.rebased(base);
2785                function.surface = match function.surface {
2786                    AnonymousFunctionSurface::FunctionKeyword {
2787                        function_keyword_span,
2788                    } => AnonymousFunctionSurface::FunctionKeyword {
2789                        function_keyword_span: function_keyword_span.rebased(base),
2790                    },
2791                    AnonymousFunctionSurface::Parens { parens_span } => {
2792                        AnonymousFunctionSurface::Parens {
2793                            parens_span: parens_span.rebased(base),
2794                        }
2795                    }
2796                };
2797                Self::rebase_stmt(function.body.as_mut(), base);
2798                Self::rebase_words(&mut function.args, base);
2799            }
2800        }
2801    }
2802
2803    fn rebase_subscript(subscript: &mut Subscript, base: Position) {
2804        subscript.text.rebased(base);
2805        if let Some(raw) = &mut subscript.raw {
2806            raw.rebased(base);
2807        }
2808        if let Some(word) = &mut subscript.word_ast {
2809            Self::rebase_word(word, base);
2810        }
2811        if let Some(expr) = &mut subscript.arithmetic_ast {
2812            Self::rebase_arithmetic_expr(expr, base);
2813        }
2814    }
2815
2816    fn rebase_var_ref(reference: &mut VarRef, base: Position) {
2817        reference.span = reference.span.rebased(base);
2818        reference.name_span = reference.name_span.rebased(base);
2819        if let Some(subscript) = &mut reference.subscript {
2820            Self::rebase_subscript(subscript, base);
2821        }
2822    }
2823
2824    fn rebase_array_expr(array: &mut ArrayExpr, base: Position) {
2825        array.span = array.span.rebased(base);
2826        for element in &mut array.elements {
2827            match element {
2828                ArrayElem::Sequential(word) => Self::rebase_word(word, base),
2829                ArrayElem::Keyed { key, value } | ArrayElem::KeyedAppend { key, value } => {
2830                    Self::rebase_subscript(key, base);
2831                    Self::rebase_word(value, base);
2832                }
2833            }
2834        }
2835    }
2836
2837    fn rebase_compound(compound: &mut CompoundCommand, base: Position) {
2838        match compound {
2839            CompoundCommand::If(command) => {
2840                command.span = command.span.rebased(base);
2841                command.syntax = match command.syntax {
2842                    IfSyntax::ThenFi { then_span, fi_span } => IfSyntax::ThenFi {
2843                        then_span: then_span.rebased(base),
2844                        fi_span: fi_span.rebased(base),
2845                    },
2846                    IfSyntax::Brace {
2847                        left_brace_span,
2848                        right_brace_span,
2849                    } => IfSyntax::Brace {
2850                        left_brace_span: left_brace_span.rebased(base),
2851                        right_brace_span: right_brace_span.rebased(base),
2852                    },
2853                };
2854                Self::rebase_stmt_seq(&mut command.condition, base);
2855                Self::rebase_stmt_seq(&mut command.then_branch, base);
2856                for (condition, body) in &mut command.elif_branches {
2857                    Self::rebase_stmt_seq(condition, base);
2858                    Self::rebase_stmt_seq(body, base);
2859                }
2860                if let Some(else_branch) = &mut command.else_branch {
2861                    Self::rebase_stmt_seq(else_branch, base);
2862                }
2863            }
2864            CompoundCommand::For(command) => {
2865                command.span = command.span.rebased(base);
2866                for target in &mut command.targets {
2867                    target.span = target.span.rebased(base);
2868                }
2869                if let Some(words) = &mut command.words {
2870                    Self::rebase_words(words, base);
2871                }
2872                command.syntax = match command.syntax {
2873                    ForSyntax::InDoDone {
2874                        in_span,
2875                        do_span,
2876                        done_span,
2877                    } => ForSyntax::InDoDone {
2878                        in_span: in_span.map(|span| span.rebased(base)),
2879                        do_span: do_span.rebased(base),
2880                        done_span: done_span.rebased(base),
2881                    },
2882                    ForSyntax::InDirect { in_span } => ForSyntax::InDirect {
2883                        in_span: in_span.map(|span| span.rebased(base)),
2884                    },
2885                    ForSyntax::InBrace {
2886                        in_span,
2887                        left_brace_span,
2888                        right_brace_span,
2889                    } => ForSyntax::InBrace {
2890                        in_span: in_span.map(|span| span.rebased(base)),
2891                        left_brace_span: left_brace_span.rebased(base),
2892                        right_brace_span: right_brace_span.rebased(base),
2893                    },
2894                    ForSyntax::ParenDoDone {
2895                        left_paren_span,
2896                        right_paren_span,
2897                        do_span,
2898                        done_span,
2899                    } => ForSyntax::ParenDoDone {
2900                        left_paren_span: left_paren_span.rebased(base),
2901                        right_paren_span: right_paren_span.rebased(base),
2902                        do_span: do_span.rebased(base),
2903                        done_span: done_span.rebased(base),
2904                    },
2905                    ForSyntax::ParenDirect {
2906                        left_paren_span,
2907                        right_paren_span,
2908                    } => ForSyntax::ParenDirect {
2909                        left_paren_span: left_paren_span.rebased(base),
2910                        right_paren_span: right_paren_span.rebased(base),
2911                    },
2912                    ForSyntax::ParenBrace {
2913                        left_paren_span,
2914                        right_paren_span,
2915                        left_brace_span,
2916                        right_brace_span,
2917                    } => ForSyntax::ParenBrace {
2918                        left_paren_span: left_paren_span.rebased(base),
2919                        right_paren_span: right_paren_span.rebased(base),
2920                        left_brace_span: left_brace_span.rebased(base),
2921                        right_brace_span: right_brace_span.rebased(base),
2922                    },
2923                };
2924                Self::rebase_stmt_seq(&mut command.body, base);
2925            }
2926            CompoundCommand::Repeat(command) => {
2927                command.span = command.span.rebased(base);
2928                Self::rebase_word(&mut command.count, base);
2929                command.syntax = match command.syntax {
2930                    RepeatSyntax::DoDone { do_span, done_span } => RepeatSyntax::DoDone {
2931                        do_span: do_span.rebased(base),
2932                        done_span: done_span.rebased(base),
2933                    },
2934                    RepeatSyntax::Direct => RepeatSyntax::Direct,
2935                    RepeatSyntax::Brace {
2936                        left_brace_span,
2937                        right_brace_span,
2938                    } => RepeatSyntax::Brace {
2939                        left_brace_span: left_brace_span.rebased(base),
2940                        right_brace_span: right_brace_span.rebased(base),
2941                    },
2942                };
2943                Self::rebase_stmt_seq(&mut command.body, base);
2944            }
2945            CompoundCommand::Foreach(command) => {
2946                command.span = command.span.rebased(base);
2947                command.variable_span = command.variable_span.rebased(base);
2948                Self::rebase_words(&mut command.words, base);
2949                command.syntax = match command.syntax {
2950                    ForeachSyntax::ParenBrace {
2951                        left_paren_span,
2952                        right_paren_span,
2953                        left_brace_span,
2954                        right_brace_span,
2955                    } => ForeachSyntax::ParenBrace {
2956                        left_paren_span: left_paren_span.rebased(base),
2957                        right_paren_span: right_paren_span.rebased(base),
2958                        left_brace_span: left_brace_span.rebased(base),
2959                        right_brace_span: right_brace_span.rebased(base),
2960                    },
2961                    ForeachSyntax::InDoDone {
2962                        in_span,
2963                        do_span,
2964                        done_span,
2965                    } => ForeachSyntax::InDoDone {
2966                        in_span: in_span.rebased(base),
2967                        do_span: do_span.rebased(base),
2968                        done_span: done_span.rebased(base),
2969                    },
2970                };
2971                Self::rebase_stmt_seq(&mut command.body, base);
2972            }
2973            CompoundCommand::ArithmeticFor(command) => {
2974                command.span = command.span.rebased(base);
2975                command.left_paren_span = command.left_paren_span.rebased(base);
2976                command.init_span = command.init_span.map(|span| span.rebased(base));
2977                if let Some(expr) = &mut command.init_ast {
2978                    Self::rebase_arithmetic_expr(expr, base);
2979                }
2980                command.first_semicolon_span = command.first_semicolon_span.rebased(base);
2981                command.condition_span = command.condition_span.map(|span| span.rebased(base));
2982                if let Some(expr) = &mut command.condition_ast {
2983                    Self::rebase_arithmetic_expr(expr, base);
2984                }
2985                command.second_semicolon_span = command.second_semicolon_span.rebased(base);
2986                command.step_span = command.step_span.map(|span| span.rebased(base));
2987                if let Some(expr) = &mut command.step_ast {
2988                    Self::rebase_arithmetic_expr(expr, base);
2989                }
2990                command.right_paren_span = command.right_paren_span.rebased(base);
2991                Self::rebase_stmt_seq(&mut command.body, base);
2992            }
2993            CompoundCommand::While(command) => {
2994                command.span = command.span.rebased(base);
2995                Self::rebase_stmt_seq(&mut command.condition, base);
2996                Self::rebase_stmt_seq(&mut command.body, base);
2997            }
2998            CompoundCommand::Until(command) => {
2999                command.span = command.span.rebased(base);
3000                Self::rebase_stmt_seq(&mut command.condition, base);
3001                Self::rebase_stmt_seq(&mut command.body, base);
3002            }
3003            CompoundCommand::Case(command) => {
3004                command.span = command.span.rebased(base);
3005                Self::rebase_word(&mut command.word, base);
3006                for case in &mut command.cases {
3007                    Self::rebase_patterns(&mut case.patterns, base);
3008                    Self::rebase_stmt_seq(&mut case.body, base);
3009                }
3010            }
3011            CompoundCommand::Select(command) => {
3012                command.span = command.span.rebased(base);
3013                command.variable_span = command.variable_span.rebased(base);
3014                Self::rebase_words(&mut command.words, base);
3015                Self::rebase_stmt_seq(&mut command.body, base);
3016            }
3017            CompoundCommand::Subshell(commands) | CompoundCommand::BraceGroup(commands) => {
3018                Self::rebase_stmt_seq(commands, base);
3019            }
3020            CompoundCommand::Arithmetic(command) => {
3021                command.span = command.span.rebased(base);
3022                command.left_paren_span = command.left_paren_span.rebased(base);
3023                command.expr_span = command.expr_span.map(|span| span.rebased(base));
3024                if let Some(expr) = &mut command.expr_ast {
3025                    Self::rebase_arithmetic_expr(expr, base);
3026                }
3027                command.right_paren_span = command.right_paren_span.rebased(base);
3028            }
3029            CompoundCommand::Time(command) => {
3030                command.span = command.span.rebased(base);
3031                if let Some(inner) = &mut command.command {
3032                    Self::rebase_stmt(inner.as_mut(), base);
3033                }
3034            }
3035            CompoundCommand::Conditional(command) => {
3036                command.span = command.span.rebased(base);
3037                command.left_bracket_span = command.left_bracket_span.rebased(base);
3038                command.right_bracket_span = command.right_bracket_span.rebased(base);
3039                Self::rebase_conditional_expr(&mut command.expression, base);
3040            }
3041            CompoundCommand::Coproc(command) => {
3042                command.span = command.span.rebased(base);
3043                command.name_span = command.name_span.map(|span| span.rebased(base));
3044                Self::rebase_stmt(command.body.as_mut(), base);
3045            }
3046            CompoundCommand::Always(command) => {
3047                command.span = command.span.rebased(base);
3048                Self::rebase_stmt_seq(&mut command.body, base);
3049                Self::rebase_stmt_seq(&mut command.always_body, base);
3050            }
3051        }
3052    }
3053
3054    fn materialize_stmt_seq_source_backing(sequence: &mut StmtSeq, source: &str) {
3055        for stmt in &mut sequence.stmts {
3056            Self::materialize_stmt_source_backing(stmt, source);
3057        }
3058    }
3059
3060    fn materialize_stmt_source_backing(stmt: &mut Stmt, source: &str) {
3061        Self::materialize_ast_command_source_backing(&mut stmt.command, source);
3062    }
3063
3064    fn materialize_ast_command_source_backing(command: &mut AstCommand, source: &str) {
3065        match command {
3066            AstCommand::Simple(simple) => {
3067                Self::materialize_word_source_backing(&mut simple.name, source);
3068            }
3069            AstCommand::Builtin(_) | AstCommand::Decl(_) => {}
3070            AstCommand::Binary(binary) => {
3071                Self::materialize_stmt_source_backing(binary.left.as_mut(), source);
3072                Self::materialize_stmt_source_backing(binary.right.as_mut(), source);
3073            }
3074            AstCommand::Compound(compound) => {
3075                Self::materialize_compound_source_backing(compound, source);
3076            }
3077            AstCommand::Function(function) => {
3078                Self::materialize_stmt_source_backing(function.body.as_mut(), source);
3079            }
3080            AstCommand::AnonymousFunction(function) => {
3081                Self::materialize_stmt_source_backing(function.body.as_mut(), source);
3082            }
3083        }
3084    }
3085
3086    fn materialize_compound_source_backing(compound: &mut CompoundCommand, source: &str) {
3087        match compound {
3088            CompoundCommand::If(command) => {
3089                Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
3090                Self::materialize_stmt_seq_source_backing(&mut command.then_branch, source);
3091                for (condition, body) in &mut command.elif_branches {
3092                    Self::materialize_stmt_seq_source_backing(condition, source);
3093                    Self::materialize_stmt_seq_source_backing(body, source);
3094                }
3095                if let Some(else_branch) = &mut command.else_branch {
3096                    Self::materialize_stmt_seq_source_backing(else_branch, source);
3097                }
3098            }
3099            CompoundCommand::For(command) => {
3100                Self::materialize_stmt_seq_source_backing(&mut command.body, source)
3101            }
3102            CompoundCommand::Repeat(command) => {
3103                Self::materialize_stmt_seq_source_backing(&mut command.body, source)
3104            }
3105            CompoundCommand::Foreach(command) => {
3106                Self::materialize_stmt_seq_source_backing(&mut command.body, source)
3107            }
3108            CompoundCommand::ArithmeticFor(command) => {
3109                Self::materialize_stmt_seq_source_backing(&mut command.body, source)
3110            }
3111            CompoundCommand::While(command) => {
3112                Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
3113                Self::materialize_stmt_seq_source_backing(&mut command.body, source);
3114            }
3115            CompoundCommand::Until(command) => {
3116                Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
3117                Self::materialize_stmt_seq_source_backing(&mut command.body, source);
3118            }
3119            CompoundCommand::Case(command) => {
3120                for case in &mut command.cases {
3121                    Self::materialize_stmt_seq_source_backing(&mut case.body, source);
3122                }
3123            }
3124            CompoundCommand::Select(command) => {
3125                Self::materialize_stmt_seq_source_backing(&mut command.body, source)
3126            }
3127            CompoundCommand::Subshell(commands) | CompoundCommand::BraceGroup(commands) => {
3128                Self::materialize_stmt_seq_source_backing(commands, source);
3129            }
3130            CompoundCommand::Arithmetic(_) => {}
3131            CompoundCommand::Time(command) => {
3132                if let Some(inner) = &mut command.command {
3133                    Self::materialize_stmt_source_backing(inner.as_mut(), source);
3134                }
3135            }
3136            CompoundCommand::Conditional(_) => {}
3137            CompoundCommand::Coproc(command) => {
3138                Self::materialize_stmt_source_backing(command.body.as_mut(), source);
3139            }
3140            CompoundCommand::Always(command) => {
3141                Self::materialize_stmt_seq_source_backing(&mut command.body, source);
3142                Self::materialize_stmt_seq_source_backing(&mut command.always_body, source);
3143            }
3144        }
3145    }
3146
3147    fn rebase_words(words: &mut [Word], base: Position) {
3148        for word in words {
3149            Self::rebase_word(word, base);
3150        }
3151    }
3152
3153    fn rebase_patterns(patterns: &mut [Pattern], base: Position) {
3154        for pattern in patterns {
3155            Self::rebase_pattern(pattern, base);
3156        }
3157    }
3158
3159    fn materialize_literal_text_source_backing(text: &mut LiteralText, span: Span, source: &str) {
3160        match text {
3161            LiteralText::Source => {
3162                *text = LiteralText::owned(span.slice(source).to_string());
3163            }
3164            LiteralText::CookedSource(cooked) => {
3165                *text = LiteralText::owned(cooked.to_string());
3166            }
3167            LiteralText::Owned(_) => {}
3168        }
3169    }
3170
3171    fn materialize_source_text_source_backing(text: &mut SourceText, source: &str) {
3172        if text.is_source_backed() {
3173            let span = text.span();
3174            let cooked = text.slice(source).to_string();
3175            *text = SourceText::cooked(span, cooked);
3176        }
3177    }
3178
3179    fn materialize_word_source_backing(word: &mut Word, source: &str) {
3180        for part in &mut word.parts {
3181            Self::materialize_word_part_source_backing(part, source);
3182        }
3183    }
3184
3185    fn materialize_pattern_source_backing(pattern: &mut Pattern, source: &str) {
3186        for part in &mut pattern.parts {
3187            Self::materialize_pattern_part_source_backing(part, source);
3188        }
3189    }
3190
3191    fn materialize_pattern_part_source_backing(part: &mut PatternPartNode, source: &str) {
3192        match &mut part.kind {
3193            PatternPart::Literal(text) => {
3194                Self::materialize_literal_text_source_backing(text, part.span, source);
3195            }
3196            PatternPart::CharClass(text) => {
3197                Self::materialize_source_text_source_backing(text, source);
3198            }
3199            PatternPart::Group { patterns, .. } => {
3200                for pattern in patterns {
3201                    Self::materialize_pattern_source_backing(pattern, source);
3202                }
3203            }
3204            PatternPart::Word(word) => Self::materialize_word_source_backing(word, source),
3205            PatternPart::AnyString | PatternPart::AnyChar => {}
3206        }
3207    }
3208
3209    fn materialize_word_part_source_backing(part: &mut WordPartNode, source: &str) {
3210        match &mut part.kind {
3211            WordPart::Literal(text) => {
3212                Self::materialize_literal_text_source_backing(text, part.span, source);
3213            }
3214            WordPart::ZshQualifiedGlob(glob) => {
3215                Self::materialize_zsh_qualified_glob_source_backing(glob, source);
3216            }
3217            WordPart::SingleQuoted { value, .. } => {
3218                Self::materialize_source_text_source_backing(value, source);
3219            }
3220            WordPart::DoubleQuoted { parts, .. } => {
3221                for part in parts {
3222                    Self::materialize_word_part_source_backing(part, source);
3223                }
3224            }
3225            WordPart::Parameter(parameter) => {
3226                Self::materialize_source_text_source_backing(&mut parameter.raw_body, source);
3227                Self::materialize_parameter_expansion_syntax_source_backing(
3228                    &mut parameter.syntax,
3229                    source,
3230                );
3231            }
3232            WordPart::ParameterExpansion {
3233                reference,
3234                operator,
3235                operand,
3236                operand_word_ast,
3237                ..
3238            } => {
3239                Self::materialize_var_ref_source_backing(reference, source);
3240                Self::materialize_parameter_operator_source_backing(operator, source);
3241                if let Some(operand) = operand {
3242                    Self::materialize_source_text_source_backing(operand, source);
3243                }
3244                if let Some(word_ast) = operand_word_ast {
3245                    Self::materialize_word_source_backing(word_ast, source);
3246                }
3247            }
3248            WordPart::ArrayAccess(reference)
3249            | WordPart::Length(reference)
3250            | WordPart::ArrayLength(reference)
3251            | WordPart::ArrayIndices(reference)
3252            | WordPart::Transformation { reference, .. } => {
3253                Self::materialize_var_ref_source_backing(reference, source);
3254            }
3255            WordPart::Substring {
3256                reference,
3257                offset,
3258                offset_ast,
3259                offset_word_ast,
3260                length,
3261                length_ast,
3262                length_word_ast,
3263                ..
3264            }
3265            | WordPart::ArraySlice {
3266                reference,
3267                offset,
3268                offset_ast,
3269                offset_word_ast,
3270                length,
3271                length_ast,
3272                length_word_ast,
3273                ..
3274            } => {
3275                Self::materialize_var_ref_source_backing(reference, source);
3276                Self::materialize_source_text_source_backing(offset, source);
3277                Self::materialize_word_source_backing(offset_word_ast, source);
3278                if let Some(expr) = offset_ast {
3279                    Self::materialize_arithmetic_expr_source_backing(expr, source);
3280                }
3281                if let Some(length) = length {
3282                    Self::materialize_source_text_source_backing(length, source);
3283                }
3284                if let Some(word_ast) = length_word_ast {
3285                    Self::materialize_word_source_backing(word_ast, source);
3286                }
3287                if let Some(expr) = length_ast {
3288                    Self::materialize_arithmetic_expr_source_backing(expr, source);
3289                }
3290            }
3291            WordPart::IndirectExpansion {
3292                reference,
3293                operator,
3294                operand,
3295                operand_word_ast,
3296                ..
3297            } => {
3298                Self::materialize_var_ref_source_backing(reference, source);
3299                if let Some(operator) = operator {
3300                    Self::materialize_parameter_operator_source_backing(operator, source);
3301                }
3302                if let Some(operand) = operand {
3303                    Self::materialize_source_text_source_backing(operand, source);
3304                }
3305                if let Some(word_ast) = operand_word_ast {
3306                    Self::materialize_word_source_backing(word_ast, source);
3307                }
3308            }
3309            WordPart::ArithmeticExpansion {
3310                expression,
3311                expression_ast,
3312                expression_word_ast,
3313                ..
3314            } => {
3315                Self::materialize_source_text_source_backing(expression, source);
3316                Self::materialize_word_source_backing(expression_word_ast, source);
3317                if let Some(expr) = expression_ast {
3318                    Self::materialize_arithmetic_expr_source_backing(expr, source);
3319                }
3320            }
3321            WordPart::CommandSubstitution { .. }
3322            | WordPart::ProcessSubstitution { .. }
3323            | WordPart::Variable(_)
3324            | WordPart::PrefixMatch { .. } => {}
3325        }
3326    }
3327
3328    fn materialize_var_ref_source_backing(reference: &mut VarRef, source: &str) {
3329        if let Some(subscript) = &mut reference.subscript {
3330            Self::materialize_subscript_source_backing(subscript, source);
3331        }
3332    }
3333
3334    fn materialize_subscript_source_backing(subscript: &mut Subscript, source: &str) {
3335        Self::materialize_source_text_source_backing(&mut subscript.text, source);
3336        if let Some(raw) = &mut subscript.raw {
3337            Self::materialize_source_text_source_backing(raw, source);
3338        }
3339        if let Some(word_ast) = &mut subscript.word_ast {
3340            Self::materialize_word_source_backing(word_ast, source);
3341        }
3342        if let Some(expr) = &mut subscript.arithmetic_ast {
3343            Self::materialize_arithmetic_expr_source_backing(expr, source);
3344        }
3345    }
3346
3347    fn materialize_zsh_qualified_glob_source_backing(glob: &mut ZshQualifiedGlob, source: &str) {
3348        for segment in &mut glob.segments {
3349            match segment {
3350                ZshGlobSegment::Pattern(pattern) => {
3351                    Self::materialize_pattern_source_backing(pattern, source);
3352                }
3353                ZshGlobSegment::InlineControl(_) => {}
3354            }
3355        }
3356        if let Some(qualifiers) = &mut glob.qualifiers {
3357            for fragment in &mut qualifiers.fragments {
3358                match fragment {
3359                    ZshGlobQualifier::LetterSequence { text, .. } => {
3360                        Self::materialize_source_text_source_backing(text, source);
3361                    }
3362                    ZshGlobQualifier::NumericArgument { start, end, .. } => {
3363                        Self::materialize_source_text_source_backing(start, source);
3364                        if let Some(end) = end {
3365                            Self::materialize_source_text_source_backing(end, source);
3366                        }
3367                    }
3368                    ZshGlobQualifier::Negation { .. } | ZshGlobQualifier::Flag { .. } => {}
3369                }
3370            }
3371        }
3372    }
3373
3374    fn materialize_parameter_expansion_syntax_source_backing(
3375        syntax: &mut ParameterExpansionSyntax,
3376        source: &str,
3377    ) {
3378        match syntax {
3379            ParameterExpansionSyntax::Bourne(syntax) => match syntax {
3380                BourneParameterExpansion::Access { reference }
3381                | BourneParameterExpansion::Length { reference }
3382                | BourneParameterExpansion::Indices { reference }
3383                | BourneParameterExpansion::Transformation { reference, .. } => {
3384                    Self::materialize_var_ref_source_backing(reference, source);
3385                }
3386                BourneParameterExpansion::Indirect {
3387                    reference,
3388                    operator,
3389                    operand,
3390                    operand_word_ast,
3391                    ..
3392                } => {
3393                    Self::materialize_var_ref_source_backing(reference, source);
3394                    if let Some(operator) = operator {
3395                        Self::materialize_parameter_operator_source_backing(operator, source);
3396                    }
3397                    if let Some(operand) = operand {
3398                        Self::materialize_source_text_source_backing(operand, source);
3399                    }
3400                    if let Some(word_ast) = operand_word_ast {
3401                        Self::materialize_word_source_backing(word_ast, source);
3402                    }
3403                }
3404                BourneParameterExpansion::PrefixMatch { .. } => {}
3405                BourneParameterExpansion::Slice {
3406                    reference,
3407                    offset,
3408                    offset_ast,
3409                    offset_word_ast,
3410                    length,
3411                    length_ast,
3412                    length_word_ast,
3413                } => {
3414                    Self::materialize_var_ref_source_backing(reference, source);
3415                    Self::materialize_source_text_source_backing(offset, source);
3416                    Self::materialize_word_source_backing(offset_word_ast, source);
3417                    if let Some(expr) = offset_ast {
3418                        Self::materialize_arithmetic_expr_source_backing(expr, source);
3419                    }
3420                    if let Some(length) = length {
3421                        Self::materialize_source_text_source_backing(length, source);
3422                    }
3423                    if let Some(word_ast) = length_word_ast {
3424                        Self::materialize_word_source_backing(word_ast, source);
3425                    }
3426                    if let Some(expr) = length_ast {
3427                        Self::materialize_arithmetic_expr_source_backing(expr, source);
3428                    }
3429                }
3430                BourneParameterExpansion::Operation {
3431                    reference,
3432                    operator,
3433                    operand,
3434                    operand_word_ast,
3435                    ..
3436                } => {
3437                    Self::materialize_var_ref_source_backing(reference, source);
3438                    Self::materialize_parameter_operator_source_backing(operator, source);
3439                    if let Some(operand) = operand {
3440                        Self::materialize_source_text_source_backing(operand, source);
3441                    }
3442                    if let Some(word_ast) = operand_word_ast {
3443                        Self::materialize_word_source_backing(word_ast, source);
3444                    }
3445                }
3446            },
3447            ParameterExpansionSyntax::Zsh(syntax) => {
3448                match &mut syntax.target {
3449                    ZshExpansionTarget::Reference(reference) => {
3450                        Self::materialize_var_ref_source_backing(reference, source);
3451                    }
3452                    ZshExpansionTarget::Word(word) => {
3453                        Self::materialize_word_source_backing(word, source);
3454                    }
3455                    ZshExpansionTarget::Nested(parameter) => {
3456                        Self::materialize_source_text_source_backing(
3457                            &mut parameter.raw_body,
3458                            source,
3459                        );
3460                        Self::materialize_parameter_expansion_syntax_source_backing(
3461                            &mut parameter.syntax,
3462                            source,
3463                        );
3464                    }
3465                    ZshExpansionTarget::Empty => {}
3466                }
3467                for modifier in &mut syntax.modifiers {
3468                    if let Some(argument) = &mut modifier.argument {
3469                        Self::materialize_source_text_source_backing(argument, source);
3470                    }
3471                    if let Some(argument_word_ast) = &mut modifier.argument_word_ast {
3472                        Self::materialize_word_source_backing(argument_word_ast, source);
3473                    }
3474                }
3475                if let Some(operation) = &mut syntax.operation {
3476                    match operation {
3477                        ZshExpansionOperation::PatternOperation {
3478                            operand,
3479                            operand_word_ast,
3480                            ..
3481                        }
3482                        | ZshExpansionOperation::Defaulting {
3483                            operand,
3484                            operand_word_ast,
3485                            ..
3486                        }
3487                        | ZshExpansionOperation::TrimOperation {
3488                            operand,
3489                            operand_word_ast,
3490                            ..
3491                        } => {
3492                            Self::materialize_source_text_source_backing(operand, source);
3493                            Self::materialize_word_source_backing(operand_word_ast, source);
3494                        }
3495                        ZshExpansionOperation::Unknown { text, word_ast } => {
3496                            Self::materialize_source_text_source_backing(text, source);
3497                            Self::materialize_word_source_backing(word_ast, source);
3498                        }
3499                        ZshExpansionOperation::ReplacementOperation {
3500                            pattern,
3501                            pattern_word_ast,
3502                            replacement,
3503                            replacement_word_ast,
3504                            ..
3505                        } => {
3506                            Self::materialize_source_text_source_backing(pattern, source);
3507                            Self::materialize_word_source_backing(pattern_word_ast, source);
3508                            if let Some(replacement) = replacement {
3509                                Self::materialize_source_text_source_backing(replacement, source);
3510                            }
3511                            if let Some(replacement_word_ast) = replacement_word_ast {
3512                                Self::materialize_word_source_backing(replacement_word_ast, source);
3513                            }
3514                        }
3515                        ZshExpansionOperation::Slice {
3516                            offset,
3517                            offset_word_ast,
3518                            length,
3519                            length_word_ast,
3520                        } => {
3521                            Self::materialize_source_text_source_backing(offset, source);
3522                            Self::materialize_word_source_backing(offset_word_ast, source);
3523                            if let Some(length) = length {
3524                                Self::materialize_source_text_source_backing(length, source);
3525                            }
3526                            if let Some(length_word_ast) = length_word_ast {
3527                                Self::materialize_word_source_backing(length_word_ast, source);
3528                            }
3529                        }
3530                    }
3531                }
3532            }
3533        }
3534    }
3535
3536    fn materialize_parameter_operator_source_backing(operator: &mut ParameterOp, source: &str) {
3537        match operator {
3538            ParameterOp::RemovePrefixShort { pattern }
3539            | ParameterOp::RemovePrefixLong { pattern }
3540            | ParameterOp::RemoveSuffixShort { pattern }
3541            | ParameterOp::RemoveSuffixLong { pattern } => {
3542                Self::materialize_pattern_source_backing(pattern, source);
3543            }
3544            ParameterOp::ReplaceFirst {
3545                pattern,
3546                replacement,
3547                replacement_word_ast,
3548            }
3549            | ParameterOp::ReplaceAll {
3550                pattern,
3551                replacement,
3552                replacement_word_ast,
3553            } => {
3554                Self::materialize_pattern_source_backing(pattern, source);
3555                Self::materialize_source_text_source_backing(replacement, source);
3556                Self::materialize_word_source_backing(replacement_word_ast, source);
3557            }
3558            ParameterOp::UseDefault
3559            | ParameterOp::AssignDefault
3560            | ParameterOp::UseReplacement
3561            | ParameterOp::Error
3562            | ParameterOp::UpperFirst
3563            | ParameterOp::UpperAll
3564            | ParameterOp::LowerFirst
3565            | ParameterOp::LowerAll => {}
3566        }
3567    }
3568
3569    fn materialize_arithmetic_expr_source_backing(expr: &mut ArithmeticExprNode, source: &str) {
3570        match &mut expr.kind {
3571            ArithmeticExpr::Number(text) => {
3572                Self::materialize_source_text_source_backing(text, source);
3573            }
3574            ArithmeticExpr::Variable(_) => {}
3575            ArithmeticExpr::Indexed { index, .. } => {
3576                Self::materialize_arithmetic_expr_source_backing(index, source);
3577            }
3578            ArithmeticExpr::ShellWord(word) => {
3579                Self::materialize_word_source_backing(word, source);
3580            }
3581            ArithmeticExpr::Parenthesized { expression } => {
3582                Self::materialize_arithmetic_expr_source_backing(expression, source);
3583            }
3584            ArithmeticExpr::Unary { expr, .. } | ArithmeticExpr::Postfix { expr, .. } => {
3585                Self::materialize_arithmetic_expr_source_backing(expr, source);
3586            }
3587            ArithmeticExpr::Binary { left, right, .. } => {
3588                Self::materialize_arithmetic_expr_source_backing(left, source);
3589                Self::materialize_arithmetic_expr_source_backing(right, source);
3590            }
3591            ArithmeticExpr::Conditional {
3592                condition,
3593                then_expr,
3594                else_expr,
3595            } => {
3596                Self::materialize_arithmetic_expr_source_backing(condition, source);
3597                Self::materialize_arithmetic_expr_source_backing(then_expr, source);
3598                Self::materialize_arithmetic_expr_source_backing(else_expr, source);
3599            }
3600            ArithmeticExpr::Assignment { target, value, .. } => {
3601                Self::materialize_arithmetic_lvalue_source_backing(target, source);
3602                Self::materialize_arithmetic_expr_source_backing(value, source);
3603            }
3604        }
3605    }
3606
3607    fn materialize_arithmetic_lvalue_source_backing(target: &mut ArithmeticLvalue, source: &str) {
3608        match target {
3609            ArithmeticLvalue::Variable(_) => {}
3610            ArithmeticLvalue::Indexed { index, .. } => {
3611                Self::materialize_arithmetic_expr_source_backing(index, source);
3612            }
3613        }
3614    }
3615
3616    fn rebase_word(word: &mut Word, base: Position) {
3617        word.span = word.span.rebased(base);
3618        for brace in &mut word.brace_syntax {
3619            brace.span = brace.span.rebased(base);
3620        }
3621        Self::rebase_word_parts(&mut word.parts, base);
3622    }
3623
3624    fn rebase_heredoc_body(body: &mut HeredocBody, base: Position) {
3625        body.span = body.span.rebased(base);
3626        for part in &mut body.parts {
3627            Self::rebase_heredoc_body_part(part, base);
3628        }
3629    }
3630
3631    fn rebase_pattern(pattern: &mut Pattern, base: Position) {
3632        pattern.span = pattern.span.rebased(base);
3633        Self::rebase_pattern_parts(&mut pattern.parts, base);
3634    }
3635
3636    fn rebase_word_parts(parts: &mut [WordPartNode], base: Position) {
3637        for part in parts {
3638            Self::rebase_word_part(part, base);
3639        }
3640    }
3641
3642    fn rebase_pattern_parts(parts: &mut [PatternPartNode], base: Position) {
3643        for part in parts {
3644            part.span = part.span.rebased(base);
3645            match &mut part.kind {
3646                PatternPart::CharClass(text) => text.rebased(base),
3647                PatternPart::Group { patterns, .. } => Self::rebase_patterns(patterns, base),
3648                PatternPart::Word(word) => Self::rebase_word(word, base),
3649                PatternPart::Literal(_) | PatternPart::AnyString | PatternPart::AnyChar => {}
3650            }
3651        }
3652    }
3653
3654    fn rebase_heredoc_body_part(part: &mut HeredocBodyPartNode, base: Position) {
3655        part.span = part.span.rebased(base);
3656        match &mut part.kind {
3657            HeredocBodyPart::Literal(_) | HeredocBodyPart::Variable(_) => {}
3658            HeredocBodyPart::CommandSubstitution { body, .. } => Self::rebase_stmt_seq(body, base),
3659            HeredocBodyPart::ArithmeticExpansion {
3660                expression,
3661                expression_ast,
3662                expression_word_ast,
3663                ..
3664            } => {
3665                expression.rebased(base);
3666                Self::rebase_word(expression_word_ast, base);
3667                if let Some(expr) = expression_ast {
3668                    Self::rebase_arithmetic_expr(expr, base);
3669                }
3670            }
3671            HeredocBodyPart::Parameter(parameter) => {
3672                parameter.span = parameter.span.rebased(base);
3673                parameter.raw_body.rebased(base);
3674                Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
3675            }
3676        }
3677    }
3678
3679    fn rebase_word_part(part: &mut WordPartNode, base: Position) {
3680        part.span = part.span.rebased(base);
3681        match &mut part.kind {
3682            WordPart::ZshQualifiedGlob(glob) => Self::rebase_zsh_qualified_glob(glob, base),
3683            WordPart::SingleQuoted { value, .. } => value.rebased(base),
3684            WordPart::DoubleQuoted { parts, .. } => Self::rebase_word_parts(parts, base),
3685            WordPart::Parameter(parameter) => {
3686                parameter.span = parameter.span.rebased(base);
3687                parameter.raw_body.rebased(base);
3688                Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
3689            }
3690            WordPart::ParameterExpansion {
3691                reference,
3692                operator,
3693                operand,
3694                operand_word_ast,
3695                ..
3696            } => {
3697                Self::rebase_var_ref(reference, base);
3698                match operator {
3699                    ParameterOp::RemovePrefixShort { pattern }
3700                    | ParameterOp::RemovePrefixLong { pattern }
3701                    | ParameterOp::RemoveSuffixShort { pattern }
3702                    | ParameterOp::RemoveSuffixLong { pattern } => {
3703                        Self::rebase_pattern(pattern, base);
3704                    }
3705                    ParameterOp::ReplaceFirst {
3706                        pattern,
3707                        replacement,
3708                        ..
3709                    }
3710                    | ParameterOp::ReplaceAll {
3711                        pattern,
3712                        replacement,
3713                        ..
3714                    } => {
3715                        Self::rebase_pattern(pattern, base);
3716                        replacement.rebased(base);
3717                    }
3718                    ParameterOp::UseDefault
3719                    | ParameterOp::AssignDefault
3720                    | ParameterOp::UseReplacement
3721                    | ParameterOp::Error
3722                    | ParameterOp::UpperFirst
3723                    | ParameterOp::UpperAll
3724                    | ParameterOp::LowerFirst
3725                    | ParameterOp::LowerAll => {}
3726                }
3727                if let Some(operand) = operand {
3728                    operand.rebased(base);
3729                }
3730                if let Some(word_ast) = operand_word_ast {
3731                    Self::rebase_word(word_ast, base);
3732                }
3733            }
3734            WordPart::ArrayAccess(reference)
3735            | WordPart::Length(reference)
3736            | WordPart::ArrayLength(reference)
3737            | WordPart::ArrayIndices(reference)
3738            | WordPart::Transformation { reference, .. } => Self::rebase_var_ref(reference, base),
3739            WordPart::Substring {
3740                reference,
3741                offset,
3742                offset_ast,
3743                offset_word_ast,
3744                length,
3745                length_ast,
3746                length_word_ast,
3747                ..
3748            }
3749            | WordPart::ArraySlice {
3750                reference,
3751                offset,
3752                offset_ast,
3753                offset_word_ast,
3754                length,
3755                length_ast,
3756                length_word_ast,
3757                ..
3758            } => {
3759                Self::rebase_var_ref(reference, base);
3760                offset.rebased(base);
3761                Self::rebase_word(offset_word_ast, base);
3762                if let Some(expr) = offset_ast {
3763                    Self::rebase_arithmetic_expr(expr, base);
3764                }
3765                if let Some(length) = length {
3766                    length.rebased(base);
3767                }
3768                if let Some(word_ast) = length_word_ast {
3769                    Self::rebase_word(word_ast, base);
3770                }
3771                if let Some(expr) = length_ast {
3772                    Self::rebase_arithmetic_expr(expr, base);
3773                }
3774            }
3775            WordPart::IndirectExpansion {
3776                reference,
3777                operator,
3778                operand,
3779                operand_word_ast,
3780                ..
3781            } => {
3782                Self::rebase_var_ref(reference, base);
3783                if let Some(operator) = operator {
3784                    Self::rebase_parameter_operator(operator, base);
3785                }
3786                if let Some(operand) = operand {
3787                    operand.rebased(base);
3788                }
3789                if let Some(word_ast) = operand_word_ast {
3790                    Self::rebase_word(word_ast, base);
3791                }
3792            }
3793            WordPart::ArithmeticExpansion {
3794                expression,
3795                expression_ast,
3796                expression_word_ast,
3797                ..
3798            } => {
3799                expression.rebased(base);
3800                Self::rebase_word(expression_word_ast, base);
3801                if let Some(expr) = expression_ast {
3802                    Self::rebase_arithmetic_expr(expr, base);
3803                }
3804            }
3805            WordPart::CommandSubstitution { body, .. }
3806            | WordPart::ProcessSubstitution { body, .. } => Self::rebase_stmt_seq(body, base),
3807            WordPart::Literal(_) | WordPart::Variable(_) | WordPart::PrefixMatch { .. } => {}
3808        }
3809    }
3810
3811    fn rebase_zsh_qualified_glob(glob: &mut ZshQualifiedGlob, base: Position) {
3812        glob.span = glob.span.rebased(base);
3813        for segment in &mut glob.segments {
3814            Self::rebase_zsh_glob_segment(segment, base);
3815        }
3816        if let Some(qualifiers) = &mut glob.qualifiers {
3817            Self::rebase_zsh_glob_qualifier_group(qualifiers, base);
3818        }
3819    }
3820
3821    fn rebase_zsh_glob_segment(segment: &mut ZshGlobSegment, base: Position) {
3822        match segment {
3823            ZshGlobSegment::Pattern(pattern) => Self::rebase_pattern(pattern, base),
3824            ZshGlobSegment::InlineControl(control) => {
3825                Self::rebase_zsh_inline_glob_control(control, base)
3826            }
3827        }
3828    }
3829
3830    fn rebase_zsh_inline_glob_control(control: &mut ZshInlineGlobControl, base: Position) {
3831        match control {
3832            ZshInlineGlobControl::CaseInsensitive { span }
3833            | ZshInlineGlobControl::Backreferences { span }
3834            | ZshInlineGlobControl::StartAnchor { span }
3835            | ZshInlineGlobControl::EndAnchor { span } => {
3836                *span = span.rebased(base);
3837            }
3838        }
3839    }
3840
3841    fn rebase_zsh_glob_qualifier_group(group: &mut ZshGlobQualifierGroup, base: Position) {
3842        group.span = group.span.rebased(base);
3843        for fragment in &mut group.fragments {
3844            match fragment {
3845                ZshGlobQualifier::Negation { span } | ZshGlobQualifier::Flag { span, .. } => {
3846                    *span = span.rebased(base);
3847                }
3848                ZshGlobQualifier::LetterSequence { text, span } => {
3849                    *span = span.rebased(base);
3850                    text.rebased(base);
3851                }
3852                ZshGlobQualifier::NumericArgument { span, start, end } => {
3853                    *span = span.rebased(base);
3854                    start.rebased(base);
3855                    if let Some(end) = end {
3856                        end.rebased(base);
3857                    }
3858                }
3859            }
3860        }
3861    }
3862
3863    fn rebase_parameter_expansion_syntax(syntax: &mut ParameterExpansionSyntax, base: Position) {
3864        match syntax {
3865            ParameterExpansionSyntax::Bourne(syntax) => match syntax {
3866                BourneParameterExpansion::Access { reference }
3867                | BourneParameterExpansion::Length { reference }
3868                | BourneParameterExpansion::Indices { reference }
3869                | BourneParameterExpansion::Transformation { reference, .. } => {
3870                    Self::rebase_var_ref(reference, base);
3871                }
3872                BourneParameterExpansion::Indirect {
3873                    reference,
3874                    operand,
3875                    operator,
3876                    operand_word_ast,
3877                    ..
3878                } => {
3879                    Self::rebase_var_ref(reference, base);
3880                    if let Some(operator) = operator {
3881                        Self::rebase_parameter_operator(operator, base);
3882                    }
3883                    if let Some(operand) = operand {
3884                        operand.rebased(base);
3885                    }
3886                    if let Some(word_ast) = operand_word_ast {
3887                        Self::rebase_word(word_ast, base);
3888                    }
3889                }
3890                BourneParameterExpansion::PrefixMatch { .. } => {}
3891                BourneParameterExpansion::Slice {
3892                    reference,
3893                    offset,
3894                    offset_ast,
3895                    offset_word_ast,
3896                    length,
3897                    length_ast,
3898                    length_word_ast,
3899                } => {
3900                    Self::rebase_var_ref(reference, base);
3901                    offset.rebased(base);
3902                    Self::rebase_word(offset_word_ast, base);
3903                    if let Some(expr) = offset_ast {
3904                        Self::rebase_arithmetic_expr(expr, base);
3905                    }
3906                    if let Some(length) = length {
3907                        length.rebased(base);
3908                    }
3909                    if let Some(word_ast) = length_word_ast {
3910                        Self::rebase_word(word_ast, base);
3911                    }
3912                    if let Some(expr) = length_ast {
3913                        Self::rebase_arithmetic_expr(expr, base);
3914                    }
3915                }
3916                BourneParameterExpansion::Operation {
3917                    reference,
3918                    operator,
3919                    operand,
3920                    operand_word_ast,
3921                    ..
3922                } => {
3923                    Self::rebase_var_ref(reference, base);
3924                    Self::rebase_parameter_operator(operator, base);
3925                    if let Some(operand) = operand {
3926                        operand.rebased(base);
3927                    }
3928                    if let Some(word_ast) = operand_word_ast {
3929                        Self::rebase_word(word_ast, base);
3930                    }
3931                }
3932            },
3933            ParameterExpansionSyntax::Zsh(syntax) => {
3934                match &mut syntax.target {
3935                    ZshExpansionTarget::Reference(reference) => {
3936                        Self::rebase_var_ref(reference, base)
3937                    }
3938                    ZshExpansionTarget::Word(word) => Self::rebase_word(word, base),
3939                    ZshExpansionTarget::Nested(parameter) => {
3940                        parameter.span = parameter.span.rebased(base);
3941                        parameter.raw_body.rebased(base);
3942                        Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
3943                    }
3944                    ZshExpansionTarget::Empty => {}
3945                }
3946                for modifier in &mut syntax.modifiers {
3947                    modifier.span = modifier.span.rebased(base);
3948                    if let Some(argument) = &mut modifier.argument {
3949                        argument.rebased(base);
3950                    }
3951                    if let Some(argument_word_ast) = &mut modifier.argument_word_ast {
3952                        Self::rebase_word(argument_word_ast, base);
3953                    }
3954                }
3955                if let Some(length_prefix) = &mut syntax.length_prefix {
3956                    *length_prefix = length_prefix.rebased(base);
3957                }
3958                if let Some(operation) = &mut syntax.operation {
3959                    match operation {
3960                        ZshExpansionOperation::PatternOperation {
3961                            operand,
3962                            operand_word_ast,
3963                            ..
3964                        }
3965                        | ZshExpansionOperation::Defaulting {
3966                            operand,
3967                            operand_word_ast,
3968                            ..
3969                        }
3970                        | ZshExpansionOperation::TrimOperation {
3971                            operand,
3972                            operand_word_ast,
3973                            ..
3974                        } => {
3975                            operand.rebased(base);
3976                            Self::rebase_word(operand_word_ast, base);
3977                        }
3978                        ZshExpansionOperation::Unknown { text, word_ast } => {
3979                            text.rebased(base);
3980                            Self::rebase_word(word_ast, base);
3981                        }
3982                        ZshExpansionOperation::ReplacementOperation {
3983                            pattern,
3984                            pattern_word_ast,
3985                            replacement,
3986                            replacement_word_ast,
3987                            ..
3988                        } => {
3989                            pattern.rebased(base);
3990                            Self::rebase_word(pattern_word_ast, base);
3991                            if let Some(replacement) = replacement {
3992                                replacement.rebased(base);
3993                            }
3994                            if let Some(replacement_word_ast) = replacement_word_ast {
3995                                Self::rebase_word(replacement_word_ast, base);
3996                            }
3997                        }
3998                        ZshExpansionOperation::Slice {
3999                            offset,
4000                            offset_word_ast,
4001                            length,
4002                            length_word_ast,
4003                        } => {
4004                            offset.rebased(base);
4005                            Self::rebase_word(offset_word_ast, base);
4006                            if let Some(length) = length {
4007                                length.rebased(base);
4008                            }
4009                            if let Some(length_word_ast) = length_word_ast {
4010                                Self::rebase_word(length_word_ast, base);
4011                            }
4012                        }
4013                    }
4014                }
4015            }
4016        }
4017    }
4018
4019    fn rebase_parameter_operator(operator: &mut ParameterOp, base: Position) {
4020        match operator {
4021            ParameterOp::RemovePrefixShort { pattern }
4022            | ParameterOp::RemovePrefixLong { pattern }
4023            | ParameterOp::RemoveSuffixShort { pattern }
4024            | ParameterOp::RemoveSuffixLong { pattern } => {
4025                Self::rebase_pattern(pattern, base);
4026            }
4027            ParameterOp::ReplaceFirst {
4028                pattern,
4029                replacement,
4030                replacement_word_ast,
4031            }
4032            | ParameterOp::ReplaceAll {
4033                pattern,
4034                replacement,
4035                replacement_word_ast,
4036            } => {
4037                Self::rebase_pattern(pattern, base);
4038                replacement.rebased(base);
4039                Self::rebase_word(replacement_word_ast, base);
4040            }
4041            ParameterOp::UseDefault
4042            | ParameterOp::AssignDefault
4043            | ParameterOp::UseReplacement
4044            | ParameterOp::Error
4045            | ParameterOp::UpperFirst
4046            | ParameterOp::UpperAll
4047            | ParameterOp::LowerFirst
4048            | ParameterOp::LowerAll => {}
4049        }
4050    }
4051
4052    fn rebase_conditional_expr(expr: &mut ConditionalExpr, base: Position) {
4053        match expr {
4054            ConditionalExpr::Binary(binary) => {
4055                binary.op_span = binary.op_span.rebased(base);
4056                Self::rebase_conditional_expr(&mut binary.left, base);
4057                Self::rebase_conditional_expr(&mut binary.right, base);
4058            }
4059            ConditionalExpr::Unary(unary) => {
4060                unary.op_span = unary.op_span.rebased(base);
4061                Self::rebase_conditional_expr(&mut unary.expr, base);
4062            }
4063            ConditionalExpr::Parenthesized(paren) => {
4064                paren.left_paren_span = paren.left_paren_span.rebased(base);
4065                paren.right_paren_span = paren.right_paren_span.rebased(base);
4066                Self::rebase_conditional_expr(&mut paren.expr, base);
4067            }
4068            ConditionalExpr::Word(word) | ConditionalExpr::Regex(word) => {
4069                Self::rebase_word(word, base);
4070            }
4071            ConditionalExpr::Pattern(pattern) => Self::rebase_pattern(pattern, base),
4072            ConditionalExpr::VarRef(var_ref) => Self::rebase_var_ref(var_ref, base),
4073        }
4074    }
4075
4076    fn rebase_arithmetic_expr(expr: &mut ArithmeticExprNode, base: Position) {
4077        expr.span = expr.span.rebased(base);
4078        match &mut expr.kind {
4079            ArithmeticExpr::Number(text) => text.rebased(base),
4080            ArithmeticExpr::Variable(_) => {}
4081            ArithmeticExpr::Indexed { index, .. } => Self::rebase_arithmetic_expr(index, base),
4082            ArithmeticExpr::ShellWord(word) => Self::rebase_word(word, base),
4083            ArithmeticExpr::Parenthesized { expression } => {
4084                Self::rebase_arithmetic_expr(expression, base)
4085            }
4086            ArithmeticExpr::Unary { expr, .. } | ArithmeticExpr::Postfix { expr, .. } => {
4087                Self::rebase_arithmetic_expr(expr, base)
4088            }
4089            ArithmeticExpr::Binary { left, right, .. } => {
4090                Self::rebase_arithmetic_expr(left, base);
4091                Self::rebase_arithmetic_expr(right, base);
4092            }
4093            ArithmeticExpr::Conditional {
4094                condition,
4095                then_expr,
4096                else_expr,
4097            } => {
4098                Self::rebase_arithmetic_expr(condition, base);
4099                Self::rebase_arithmetic_expr(then_expr, base);
4100                Self::rebase_arithmetic_expr(else_expr, base);
4101            }
4102            ArithmeticExpr::Assignment { target, value, .. } => {
4103                Self::rebase_arithmetic_lvalue(target, base);
4104                Self::rebase_arithmetic_expr(value, base);
4105            }
4106        }
4107    }
4108
4109    fn rebase_arithmetic_lvalue(target: &mut ArithmeticLvalue, base: Position) {
4110        match target {
4111            ArithmeticLvalue::Variable(_) => {}
4112            ArithmeticLvalue::Indexed { index, .. } => Self::rebase_arithmetic_expr(index, base),
4113        }
4114    }
4115
4116    fn push_word_part(parts: &mut WordPartBuffer, part: WordPart, start: Position, end: Position) {
4117        Self::push_word_part_node(
4118            parts,
4119            WordPartNode::new(part, Span::from_positions(start, end)),
4120        );
4121    }
4122
4123    fn push_word_part_node(parts: &mut WordPartBuffer, part: WordPartNode) {
4124        parts.push(part);
4125    }
4126
4127    fn flush_literal_part(
4128        &self,
4129        parts: &mut WordPartBuffer,
4130        current: &mut String,
4131        current_start: Position,
4132        end: Position,
4133        source_backed: bool,
4134    ) {
4135        if !current.is_empty() {
4136            Self::push_word_part(
4137                parts,
4138                WordPart::Literal(self.literal_text(
4139                    std::mem::take(current),
4140                    current_start,
4141                    end,
4142                    source_backed,
4143                )),
4144                current_start,
4145                end,
4146            );
4147        }
4148    }
4149
4150    fn word_part_buffer_with_capacity(capacity: usize) -> WordPartBuffer {
4151        if capacity <= 2 {
4152            WordPartBuffer::new()
4153        } else {
4154            WordPartBuffer::with_capacity(capacity)
4155        }
4156    }
4157
4158    fn literal_text(
4159        &self,
4160        text: String,
4161        start: Position,
4162        end: Position,
4163        source_backed: bool,
4164    ) -> LiteralText {
4165        let span = Span::from_positions(start, end);
4166        if self.source_matches(span, &text) {
4167            LiteralText::source()
4168        } else if source_backed {
4169            LiteralText::cooked_source(text)
4170        } else {
4171            LiteralText::owned(text)
4172        }
4173    }
4174
4175    fn literal_text_from_str(
4176        &self,
4177        text: &str,
4178        start: Position,
4179        end: Position,
4180        source_backed: bool,
4181    ) -> LiteralText {
4182        self.literal_text_impl(text, None, start, end, source_backed)
4183    }
4184
4185    fn literal_text_impl(
4186        &self,
4187        text: &str,
4188        owned: Option<String>,
4189        start: Position,
4190        end: Position,
4191        source_backed: bool,
4192    ) -> LiteralText {
4193        let span = Span::from_positions(start, end);
4194        if self.source_matches(span, text) {
4195            LiteralText::source()
4196        } else if source_backed {
4197            LiteralText::cooked_source(owned.unwrap_or_else(|| text.to_owned()))
4198        } else {
4199            LiteralText::owned(owned.unwrap_or_else(|| text.to_owned()))
4200        }
4201    }
4202
4203    fn source_text(&self, text: String, start: Position, end: Position) -> SourceText {
4204        let span = Span::from_positions(start, end);
4205        if self.source_matches(span, &text) {
4206            SourceText::source(span)
4207        } else {
4208            SourceText::cooked(span, text)
4209        }
4210    }
4211
4212    fn source_text_from_str(&self, text: &str, start: Position, end: Position) -> SourceText {
4213        self.source_text_impl(text, None, start, end)
4214    }
4215
4216    fn source_text_impl(
4217        &self,
4218        text: &str,
4219        owned: Option<String>,
4220        start: Position,
4221        end: Position,
4222    ) -> SourceText {
4223        let span = Span::from_positions(start, end);
4224        if self.source_matches(span, text) {
4225            SourceText::source(span)
4226        } else {
4227            SourceText::cooked(span, owned.unwrap_or_else(|| text.to_owned()))
4228        }
4229    }
4230
4231    fn empty_source_text(&self, pos: Position) -> SourceText {
4232        SourceText::source(Span::from_positions(pos, pos))
4233    }
4234
4235    fn input_prefix_ends_with(&self, end_offset: usize, ch: char) -> bool {
4236        self.input
4237            .get(..end_offset)
4238            .is_some_and(|prefix| prefix.ends_with(ch))
4239    }
4240
4241    fn input_span_ends_with(&self, start: Position, end: Position, ch: char) -> bool {
4242        self.input
4243            .get(start.offset..end.offset)
4244            .is_some_and(|slice| slice.ends_with(ch))
4245    }
4246
4247    fn input_suffix_starts_with(&self, start_offset: usize, ch: char) -> bool {
4248        self.input
4249            .get(start_offset..)
4250            .is_some_and(|suffix| suffix.starts_with(ch))
4251    }
4252
4253    fn subscript_source_text(&self, raw: &str, span: Span) -> (SourceText, Option<SourceText>) {
4254        if raw.len() >= 2
4255            && ((raw.starts_with('"') && raw.ends_with('"'))
4256                || (raw.starts_with('\'') && raw.ends_with('\'')))
4257        {
4258            let raw_text = raw.to_string();
4259            let raw = if self.source_matches(span, raw) {
4260                SourceText::source(span)
4261            } else {
4262                SourceText::cooked(span, raw_text.clone())
4263            };
4264            let cooked = raw_text[1..raw_text.len() - 1].to_string();
4265            return (self.source_text(cooked, span.start, span.end), Some(raw));
4266        }
4267
4268        let text = if self.source_matches(span, raw) {
4269            SourceText::source(span)
4270        } else {
4271            SourceText::cooked(span, raw.to_string())
4272        };
4273        (text, None)
4274    }
4275
4276    fn subscript_from_source_text(
4277        &self,
4278        text: SourceText,
4279        raw: Option<SourceText>,
4280        interpretation: SubscriptInterpretation,
4281    ) -> Subscript {
4282        let kind = match text.slice(self.input).trim() {
4283            "@" => SubscriptKind::Selector(SubscriptSelector::At),
4284            "*" => SubscriptKind::Selector(SubscriptSelector::Star),
4285            _ => SubscriptKind::Ordinary,
4286        };
4287        let word_ast = if matches!(kind, SubscriptKind::Ordinary) {
4288            Some(self.parse_source_text_as_word(raw.as_ref().unwrap_or(&text)))
4289        } else {
4290            None
4291        };
4292        let arithmetic_ast = if matches!(kind, SubscriptKind::Ordinary) {
4293            self.simple_subscript_arithmetic_ast(&text)
4294                .or_else(|| self.maybe_parse_source_text_as_arithmetic(&text))
4295        } else {
4296            None
4297        };
4298        Subscript {
4299            text,
4300            raw,
4301            kind,
4302            interpretation,
4303            word_ast,
4304            arithmetic_ast,
4305        }
4306    }
4307
4308    fn simple_subscript_arithmetic_ast(&self, text: &SourceText) -> Option<ArithmeticExprNode> {
4309        if !text.is_source_backed() {
4310            return None;
4311        }
4312
4313        let raw = text.slice(self.input);
4314        if raw.is_empty() || raw.trim() != raw {
4315            return None;
4316        }
4317
4318        let span = text.span();
4319        if raw.bytes().all(|byte| byte.is_ascii_digit()) {
4320            return Some(ArithmeticExprNode::new(
4321                ArithmeticExpr::Number(SourceText::source(span)),
4322                span,
4323            ));
4324        }
4325
4326        if Self::is_valid_identifier(raw) {
4327            return Some(ArithmeticExprNode::new(
4328                ArithmeticExpr::Variable(Name::from(raw)),
4329                span,
4330            ));
4331        }
4332
4333        None
4334    }
4335
4336    fn subscript_from_text(
4337        &self,
4338        raw: &str,
4339        span: Span,
4340        interpretation: SubscriptInterpretation,
4341    ) -> Subscript {
4342        let (text, raw) = self.subscript_source_text(raw, span);
4343        self.subscript_from_source_text(text, raw, interpretation)
4344    }
4345
4346    fn var_ref(
4347        &self,
4348        name: impl Into<Name>,
4349        name_span: Span,
4350        subscript: Option<Subscript>,
4351        span: Span,
4352    ) -> VarRef {
4353        VarRef {
4354            name: name.into(),
4355            name_span,
4356            subscript: subscript.map(Box::new),
4357            span,
4358        }
4359    }
4360
4361    fn parameter_var_ref(
4362        &self,
4363        part_start: Position,
4364        prefix: &str,
4365        name: &str,
4366        subscript: Option<Subscript>,
4367        part_end: Position,
4368    ) -> VarRef {
4369        let name_start = part_start.advanced_by(prefix);
4370        let name_span = Span::from_positions(name_start, name_start.advanced_by(name));
4371        self.var_ref(
4372            Name::from(name),
4373            name_span,
4374            subscript,
4375            Span::from_positions(part_start, part_end),
4376        )
4377    }
4378
4379    fn parameter_word_part_from_legacy(
4380        &self,
4381        part: WordPart,
4382        part_start: Position,
4383        part_end: Position,
4384        source_backed: bool,
4385    ) -> WordPart {
4386        let span = Span::from_positions(part_start, part_end);
4387        let raw_body = self.parameter_raw_body_from_legacy(&part, span, source_backed);
4388        let raw_body_text = raw_body.slice(self.input).to_string();
4389
4390        let syntax = match part {
4391            WordPart::ParameterExpansion {
4392                reference,
4393                operator,
4394                operand,
4395                operand_word_ast,
4396                colon_variant,
4397            } => Some(BourneParameterExpansion::Operation {
4398                reference,
4399                operator: self.enrich_parameter_operator(operator),
4400                operand,
4401                operand_word_ast,
4402                colon_variant,
4403            }),
4404            WordPart::Length(reference) | WordPart::ArrayLength(reference) => {
4405                Some(BourneParameterExpansion::Length { reference })
4406            }
4407            WordPart::ArrayAccess(reference) => {
4408                Some(BourneParameterExpansion::Access { reference })
4409            }
4410            WordPart::ArrayIndices(reference) => {
4411                Some(BourneParameterExpansion::Indices { reference })
4412            }
4413            WordPart::Substring {
4414                reference,
4415                offset,
4416                offset_ast,
4417                offset_word_ast,
4418                length,
4419                length_ast,
4420                length_word_ast,
4421            }
4422            | WordPart::ArraySlice {
4423                reference,
4424                offset,
4425                offset_ast,
4426                offset_word_ast,
4427                length,
4428                length_ast,
4429                length_word_ast,
4430            } => Some(BourneParameterExpansion::Slice {
4431                reference,
4432                offset,
4433                offset_ast,
4434                offset_word_ast,
4435                length,
4436                length_ast,
4437                length_word_ast,
4438            }),
4439            WordPart::IndirectExpansion {
4440                reference,
4441                operator,
4442                operand,
4443                operand_word_ast,
4444                colon_variant,
4445            } => Some(BourneParameterExpansion::Indirect {
4446                reference,
4447                operator: operator.map(|operator| self.enrich_parameter_operator(operator)),
4448                operand,
4449                operand_word_ast,
4450                colon_variant,
4451            }),
4452            WordPart::PrefixMatch { prefix, kind } => {
4453                Some(BourneParameterExpansion::PrefixMatch { prefix, kind })
4454            }
4455            WordPart::Transformation {
4456                reference,
4457                operator,
4458            } => Some(BourneParameterExpansion::Transformation {
4459                reference,
4460                operator,
4461            }),
4462            WordPart::Variable(name) if raw_body_text == name.as_str() => {
4463                Some(BourneParameterExpansion::Access {
4464                    reference: self.parameter_var_ref(
4465                        part_start,
4466                        "${",
4467                        name.as_str(),
4468                        None,
4469                        part_end,
4470                    ),
4471                })
4472            }
4473            other => return other,
4474        };
4475
4476        let Some(syntax) = syntax else {
4477            unreachable!("matched Some above");
4478        };
4479        WordPart::Parameter(ParameterExpansion {
4480            syntax: ParameterExpansionSyntax::Bourne(syntax),
4481            span,
4482            raw_body,
4483        })
4484    }
4485
4486    fn enrich_parameter_operator(&self, operator: ParameterOp) -> ParameterOp {
4487        match operator {
4488            ParameterOp::ReplaceFirst {
4489                pattern,
4490                replacement,
4491                ..
4492            } => ParameterOp::ReplaceFirst {
4493                pattern,
4494                replacement_word_ast: self.parse_source_text_as_word(&replacement),
4495                replacement,
4496            },
4497            ParameterOp::ReplaceAll {
4498                pattern,
4499                replacement,
4500                ..
4501            } => ParameterOp::ReplaceAll {
4502                pattern,
4503                replacement_word_ast: self.parse_source_text_as_word(&replacement),
4504                replacement,
4505            },
4506            ParameterOp::UseDefault
4507            | ParameterOp::AssignDefault
4508            | ParameterOp::UseReplacement
4509            | ParameterOp::Error
4510            | ParameterOp::RemovePrefixShort { .. }
4511            | ParameterOp::RemovePrefixLong { .. }
4512            | ParameterOp::RemoveSuffixShort { .. }
4513            | ParameterOp::RemoveSuffixLong { .. }
4514            | ParameterOp::UpperFirst
4515            | ParameterOp::UpperAll
4516            | ParameterOp::LowerFirst
4517            | ParameterOp::LowerAll => operator,
4518        }
4519    }
4520
4521    fn parameter_raw_body_from_legacy(
4522        &self,
4523        part: &WordPart,
4524        span: Span,
4525        source_backed: bool,
4526    ) -> SourceText {
4527        if source_backed && span.end.offset <= self.input.len() {
4528            let syntax = span.slice(self.input);
4529            if let Some(body) = syntax
4530                .strip_prefix("${")
4531                .and_then(|syntax| syntax.strip_suffix('}'))
4532            {
4533                let start = span.start.advanced_by("${");
4534                let end = start.advanced_by(body);
4535                return SourceText::source(Span::from_positions(start, end));
4536            }
4537        }
4538
4539        let mut syntax = String::new();
4540        self.push_word_part_syntax(&mut syntax, part, span);
4541        let body = syntax
4542            .strip_prefix("${")
4543            .and_then(|syntax| syntax.strip_suffix('}'))
4544            .unwrap_or(syntax.as_str())
4545            .to_string();
4546        SourceText::from(body)
4547    }
4548
4549    fn zsh_parameter_word_part(
4550        &mut self,
4551        raw_body: SourceText,
4552        part_start: Position,
4553        part_end: Position,
4554    ) -> WordPart {
4555        let syntax = self.parse_zsh_parameter_syntax(&raw_body, raw_body.span().start);
4556        WordPart::Parameter(ParameterExpansion {
4557            syntax: ParameterExpansionSyntax::Zsh(syntax),
4558            span: Span::from_positions(part_start, part_end),
4559            raw_body,
4560        })
4561    }
4562
4563    fn parse_zsh_modifier_group(
4564        &self,
4565        text: &str,
4566        base: Position,
4567        start: usize,
4568    ) -> Option<(usize, Vec<ZshModifier>)> {
4569        let rest = text.get(start..)?;
4570        if !rest.starts_with('(') {
4571            return None;
4572        }
4573
4574        let close_rel = rest[1..].find(')')?;
4575        let close = start + 1 + close_rel;
4576        let group_text = &text[start..=close];
4577        let inner = &text[start + 1..close];
4578        let group_start = base.advanced_by(&text[..start]);
4579        let group_span = Span::from_positions(group_start, group_start.advanced_by(group_text));
4580        let mut modifiers = Vec::new();
4581        let mut index = 0usize;
4582
4583        while index < inner.len() {
4584            let name = inner[index..].chars().next()?;
4585            index += name.len_utf8();
4586
4587            let mut argument_delimiter = None;
4588            let mut argument = None;
4589            if matches!(name, 's' | 'j')
4590                && let Some(delimiter) = inner[index..].chars().next()
4591            {
4592                index += delimiter.len_utf8();
4593                let argument_start = index;
4594                while index < inner.len() {
4595                    let ch = inner[index..].chars().next()?;
4596                    if ch == delimiter {
4597                        let argument_text = &inner[argument_start..index];
4598                        let argument_base =
4599                            group_start.advanced_by(&group_text[..1 + argument_start]);
4600                        let argument_end = argument_base.advanced_by(argument_text);
4601                        argument_delimiter = Some(delimiter);
4602                        argument = Some(self.source_text(
4603                            argument_text.to_string(),
4604                            argument_base,
4605                            argument_end,
4606                        ));
4607                        index += delimiter.len_utf8();
4608                        break;
4609                    }
4610                    index += ch.len_utf8();
4611                }
4612            }
4613
4614            let argument_word_ast = argument
4615                .as_ref()
4616                .map(|argument| self.parse_source_text_as_word(argument));
4617
4618            modifiers.push(ZshModifier {
4619                name,
4620                argument,
4621                argument_word_ast,
4622                argument_delimiter,
4623                span: group_span,
4624            });
4625        }
4626
4627        Some((close + 1, modifiers))
4628    }
4629
4630    fn parse_zsh_parameter_syntax(
4631        &mut self,
4632        raw_body: &SourceText,
4633        base: Position,
4634    ) -> ZshParameterExpansion {
4635        let text = raw_body.slice(self.input);
4636        let mut index = 0;
4637        let mut modifiers = Vec::new();
4638        let mut length_prefix = None;
4639        let source_backed = raw_body.is_source_backed();
4640
4641        while text[index..].starts_with('(')
4642            && let Some((next_index, group_modifiers)) =
4643                self.parse_zsh_modifier_group(text, base, index)
4644        {
4645            modifiers.extend(group_modifiers);
4646            index = next_index;
4647        }
4648
4649        while index < text.len() {
4650            let Some(flag) = text[index..].chars().next() else {
4651                break;
4652            };
4653            match flag {
4654                '=' | '~' | '^' => {
4655                    let modifier_start = base.advanced_by(&text[..index]);
4656                    let modifier_end =
4657                        modifier_start.advanced_by(&text[index..index + flag.len_utf8()]);
4658                    modifiers.push(ZshModifier {
4659                        name: flag,
4660                        argument: None,
4661                        argument_word_ast: None,
4662                        argument_delimiter: None,
4663                        span: Span::from_positions(modifier_start, modifier_end),
4664                    });
4665                    index += flag.len_utf8();
4666                }
4667                '#' if length_prefix.is_none() => {
4668                    let prefix_start = base.advanced_by(&text[..index]);
4669                    let prefix_end = prefix_start.advanced_by("#");
4670                    length_prefix = Some(Span::from_positions(prefix_start, prefix_end));
4671                    index += '#'.len_utf8();
4672                }
4673                _ => break,
4674            }
4675        }
4676
4677        let (target, operation_index) = if text[index..].starts_with("${") {
4678            let end = self
4679                .find_matching_parameter_end(&text[index..])
4680                .unwrap_or(text.len() - index);
4681            let nested_text = &text[index..index + end];
4682            let target =
4683                self.parse_nested_parameter_target(nested_text, base.advanced_by(&text[..index]));
4684            (target, index + end)
4685        } else if text[index..].starts_with(':') || text[index..].is_empty() {
4686            (ZshExpansionTarget::Empty, index)
4687        } else {
4688            let end = self
4689                .find_zsh_operation_start(&text[index..])
4690                .map(|offset| index + offset)
4691                .unwrap_or(text.len());
4692            let raw_target = &text[index..end];
4693            let trimmed = raw_target.trim();
4694            let target = if trimmed.is_empty() {
4695                ZshExpansionTarget::Empty
4696            } else {
4697                let leading = raw_target
4698                    .len()
4699                    .saturating_sub(raw_target.trim_start().len());
4700                let target_base = base.advanced_by(&text[..index + leading]);
4701                self.parse_zsh_target_from_text(
4702                    trimmed,
4703                    target_base,
4704                    source_backed && leading == 0 && trimmed.len() == raw_target.len(),
4705                )
4706            };
4707            (target, end)
4708        };
4709
4710        let operation = (operation_index < text.len()).then(|| {
4711            self.parse_zsh_parameter_operation(
4712                &text[operation_index..],
4713                base.advanced_by(&text[..operation_index]),
4714            )
4715        });
4716
4717        ZshParameterExpansion {
4718            target,
4719            modifiers,
4720            length_prefix,
4721            operation,
4722        }
4723    }
4724
4725    fn parse_zsh_target_from_text(
4726        &mut self,
4727        text: &str,
4728        base: Position,
4729        source_backed: bool,
4730    ) -> ZshExpansionTarget {
4731        let trimmed = text.trim();
4732        if trimmed.is_empty() {
4733            return ZshExpansionTarget::Empty;
4734        }
4735
4736        if trimmed.starts_with("${") && trimmed.ends_with('}') {
4737            return self.parse_nested_parameter_target(trimmed, base);
4738        }
4739
4740        if let Some(reference) = self.maybe_parse_loose_var_ref_target(trimmed) {
4741            return ZshExpansionTarget::Reference(reference);
4742        }
4743
4744        let span = Span::from_positions(base, base.advanced_by(trimmed));
4745        let word = self.parse_word_with_context(trimmed, span, base, source_backed);
4746        if let Some(reference) =
4747            self.parse_var_ref_from_word(&word, SubscriptInterpretation::Contextual)
4748        {
4749            ZshExpansionTarget::Reference(reference)
4750        } else {
4751            ZshExpansionTarget::Word(word)
4752        }
4753    }
4754
4755    fn maybe_parse_loose_var_ref_target(&self, text: &str) -> Option<VarRef> {
4756        let trimmed = text.trim();
4757        Self::looks_like_plain_parameter_access(trimmed).then(|| self.parse_loose_var_ref(trimmed))
4758    }
4759
4760    fn is_plain_special_parameter_name(name: &str) -> bool {
4761        matches!(name, "#" | "$" | "!" | "*" | "@" | "?" | "-") || name == "0"
4762    }
4763
4764    fn looks_like_plain_parameter_access(text: &str) -> bool {
4765        let trimmed = text.trim();
4766        if trimmed.is_empty() {
4767            return false;
4768        }
4769
4770        let name = if let Some(open) = trimmed.find('[') {
4771            if !trimmed.ends_with(']') {
4772                return false;
4773            }
4774            &trimmed[..open]
4775        } else {
4776            trimmed
4777        };
4778
4779        Self::is_valid_identifier(name)
4780            || name.bytes().all(|byte| byte.is_ascii_digit())
4781            || Self::is_plain_special_parameter_name(name)
4782    }
4783
4784    fn parse_nested_parameter_target(&mut self, text: &str, base: Position) -> ZshExpansionTarget {
4785        if !(text.starts_with("${") && text.ends_with('}')) {
4786            return self.parse_zsh_target_from_text(text, base, false);
4787        }
4788
4789        let raw_body_start = base.advanced_by("${");
4790        let raw_body = self.source_text(
4791            text[2..text.len() - 1].to_string(),
4792            raw_body_start,
4793            base.advanced_by(&text[..text.len() - 1]),
4794        );
4795        let raw_body_text = raw_body.slice(self.input);
4796        let has_operation = self.find_zsh_operation_start(raw_body_text).is_some();
4797        let syntax = if Self::looks_like_plain_parameter_access(raw_body_text) && !has_operation {
4798            ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access {
4799                reference: self.parse_loose_var_ref(raw_body_text),
4800            })
4801        } else if raw_body_text.starts_with('(')
4802            || raw_body_text.starts_with(':')
4803            || raw_body_text.starts_with('=')
4804            || raw_body_text.starts_with('^')
4805            || raw_body_text.starts_with('~')
4806            || raw_body_text.starts_with('.')
4807            || raw_body_text.starts_with('#')
4808            || raw_body_text.starts_with('"')
4809            || raw_body_text.starts_with('\'')
4810            || raw_body_text.starts_with('$')
4811            || has_operation
4812        {
4813            ParameterExpansionSyntax::Zsh(
4814                self.parse_zsh_parameter_syntax(&raw_body, raw_body_start),
4815            )
4816        } else {
4817            ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access {
4818                reference: self.parse_loose_var_ref(raw_body_text),
4819            })
4820        };
4821
4822        ZshExpansionTarget::Nested(Box::new(ParameterExpansion {
4823            syntax,
4824            span: Span::from_positions(base, base.advanced_by(text)),
4825            raw_body,
4826        }))
4827    }
4828
4829    fn parse_loose_var_ref(&self, text: &str) -> VarRef {
4830        let trimmed = text.trim();
4831        if let Some(open) = trimmed.find('[')
4832            && trimmed.ends_with(']')
4833        {
4834            let name = &trimmed[..open];
4835            let subscript_text = &trimmed[open + 1..trimmed.len() - 1];
4836            let subscript = self.subscript_from_source_text(
4837                SourceText::from(subscript_text.to_string()),
4838                None,
4839                SubscriptInterpretation::Contextual,
4840            );
4841            return VarRef {
4842                name: Name::from(name),
4843                name_span: Span::new(),
4844                subscript: Some(Box::new(subscript)),
4845                span: Span::new(),
4846            };
4847        }
4848
4849        VarRef {
4850            name: Name::from(trimmed),
4851            name_span: Span::new(),
4852            subscript: None,
4853            span: Span::new(),
4854        }
4855    }
4856
4857    fn find_matching_parameter_end(&self, text: &str) -> Option<usize> {
4858        let mut depth = 0_i32;
4859        let mut chars = text.char_indices().peekable();
4860
4861        while let Some((index, ch)) = chars.next() {
4862            match ch {
4863                '$' if chars.peek().is_some_and(|(_, next)| *next == '{') => {
4864                    depth += 1;
4865                }
4866                '}' => {
4867                    depth -= 1;
4868                    if depth == 0 {
4869                        return Some(index + ch.len_utf8());
4870                    }
4871                }
4872                _ => {}
4873            }
4874        }
4875
4876        None
4877    }
4878
4879    fn find_zsh_operation_start(&self, text: &str) -> Option<usize> {
4880        let mut bracket_depth = 0_usize;
4881        let mut in_single = false;
4882        let mut in_double = false;
4883        let mut escaped = false;
4884
4885        for (index, ch) in text.char_indices() {
4886            if escaped {
4887                escaped = false;
4888                continue;
4889            }
4890
4891            match ch {
4892                '\\' if !in_single => escaped = true,
4893                '\'' if !in_double => in_single = !in_single,
4894                '"' if !in_single => in_double = !in_double,
4895                '[' if !in_single && !in_double => bracket_depth += 1,
4896                ']' if !in_single && !in_double && bracket_depth > 0 => bracket_depth -= 1,
4897                ':' if !in_single && !in_double && bracket_depth == 0 => return Some(index),
4898                '#' | '%' | '/' | '^' | ',' | '~'
4899                    if !in_single && !in_double && bracket_depth == 0 && index > 0 =>
4900                {
4901                    return Some(index);
4902                }
4903                _ => {}
4904            }
4905        }
4906
4907        None
4908    }
4909
4910    fn zsh_operation_source_text(
4911        &self,
4912        text: &str,
4913        base: Position,
4914        start: usize,
4915        end: usize,
4916    ) -> SourceText {
4917        self.source_text(
4918            text[start..end].to_string(),
4919            base.advanced_by(&text[..start]),
4920            base.advanced_by(&text[..end]),
4921        )
4922    }
4923
4924    fn find_zsh_top_level_delimiter(&self, text: &str, delimiter: char) -> Option<usize> {
4925        let mut chars = text.char_indices().peekable();
4926        let mut in_single = false;
4927        let mut in_double = false;
4928        let mut escaped = false;
4929        let mut brace_depth = 0_usize;
4930        let mut paren_depth = 0_usize;
4931
4932        while let Some((index, ch)) = chars.next() {
4933            if escaped {
4934                escaped = false;
4935                continue;
4936            }
4937
4938            match ch {
4939                '\\' if !in_single => escaped = true,
4940                '\'' if !in_double => in_single = !in_single,
4941                '"' if !in_single => in_double = !in_double,
4942                '$' if !in_single => {
4943                    if let Some((_, next)) = chars.peek() {
4944                        if *next == '{' {
4945                            brace_depth += 1;
4946                            chars.next();
4947                        } else if *next == '(' {
4948                            paren_depth += 1;
4949                            chars.next();
4950                            if let Some((_, after)) = chars.peek()
4951                                && *after == '('
4952                            {
4953                                paren_depth += 1;
4954                                chars.next();
4955                            }
4956                        }
4957                    }
4958                }
4959                '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
4960                ')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
4961                _ if ch == delimiter
4962                    && !in_single
4963                    && !in_double
4964                    && brace_depth == 0
4965                    && paren_depth == 0 =>
4966                {
4967                    return Some(index);
4968                }
4969                _ => {}
4970            }
4971        }
4972
4973        None
4974    }
4975
4976    fn zsh_simple_modifier_suffix_segment(segment: &str) -> bool {
4977        let mut chars = segment.chars();
4978        let Some(first) = chars.next() else {
4979            return false;
4980        };
4981
4982        match first {
4983            'a' | 'A' | 'c' | 'e' | 'l' | 'P' | 'q' | 'Q' | 'r' | 'u' => chars.next().is_none(),
4984            'h' | 't' => chars.all(|ch| ch.is_ascii_digit()),
4985            _ => false,
4986        }
4987    }
4988
4989    fn zsh_modifier_suffix_candidate(rest: &str) -> bool {
4990        if rest.is_empty() {
4991            return false;
4992        }
4993
4994        let Some(first) = rest.chars().next() else {
4995            return false;
4996        };
4997        if first.is_ascii_digit()
4998            || first.is_ascii_whitespace()
4999            || matches!(first, '$' | '\'' | '"' | '(' | '{')
5000        {
5001            return false;
5002        }
5003
5004        rest.split(':')
5005            .all(Self::zsh_simple_modifier_suffix_segment)
5006    }
5007
5008    fn zsh_slice_candidate(rest: &str) -> bool {
5009        let Some(first) = rest.chars().next() else {
5010            return false;
5011        };
5012
5013        !Self::zsh_modifier_suffix_candidate(rest)
5014            && (first.is_ascii_alphanumeric()
5015                || first == '_'
5016                || first.is_ascii_whitespace()
5017                || matches!(first, '$' | '\'' | '"' | '(' | '{'))
5018    }
5019
5020    fn parse_zsh_parameter_operation(&self, text: &str, base: Position) -> ZshExpansionOperation {
5021        if let Some(operand) = text.strip_prefix(":#") {
5022            let operand = self.source_text(
5023                operand.to_string(),
5024                base.advanced_by(":#"),
5025                base.advanced_by(text),
5026            );
5027            return ZshExpansionOperation::PatternOperation {
5028                kind: ZshPatternOp::Filter,
5029                operand_word_ast: self.parse_source_text_as_word(&operand),
5030                operand,
5031            };
5032        }
5033
5034        if let Some((kind, operand)) = text
5035            .strip_prefix(":-")
5036            .map(|operand| (ZshDefaultingOp::UseDefault, operand))
5037            .or_else(|| {
5038                text.strip_prefix(":=")
5039                    .map(|operand| (ZshDefaultingOp::AssignDefault, operand))
5040            })
5041            .or_else(|| {
5042                text.strip_prefix(":+")
5043                    .map(|operand| (ZshDefaultingOp::UseReplacement, operand))
5044            })
5045            .or_else(|| {
5046                text.strip_prefix(":?")
5047                    .map(|operand| (ZshDefaultingOp::Error, operand))
5048            })
5049        {
5050            let operand = self.source_text(
5051                operand.to_string(),
5052                base.advanced_by(&text[..2]),
5053                base.advanced_by(text),
5054            );
5055            return ZshExpansionOperation::Defaulting {
5056                kind,
5057                operand_word_ast: self.parse_source_text_as_word(&operand),
5058                operand,
5059                colon_variant: true,
5060            };
5061        }
5062
5063        if let Some((kind, prefix_len)) = [
5064            ("##", ZshTrimOp::RemovePrefixLong),
5065            ("#", ZshTrimOp::RemovePrefixShort),
5066            ("%%", ZshTrimOp::RemoveSuffixLong),
5067            ("%", ZshTrimOp::RemoveSuffixShort),
5068        ]
5069        .into_iter()
5070        .find_map(|(prefix, kind)| text.starts_with(prefix).then_some((kind, prefix.len())))
5071        {
5072            let operand = self.zsh_operation_source_text(text, base, prefix_len, text.len());
5073            return ZshExpansionOperation::TrimOperation {
5074                kind,
5075                operand_word_ast: self.parse_source_text_as_word(&operand),
5076                operand,
5077            };
5078        }
5079
5080        if let Some((kind, prefix_len)) = [
5081            ("//", ZshReplacementOp::ReplaceAll),
5082            ("/#", ZshReplacementOp::ReplacePrefix),
5083            ("/%", ZshReplacementOp::ReplaceSuffix),
5084            ("/", ZshReplacementOp::ReplaceFirst),
5085        ]
5086        .into_iter()
5087        .find_map(|(prefix, kind)| text.starts_with(prefix).then_some((kind, prefix.len())))
5088        {
5089            let rest = &text[prefix_len..];
5090            let separator = self.find_zsh_top_level_delimiter(rest, '/');
5091            let pattern_end = separator.unwrap_or(rest.len());
5092            let pattern =
5093                self.zsh_operation_source_text(text, base, prefix_len, prefix_len + pattern_end);
5094            let replacement = separator.map(|separator| {
5095                self.zsh_operation_source_text(text, base, prefix_len + separator + 1, text.len())
5096            });
5097            return ZshExpansionOperation::ReplacementOperation {
5098                kind,
5099                pattern_word_ast: self.parse_source_text_as_word(&pattern),
5100                replacement_word_ast: self.parse_optional_source_text_as_word(replacement.as_ref()),
5101                pattern,
5102                replacement,
5103            };
5104        }
5105
5106        if let Some(rest) = text.strip_prefix(':') {
5107            if Self::zsh_modifier_suffix_candidate(rest) {
5108                let text = self.source_text(text.to_string(), base, base.advanced_by(text));
5109                return ZshExpansionOperation::Unknown {
5110                    word_ast: self.parse_source_text_as_word(&text),
5111                    text,
5112                };
5113            }
5114
5115            if Self::zsh_slice_candidate(rest) {
5116                let separator = self.find_zsh_top_level_delimiter(rest, ':');
5117                let offset_end = separator.unwrap_or(rest.len());
5118                let offset = self.zsh_operation_source_text(text, base, 1, 1 + offset_end);
5119                let length = separator.map(|separator| {
5120                    self.zsh_operation_source_text(text, base, 1 + separator + 1, text.len())
5121                });
5122                return ZshExpansionOperation::Slice {
5123                    offset_word_ast: self.parse_source_text_as_word(&offset),
5124                    length_word_ast: self.parse_optional_source_text_as_word(length.as_ref()),
5125                    offset,
5126                    length,
5127                };
5128            }
5129        }
5130
5131        let text = self.source_text(text.to_string(), base, base.advanced_by(text));
5132        ZshExpansionOperation::Unknown {
5133            word_ast: self.parse_source_text_as_word(&text),
5134            text,
5135        }
5136    }
5137
5138    fn parse_explicit_arithmetic_span(
5139        &self,
5140        span: Option<Span>,
5141        context: &'static str,
5142    ) -> Result<Option<ArithmeticExprNode>> {
5143        let Some(span) = span else {
5144            return Ok(None);
5145        };
5146        if span.slice(self.input).trim().is_empty() {
5147            return Ok(None);
5148        }
5149        arithmetic::parse_expression(
5150            span.slice(self.input),
5151            span,
5152            self.dialect,
5153            self.max_depth.saturating_sub(self.current_depth),
5154            self.fuel,
5155        )
5156        .map(Some)
5157        .map_err(|error| match error {
5158            Error::Parse { message, .. } => self.error(format!("{context}: {message}")),
5159        })
5160    }
5161
5162    fn parse_source_text_as_arithmetic(&self, text: &SourceText) -> Result<ArithmeticExprNode> {
5163        arithmetic::parse_expression(
5164            text.slice(self.input),
5165            text.span(),
5166            self.dialect,
5167            self.max_depth.saturating_sub(self.current_depth),
5168            self.fuel,
5169        )
5170    }
5171
5172    fn maybe_parse_source_text_as_arithmetic(
5173        &self,
5174        text: &SourceText,
5175    ) -> Option<ArithmeticExprNode> {
5176        if !text.is_source_backed() {
5177            return None;
5178        }
5179        self.parse_source_text_as_arithmetic(text).ok()
5180    }
5181
5182    fn parse_source_text_as_word(&self, text: &SourceText) -> Word {
5183        if let Some(word) = self.simple_source_text_as_word(text) {
5184            return word;
5185        }
5186
5187        let span = text.span();
5188        let remaining_depth = self.max_depth.saturating_sub(self.current_depth);
5189        if remaining_depth == 0 {
5190            return self.word_with_single_part(
5191                self.literal_part_from_text(text.slice(self.input), span, text.is_source_backed()),
5192                span,
5193            );
5194        }
5195        let reparse_depth = (remaining_depth - 1).min(SOURCE_TEXT_WORD_REPARSE_MAX_DEPTH);
5196
5197        if !text.is_source_backed()
5198            && span.start.offset <= span.end.offset
5199            && span.end.offset <= self.input.len()
5200        {
5201            let raw = span.slice(self.input);
5202            if raw.contains("\\\"") {
5203                return Self::parse_word_fragment_with_limits(
5204                    self.input,
5205                    raw,
5206                    span,
5207                    reparse_depth,
5208                    self.fuel,
5209                    self.shell_profile.clone(),
5210                );
5211            }
5212        }
5213
5214        Self::parse_word_fragment_with_limits(
5215            self.input,
5216            text.slice(self.input),
5217            text.span(),
5218            reparse_depth,
5219            self.fuel,
5220            self.shell_profile.clone(),
5221        )
5222    }
5223
5224    fn simple_source_text_as_word(&self, text: &SourceText) -> Option<Word> {
5225        if !text.is_source_backed() {
5226            return None;
5227        }
5228
5229        let span = text.span();
5230        let raw = text.slice(self.input);
5231        if raw.is_empty() {
5232            return Some(Word::literal_with_span("", span));
5233        }
5234
5235        if let Some(word) = self.simple_quoted_source_text_as_word(raw, span) {
5236            return Some(word);
5237        }
5238
5239        if Self::word_text_needs_parse(raw)
5240            || raw.contains(['\'', '"', '\\'])
5241            || self.zsh_glob_qualifiers_enabled_at(span.start.offset)
5242        {
5243            return None;
5244        }
5245
5246        Some(self.word_with_single_part(self.literal_part_from_text(raw, span, true), span))
5247    }
5248
5249    fn simple_quoted_source_text_as_word(&self, raw: &str, span: Span) -> Option<Word> {
5250        if raw.len() < 2 {
5251            return None;
5252        }
5253
5254        let quote = raw.as_bytes()[0];
5255        if quote != b'\'' && quote != b'"' || raw.as_bytes().last().copied() != Some(quote) {
5256            return None;
5257        }
5258
5259        let inner = &raw[1..raw.len() - 1];
5260        if quote == b'\'' && inner.contains('\'') {
5261            return None;
5262        }
5263
5264        let inner_start = span.start.advanced_by(&raw[..1]);
5265        let inner_end = inner_start.advanced_by(inner);
5266        let inner_span = Span::from_positions(inner_start, inner_end);
5267        let part = match quote {
5268            b'\'' => self.single_quoted_part_from_text(inner, inner_span, span, false),
5269            b'"' => {
5270                if Self::word_text_needs_parse(inner) || inner.contains(['\\', '"']) {
5271                    return None;
5272                }
5273                self.double_quoted_literal_part_from_text(inner, inner_span, span, true, false)
5274            }
5275            _ => unreachable!("quote is checked above"),
5276        };
5277        Some(self.word_with_single_part(part, span))
5278    }
5279
5280    fn parse_optional_source_text_as_word(&self, text: Option<&SourceText>) -> Option<Word> {
5281        text.map(|text| self.parse_source_text_as_word(text))
5282    }
5283
5284    fn source_matches(&self, span: Span, text: &str) -> bool {
5285        span.start.offset <= span.end.offset
5286            && self
5287                .input
5288                .get(span.start.offset..span.end.offset)
5289                .is_some_and(|slice| slice == text)
5290    }
5291
5292    fn checkpoint(&self) -> ParserCheckpoint<'a> {
5293        ParserCheckpoint {
5294            lexer: self.lexer.clone(),
5295            synthetic_tokens: self.synthetic_tokens.clone(),
5296            alias_replays: self.alias_replays.clone(),
5297            current_token: self.current_token.clone(),
5298            current_token_kind: self.current_token_kind,
5299            current_keyword: self.current_keyword,
5300            current_span: self.current_span,
5301            peeked_token: self.peeked_token.clone(),
5302            current_depth: self.current_depth,
5303            fuel: self.fuel,
5304            source_text_pattern_depth: self.source_text_pattern_depth,
5305            comments: self.comments.clone(),
5306            expand_next_word: self.expand_next_word,
5307            brace_group_depth: self.brace_group_depth,
5308            brace_body_stack: self.brace_body_stack.clone(),
5309            syntax_facts: self.syntax_facts.clone(),
5310            #[cfg(feature = "benchmarking")]
5311            benchmark_counters: self.benchmark_counters,
5312        }
5313    }
5314
5315    fn restore(&mut self, checkpoint: ParserCheckpoint<'a>) {
5316        self.lexer = checkpoint.lexer;
5317        self.synthetic_tokens = checkpoint.synthetic_tokens;
5318        self.alias_replays = checkpoint.alias_replays;
5319        self.current_token = checkpoint.current_token;
5320        // This is a cache over current_token/current_span, so rebuilding it is equivalent.
5321        self.current_word_cache = None;
5322        self.current_token_kind = checkpoint.current_token_kind;
5323        self.current_keyword = checkpoint.current_keyword;
5324        self.current_span = checkpoint.current_span;
5325        self.peeked_token = checkpoint.peeked_token;
5326        self.current_depth = checkpoint.current_depth;
5327        self.fuel = checkpoint.fuel;
5328        self.source_text_pattern_depth = checkpoint.source_text_pattern_depth;
5329        self.comments = checkpoint.comments;
5330        self.expand_next_word = checkpoint.expand_next_word;
5331        self.brace_group_depth = checkpoint.brace_group_depth;
5332        self.brace_body_stack = checkpoint.brace_body_stack;
5333        self.syntax_facts = checkpoint.syntax_facts;
5334        #[cfg(feature = "benchmarking")]
5335        {
5336            self.benchmark_counters = checkpoint.benchmark_counters;
5337        }
5338    }
5339
5340    fn set_current_spanned(&mut self, token: LexedToken<'a>) {
5341        #[cfg(feature = "benchmarking")]
5342        self.maybe_record_set_current_spanned_call();
5343        let span = token.span;
5344        self.current_token_kind = Some(token.kind);
5345        self.current_keyword = Self::keyword_from_token(&token);
5346        self.current_token = Some(token);
5347        self.current_word_cache = None;
5348        self.current_span = span;
5349    }
5350
5351    fn set_current_kind(&mut self, kind: TokenKind, span: Span) {
5352        self.current_token_kind = Some(kind);
5353        self.current_keyword = None;
5354        self.current_token = Some(LexedToken::punctuation(kind).with_span(span));
5355        self.current_word_cache = None;
5356        self.current_span = span;
5357    }
5358
5359    fn clear_current_token(&mut self) {
5360        self.current_token = None;
5361        self.current_word_cache = None;
5362        self.current_token_kind = None;
5363        self.current_keyword = None;
5364    }
5365
5366    fn next_pending_token(&mut self) -> Option<LexedToken<'a>> {
5367        if let Some(token) = self.synthetic_tokens.pop_front() {
5368            return Some(token.materialize());
5369        }
5370
5371        loop {
5372            let replay = self.alias_replays.last_mut()?;
5373            if let Some(token) = replay.next_token() {
5374                return Some(token);
5375            }
5376            self.alias_replays.pop();
5377        }
5378    }
5379
5380    fn next_spanned_token_with_comments(&mut self) -> Option<LexedToken<'a>> {
5381        self.next_pending_token()
5382            .or_else(|| self.lexer.next_lexed_token_with_comments())
5383    }
5384
5385    fn compile_alias_definition(&self, value: &str) -> AliasDefinition {
5386        let source = Arc::<str>::from(value.to_string());
5387        let mut lexer = Lexer::with_max_subst_depth(source.as_ref(), self.max_depth);
5388        let mut tokens = Vec::new();
5389
5390        while let Some(token) = lexer.next_lexed_token_with_comments() {
5391            tokens.push(token.into_shared(&source));
5392        }
5393
5394        AliasDefinition {
5395            tokens: tokens.into(),
5396            expands_next_word: value.chars().last().is_some_and(char::is_whitespace),
5397        }
5398    }
5399
5400    fn maybe_expand_current_alias_chain(&mut self) {
5401        if !self.expand_aliases {
5402            self.expand_next_word = false;
5403            return;
5404        }
5405
5406        let mut seen = HashSet::new();
5407        let mut expands_next_word = false;
5408
5409        loop {
5410            if self.current_token_kind != Some(TokenKind::Word) {
5411                break;
5412            }
5413            let Some(name) = self.current_token.as_ref().and_then(LexedToken::word_text) else {
5414                break;
5415            };
5416            if self.current_source_word_starts_posix_function_header(name) {
5417                break;
5418            }
5419            let Some(alias) = self.aliases.get(name).cloned() else {
5420                break;
5421            };
5422            if !seen.insert(name.to_string()) {
5423                break;
5424            }
5425
5426            expands_next_word = alias.expands_next_word;
5427            self.peeked_token = None;
5428            self.alias_replays
5429                .push(AliasReplay::new(&alias, self.current_span.start));
5430            self.advance_raw();
5431        }
5432
5433        self.expand_next_word = expands_next_word;
5434    }
5435
5436    fn current_source_word_starts_posix_function_header(&self, name: &str) -> bool {
5437        if name.contains('=') || name.contains('[') {
5438            return false;
5439        }
5440
5441        if self
5442            .current_token
5443            .as_ref()
5444            .is_some_and(|token| token.flags.is_synthetic())
5445        {
5446            return false;
5447        }
5448
5449        let Some(tail) = self.input.get(self.current_span.end.offset..) else {
5450            return false;
5451        };
5452        let tail = tail.trim_start_matches([' ', '\t']);
5453        let Some(after_left) = tail.strip_prefix('(') else {
5454            return false;
5455        };
5456        let after_left = after_left.trim_start_matches([' ', '\t']);
5457        after_left.starts_with(')')
5458    }
5459
5460    fn next_word_char(
5461        chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
5462        cursor: &mut Position,
5463    ) -> Option<char> {
5464        let ch = chars.next()?;
5465        cursor.advance(ch);
5466        Some(ch)
5467    }
5468
5469    fn next_word_char_unwrap(
5470        chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
5471        cursor: &mut Position,
5472    ) -> char {
5473        let Some(ch) = Self::next_word_char(chars, cursor) else {
5474            unreachable!("word parser should only consume characters that were already peeked");
5475        };
5476        ch
5477    }
5478
5479    fn consume_word_char_if(
5480        chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
5481        cursor: &mut Position,
5482        expected: char,
5483    ) -> bool {
5484        if chars.peek() == Some(&expected) {
5485            Self::next_word_char_unwrap(chars, cursor);
5486            true
5487        } else {
5488            false
5489        }
5490    }
5491
5492    fn read_word_while<F>(
5493        chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
5494        cursor: &mut Position,
5495        mut predicate: F,
5496    ) -> String
5497    where
5498        F: FnMut(char) -> bool,
5499    {
5500        let mut text = String::new();
5501        while let Some(&ch) = chars.peek() {
5502            if !predicate(ch) {
5503                break;
5504            }
5505            text.push(Self::next_word_char_unwrap(chars, cursor));
5506        }
5507        text
5508    }
5509
5510    fn rebase_redirects(redirects: &mut [Redirect], base: Position) {
5511        for redirect in redirects {
5512            redirect.span = redirect.span.rebased(base);
5513            redirect.fd_var_span = redirect.fd_var_span.map(|span| span.rebased(base));
5514            match &mut redirect.target {
5515                RedirectTarget::Word(word) => Self::rebase_word(word, base),
5516                RedirectTarget::Heredoc(heredoc) => {
5517                    heredoc.delimiter.span = heredoc.delimiter.span.rebased(base);
5518                    Self::rebase_word(&mut heredoc.delimiter.raw, base);
5519                    Self::rebase_heredoc_body(&mut heredoc.body, base);
5520                }
5521            }
5522        }
5523    }
5524
5525    fn rebase_assignments(assignments: &mut [Assignment], base: Position) {
5526        for assignment in assignments {
5527            assignment.span = assignment.span.rebased(base);
5528            Self::rebase_var_ref(&mut assignment.target, base);
5529            match &mut assignment.value {
5530                AssignmentValue::Scalar(word) => Self::rebase_word(word, base),
5531                AssignmentValue::Compound(array) => Self::rebase_array_expr(array, base),
5532            }
5533        }
5534    }
5535
5536    /// Create a parse error with the current position.
5537    fn error(&self, message: impl Into<String>) -> Error {
5538        Error::parse_at(
5539            message,
5540            self.current_span.start.line,
5541            self.current_span.start.column,
5542        )
5543    }
5544
5545    fn ensure_feature(
5546        &self,
5547        enabled: bool,
5548        feature: &str,
5549        unsupported_message: &str,
5550    ) -> Result<()> {
5551        if enabled {
5552            Ok(())
5553        } else {
5554            Err(self.error(format!("{feature} {unsupported_message}")))
5555        }
5556    }
5557
5558    fn ensure_double_bracket(&self) -> Result<()> {
5559        self.ensure_feature(
5560            self.dialect.features().double_bracket,
5561            "[[ ]] conditionals",
5562            "are not available in this shell mode",
5563        )
5564    }
5565
5566    fn ensure_arithmetic_for(&self) -> Result<()> {
5567        self.ensure_feature(
5568            self.dialect.features().arithmetic_for,
5569            "c-style for loops",
5570            "are not available in this shell mode",
5571        )
5572    }
5573
5574    fn ensure_coproc(&self) -> Result<()> {
5575        self.ensure_feature(
5576            self.dialect.features().coproc_keyword,
5577            "coprocess commands",
5578            "are not available in this shell mode",
5579        )
5580    }
5581
5582    fn ensure_arithmetic_command(&self) -> Result<()> {
5583        self.ensure_feature(
5584            self.dialect.features().arithmetic_command,
5585            "arithmetic commands",
5586            "are not available in this shell mode",
5587        )
5588    }
5589
5590    fn ensure_select_loop(&self) -> Result<()> {
5591        self.ensure_feature(
5592            self.dialect.features().select_loop,
5593            "select loops",
5594            "are not available in this shell mode",
5595        )
5596    }
5597
5598    fn ensure_repeat_loop(&self) -> Result<()> {
5599        self.ensure_feature(
5600            self.zsh_short_repeat_enabled(),
5601            "repeat loops",
5602            "are not available in this shell mode",
5603        )
5604    }
5605
5606    fn ensure_foreach_loop(&self) -> Result<()> {
5607        self.ensure_feature(
5608            self.zsh_short_loops_enabled(),
5609            "foreach loops",
5610            "are not available in this shell mode",
5611        )
5612    }
5613
5614    fn ensure_function_keyword(&self) -> Result<()> {
5615        self.ensure_feature(
5616            self.dialect.features().function_keyword,
5617            "function keyword definitions",
5618            "are not available in this shell mode",
5619        )
5620    }
5621
5622    /// Consume one unit of fuel, returning an error if exhausted
5623    fn tick(&mut self) -> Result<()> {
5624        if self.fuel == 0 {
5625            let used = self.max_fuel - self.fuel;
5626            return Err(Error::parse(format!(
5627                "parser fuel exhausted ({} operations, max {})",
5628                used, self.max_fuel
5629            )));
5630        }
5631        self.fuel -= 1;
5632        Ok(())
5633    }
5634
5635    /// Push nesting depth and check limit
5636    fn push_depth(&mut self) -> Result<()> {
5637        self.current_depth += 1;
5638        if self.current_depth > self.max_depth {
5639            return Err(Error::parse(format!(
5640                "AST nesting too deep ({} levels, max {})",
5641                self.current_depth, self.max_depth
5642            )));
5643        }
5644        Ok(())
5645    }
5646
5647    /// Pop nesting depth
5648    fn pop_depth(&mut self) {
5649        if self.current_depth > 0 {
5650            self.current_depth -= 1;
5651        }
5652    }
5653
5654    /// Check if current token is an error token and return the error if so
5655    fn check_error_token(&self) -> Result<()> {
5656        if self.current_token_kind == Some(TokenKind::Error) {
5657            let msg = self
5658                .current_token
5659                .as_ref()
5660                .and_then(LexedToken::error_kind)
5661                .map(|kind| kind.message())
5662                .unwrap_or("unknown lexer error");
5663            return Err(self.error(format!("syntax error: {}", msg)));
5664        }
5665        Ok(())
5666    }
5667
5668    fn parse_diagnostic_from_error(&self, error: Error) -> ParseDiagnostic {
5669        let Error::Parse { message, .. } = error;
5670        ParseDiagnostic {
5671            message,
5672            span: self.current_span,
5673        }
5674    }
5675
5676    fn compound_span(compound: &CompoundCommand) -> Span {
5677        match compound {
5678            CompoundCommand::If(command) => command.span,
5679            CompoundCommand::For(command) => command.span,
5680            CompoundCommand::Repeat(command) => command.span,
5681            CompoundCommand::Foreach(command) => command.span,
5682            CompoundCommand::ArithmeticFor(command) => command.span,
5683            CompoundCommand::While(command) => command.span,
5684            CompoundCommand::Until(command) => command.span,
5685            CompoundCommand::Case(command) => command.span,
5686            CompoundCommand::Select(command) => command.span,
5687            CompoundCommand::Subshell(body) | CompoundCommand::BraceGroup(body) => body.span,
5688            CompoundCommand::Arithmetic(command) => command.span,
5689            CompoundCommand::Time(command) => command.span,
5690            CompoundCommand::Conditional(command) => command.span,
5691            CompoundCommand::Coproc(command) => command.span,
5692            CompoundCommand::Always(command) => command.span,
5693        }
5694    }
5695
5696    fn stmt_seq_with_span(span: Span, stmts: Vec<Stmt>) -> StmtSeq {
5697        StmtSeq {
5698            leading_comments: Vec::new(),
5699            stmts,
5700            trailing_comments: Vec::new(),
5701            span,
5702        }
5703    }
5704
5705    fn binary_stmt(left: Stmt, op: BinaryOp, op_span: Span, right: Stmt) -> Stmt {
5706        let span = left.span.merge(right.span);
5707        Stmt {
5708            leading_comments: Vec::new(),
5709            command: AstCommand::Binary(BinaryCommand {
5710                left: Box::new(left),
5711                op,
5712                op_span,
5713                right: Box::new(right),
5714                span,
5715            }),
5716            negated: false,
5717            redirects: Box::default(),
5718            terminator: None,
5719            terminator_span: None,
5720            inline_comment: None,
5721            span,
5722        }
5723    }
5724
5725    fn lower_builtin_command(
5726        builtin: BuiltinCommand,
5727    ) -> (AstBuiltinCommand, SmallVec<[Redirect; 1]>, Span) {
5728        match builtin {
5729            BuiltinCommand::Break(command) => {
5730                let span = command.span;
5731                let redirects = command.redirects;
5732                (
5733                    AstBuiltinCommand::Break(AstBreakCommand {
5734                        depth: command.depth,
5735                        extra_args: command.extra_args.into_vec(),
5736                        assignments: command.assignments.into_boxed_slice(),
5737                        span,
5738                    }),
5739                    redirects,
5740                    span,
5741                )
5742            }
5743            BuiltinCommand::Continue(command) => {
5744                let span = command.span;
5745                let redirects = command.redirects;
5746                (
5747                    AstBuiltinCommand::Continue(AstContinueCommand {
5748                        depth: command.depth,
5749                        extra_args: command.extra_args.into_vec(),
5750                        assignments: command.assignments.into_boxed_slice(),
5751                        span,
5752                    }),
5753                    redirects,
5754                    span,
5755                )
5756            }
5757            BuiltinCommand::Return(command) => {
5758                let span = command.span;
5759                let redirects = command.redirects;
5760                (
5761                    AstBuiltinCommand::Return(AstReturnCommand {
5762                        code: command.code,
5763                        extra_args: command.extra_args.into_vec(),
5764                        assignments: command.assignments.into_boxed_slice(),
5765                        span,
5766                    }),
5767                    redirects,
5768                    span,
5769                )
5770            }
5771            BuiltinCommand::Exit(command) => {
5772                let span = command.span;
5773                let redirects = command.redirects;
5774                (
5775                    AstBuiltinCommand::Exit(AstExitCommand {
5776                        code: command.code,
5777                        extra_args: command.extra_args.into_vec(),
5778                        assignments: command.assignments.into_boxed_slice(),
5779                        span,
5780                    }),
5781                    redirects,
5782                    span,
5783                )
5784            }
5785        }
5786    }
5787
5788    fn lower_non_sequence_command_to_stmt(command: Command) -> Stmt {
5789        match command {
5790            Command::Simple(command) => Stmt {
5791                leading_comments: Vec::new(),
5792                command: AstCommand::Simple(AstSimpleCommand {
5793                    name: command.name,
5794                    args: command.args.into_vec(),
5795                    assignments: command.assignments.into_boxed_slice(),
5796                    span: command.span,
5797                }),
5798                negated: false,
5799                redirects: command.redirects.into_boxed_slice(),
5800                terminator: None,
5801                terminator_span: None,
5802                inline_comment: None,
5803                span: command.span,
5804            },
5805            Command::Builtin(command) => {
5806                let (command, redirects, span) = Self::lower_builtin_command(command);
5807                Stmt {
5808                    leading_comments: Vec::new(),
5809                    command: AstCommand::Builtin(command),
5810                    negated: false,
5811                    redirects: redirects.into_boxed_slice(),
5812                    terminator: None,
5813                    terminator_span: None,
5814                    inline_comment: None,
5815                    span,
5816                }
5817            }
5818            Command::Decl(command) => {
5819                let command = *command;
5820                Stmt {
5821                    leading_comments: Vec::new(),
5822                    command: AstCommand::Decl(AstDeclClause {
5823                        variant: command.variant,
5824                        variant_span: command.variant_span,
5825                        operands: command.operands.into_vec(),
5826                        assignments: command.assignments.into_boxed_slice(),
5827                        span: command.span,
5828                    }),
5829                    negated: false,
5830                    redirects: command.redirects.into_boxed_slice(),
5831                    terminator: None,
5832                    terminator_span: None,
5833                    inline_comment: None,
5834                    span: command.span,
5835                }
5836            }
5837            Command::Compound(compound, redirects) => {
5838                let span = Self::compound_span(&compound);
5839                Stmt {
5840                    leading_comments: Vec::new(),
5841                    command: AstCommand::Compound(*compound),
5842                    negated: false,
5843                    redirects: redirects.into_boxed_slice(),
5844                    terminator: None,
5845                    terminator_span: None,
5846                    inline_comment: None,
5847                    span,
5848                }
5849            }
5850            Command::Function(function) => Stmt {
5851                leading_comments: Vec::new(),
5852                span: function.span,
5853                command: AstCommand::Function(function),
5854                negated: false,
5855                redirects: Box::default(),
5856                terminator: None,
5857                terminator_span: None,
5858                inline_comment: None,
5859            },
5860            Command::AnonymousFunction(function, redirects) => Stmt {
5861                leading_comments: Vec::new(),
5862                span: function.span,
5863                command: AstCommand::AnonymousFunction(function),
5864                negated: false,
5865                redirects: redirects.into_boxed_slice(),
5866                terminator: None,
5867                terminator_span: None,
5868                inline_comment: None,
5869            },
5870        }
5871    }
5872
5873    fn comment_start(comment: Comment) -> usize {
5874        usize::from(comment.range.start())
5875    }
5876
5877    fn is_inline_comment(source: &str, stmt: &Stmt, comment: Comment) -> bool {
5878        let comment_start = Self::comment_start(comment);
5879        if comment_start < stmt.span.end.offset {
5880            return false;
5881        }
5882        source
5883            .get(stmt.span.end.offset..comment_start)
5884            .is_some_and(|gap| !gap.contains('\n'))
5885    }
5886
5887    fn take_comments_before(
5888        comments: &mut VecDeque<Comment>,
5889        end_offset: usize,
5890    ) -> VecDeque<Comment> {
5891        let mut taken = VecDeque::new();
5892        while comments
5893            .front()
5894            .is_some_and(|comment| Self::comment_start(*comment) < end_offset)
5895        {
5896            let Some(comment) = comments.pop_front() else {
5897                unreachable!("front comment should exist while draining");
5898            };
5899            taken.push_back(comment);
5900        }
5901        taken
5902    }
5903
5904    fn attach_comments_to_file(&self, file: &mut File) {
5905        let mut comments = self.comments.iter().copied().collect::<VecDeque<_>>();
5906        Self::attach_comments_to_stmt_seq_with_source(self.input, &mut file.body, &mut comments);
5907        file.body.trailing_comments.extend(comments);
5908    }
5909
5910    fn attach_comments_to_stmt_seq_with_source(
5911        source: &str,
5912        sequence: &mut StmtSeq,
5913        comments: &mut VecDeque<Comment>,
5914    ) {
5915        if sequence.stmts.is_empty() {
5916            sequence
5917                .trailing_comments
5918                .extend(Self::take_comments_before(
5919                    comments,
5920                    sequence.span.end.offset,
5921                ));
5922            return;
5923        }
5924
5925        for (index, stmt) in sequence.stmts.iter_mut().enumerate() {
5926            let leading = Self::take_comments_before(comments, stmt.span.start.offset);
5927            if index == 0 {
5928                sequence.leading_comments.extend(leading);
5929            } else {
5930                stmt.leading_comments.extend(leading);
5931            }
5932
5933            let mut nested = Self::take_comments_before(comments, stmt.span.end.offset);
5934            Self::attach_comments_to_stmt_with_source(source, stmt, &mut nested);
5935            if !nested.is_empty() {
5936                stmt.leading_comments.extend(nested);
5937            }
5938
5939            if stmt.inline_comment.is_none()
5940                && comments
5941                    .front()
5942                    .is_some_and(|comment| Self::is_inline_comment(source, stmt, *comment))
5943            {
5944                stmt.inline_comment = comments.pop_front();
5945            }
5946        }
5947
5948        sequence
5949            .trailing_comments
5950            .extend(Self::take_comments_before(
5951                comments,
5952                sequence.span.end.offset,
5953            ));
5954    }
5955
5956    fn attach_comments_to_stmt_with_source(
5957        source: &str,
5958        stmt: &mut Stmt,
5959        comments: &mut VecDeque<Comment>,
5960    ) {
5961        match &mut stmt.command {
5962            AstCommand::Binary(binary) => {
5963                let mut left_comments =
5964                    Self::take_comments_before(comments, binary.left.span.end.offset);
5965                Self::attach_comments_to_stmt_with_source(
5966                    source,
5967                    binary.left.as_mut(),
5968                    &mut left_comments,
5969                );
5970                if !left_comments.is_empty() {
5971                    binary.left.leading_comments.extend(left_comments);
5972                }
5973
5974                let mut right_comments = std::mem::take(comments);
5975                Self::attach_comments_to_stmt_with_source(
5976                    source,
5977                    binary.right.as_mut(),
5978                    &mut right_comments,
5979                );
5980                if !right_comments.is_empty() {
5981                    binary.right.leading_comments.extend(right_comments);
5982                }
5983            }
5984            AstCommand::Compound(compound) => {
5985                Self::attach_comments_to_compound_with_source(source, compound, comments);
5986            }
5987            AstCommand::Function(function) => {
5988                let mut body_comments = std::mem::take(comments);
5989                Self::attach_comments_to_stmt_with_source(
5990                    source,
5991                    function.body.as_mut(),
5992                    &mut body_comments,
5993                );
5994                if !body_comments.is_empty() {
5995                    function.body.leading_comments.extend(body_comments);
5996                }
5997            }
5998            AstCommand::AnonymousFunction(function) => {
5999                let mut body_comments = std::mem::take(comments);
6000                Self::attach_comments_to_stmt_with_source(
6001                    source,
6002                    function.body.as_mut(),
6003                    &mut body_comments,
6004                );
6005                if !body_comments.is_empty() {
6006                    function.body.leading_comments.extend(body_comments);
6007                }
6008            }
6009            AstCommand::Simple(_) | AstCommand::Builtin(_) | AstCommand::Decl(_) => {}
6010        }
6011    }
6012
6013    fn attach_comments_to_compound_with_source(
6014        source: &str,
6015        command: &mut CompoundCommand,
6016        comments: &mut VecDeque<Comment>,
6017    ) {
6018        match command {
6019            CompoundCommand::If(command) => {
6020                let mut condition =
6021                    Self::take_comments_before(comments, command.condition.span.end.offset);
6022                Self::attach_comments_to_stmt_seq_with_source(
6023                    source,
6024                    &mut command.condition,
6025                    &mut condition,
6026                );
6027                command.condition.trailing_comments.extend(condition);
6028
6029                let mut then_branch =
6030                    Self::take_comments_before(comments, command.then_branch.span.end.offset);
6031                Self::attach_comments_to_stmt_seq_with_source(
6032                    source,
6033                    &mut command.then_branch,
6034                    &mut then_branch,
6035                );
6036                command.then_branch.trailing_comments.extend(then_branch);
6037
6038                for (condition_seq, body_seq) in &mut command.elif_branches {
6039                    let mut elif_condition =
6040                        Self::take_comments_before(comments, condition_seq.span.end.offset);
6041                    Self::attach_comments_to_stmt_seq_with_source(
6042                        source,
6043                        condition_seq,
6044                        &mut elif_condition,
6045                    );
6046                    condition_seq.trailing_comments.extend(elif_condition);
6047
6048                    let mut elif_body =
6049                        Self::take_comments_before(comments, body_seq.span.end.offset);
6050                    Self::attach_comments_to_stmt_seq_with_source(source, body_seq, &mut elif_body);
6051                    body_seq.trailing_comments.extend(elif_body);
6052                }
6053
6054                if let Some(else_branch) = &mut command.else_branch {
6055                    let mut else_comments = std::mem::take(comments);
6056                    Self::attach_comments_to_stmt_seq_with_source(
6057                        source,
6058                        else_branch,
6059                        &mut else_comments,
6060                    );
6061                    else_branch.trailing_comments.extend(else_comments);
6062                }
6063            }
6064            CompoundCommand::For(command) => {
6065                let mut body_comments = std::mem::take(comments);
6066                Self::attach_comments_to_stmt_seq_with_source(
6067                    source,
6068                    &mut command.body,
6069                    &mut body_comments,
6070                );
6071                command.body.trailing_comments.extend(body_comments);
6072            }
6073            CompoundCommand::Repeat(command) => {
6074                let mut body_comments = std::mem::take(comments);
6075                Self::attach_comments_to_stmt_seq_with_source(
6076                    source,
6077                    &mut command.body,
6078                    &mut body_comments,
6079                );
6080                command.body.trailing_comments.extend(body_comments);
6081            }
6082            CompoundCommand::Foreach(command) => {
6083                let mut body_comments = std::mem::take(comments);
6084                Self::attach_comments_to_stmt_seq_with_source(
6085                    source,
6086                    &mut command.body,
6087                    &mut body_comments,
6088                );
6089                command.body.trailing_comments.extend(body_comments);
6090            }
6091            CompoundCommand::ArithmeticFor(command) => {
6092                let mut body_comments = std::mem::take(comments);
6093                Self::attach_comments_to_stmt_seq_with_source(
6094                    source,
6095                    &mut command.body,
6096                    &mut body_comments,
6097                );
6098                command.body.trailing_comments.extend(body_comments);
6099            }
6100            CompoundCommand::While(command) => {
6101                let mut condition =
6102                    Self::take_comments_before(comments, command.condition.span.end.offset);
6103                Self::attach_comments_to_stmt_seq_with_source(
6104                    source,
6105                    &mut command.condition,
6106                    &mut condition,
6107                );
6108                command.condition.trailing_comments.extend(condition);
6109
6110                let mut body_comments = std::mem::take(comments);
6111                Self::attach_comments_to_stmt_seq_with_source(
6112                    source,
6113                    &mut command.body,
6114                    &mut body_comments,
6115                );
6116                command.body.trailing_comments.extend(body_comments);
6117            }
6118            CompoundCommand::Until(command) => {
6119                let mut condition =
6120                    Self::take_comments_before(comments, command.condition.span.end.offset);
6121                Self::attach_comments_to_stmt_seq_with_source(
6122                    source,
6123                    &mut command.condition,
6124                    &mut condition,
6125                );
6126                command.condition.trailing_comments.extend(condition);
6127
6128                let mut body_comments = std::mem::take(comments);
6129                Self::attach_comments_to_stmt_seq_with_source(
6130                    source,
6131                    &mut command.body,
6132                    &mut body_comments,
6133                );
6134                command.body.trailing_comments.extend(body_comments);
6135            }
6136            CompoundCommand::Case(command) => {
6137                for case in &mut command.cases {
6138                    let mut body_comments =
6139                        Self::take_comments_before(comments, case.body.span.end.offset);
6140                    Self::attach_comments_to_stmt_seq_with_source(
6141                        source,
6142                        &mut case.body,
6143                        &mut body_comments,
6144                    );
6145                    case.body.trailing_comments.extend(body_comments);
6146                }
6147            }
6148            CompoundCommand::Select(command) => {
6149                let mut body_comments = std::mem::take(comments);
6150                Self::attach_comments_to_stmt_seq_with_source(
6151                    source,
6152                    &mut command.body,
6153                    &mut body_comments,
6154                );
6155                command.body.trailing_comments.extend(body_comments);
6156            }
6157            CompoundCommand::Subshell(body) | CompoundCommand::BraceGroup(body) => {
6158                let mut body_comments = std::mem::take(comments);
6159                Self::attach_comments_to_stmt_seq_with_source(source, body, &mut body_comments);
6160                body.trailing_comments.extend(body_comments);
6161            }
6162            CompoundCommand::Always(command) => {
6163                let mut body_comments =
6164                    Self::take_comments_before(comments, command.body.span.end.offset);
6165                Self::attach_comments_to_stmt_seq_with_source(
6166                    source,
6167                    &mut command.body,
6168                    &mut body_comments,
6169                );
6170                command.body.trailing_comments.extend(body_comments);
6171
6172                let mut always_comments = std::mem::take(comments);
6173                Self::attach_comments_to_stmt_seq_with_source(
6174                    source,
6175                    &mut command.always_body,
6176                    &mut always_comments,
6177                );
6178                command
6179                    .always_body
6180                    .trailing_comments
6181                    .extend(always_comments);
6182            }
6183            CompoundCommand::Time(command) => {
6184                if let Some(inner) = &mut command.command {
6185                    let mut inner_comments = std::mem::take(comments);
6186                    Self::attach_comments_to_stmt_with_source(
6187                        source,
6188                        inner.as_mut(),
6189                        &mut inner_comments,
6190                    );
6191                    if !inner_comments.is_empty() {
6192                        inner.leading_comments.extend(inner_comments);
6193                    }
6194                }
6195            }
6196            CompoundCommand::Coproc(command) => {
6197                let mut body_comments = std::mem::take(comments);
6198                Self::attach_comments_to_stmt_with_source(
6199                    source,
6200                    command.body.as_mut(),
6201                    &mut body_comments,
6202                );
6203                if !body_comments.is_empty() {
6204                    command.body.leading_comments.extend(body_comments);
6205                }
6206            }
6207            CompoundCommand::Arithmetic(_) | CompoundCommand::Conditional(_) => {}
6208        }
6209    }
6210
6211    fn advance_raw(&mut self) {
6212        #[cfg(feature = "benchmarking")]
6213        self.maybe_record_advance_raw_call();
6214        if let Some(peeked) = self.peeked_token.take() {
6215            self.set_current_spanned(peeked);
6216        } else {
6217            loop {
6218                match self.next_spanned_token_with_comments() {
6219                    Some(st) if st.kind == TokenKind::Comment => {
6220                        self.maybe_record_comment(&st);
6221                    }
6222                    Some(st) => {
6223                        self.set_current_spanned(st);
6224                        break;
6225                    }
6226                    None => {
6227                        self.clear_current_token();
6228                        // Keep the last span for error reporting
6229                        break;
6230                    }
6231                }
6232            }
6233        }
6234    }
6235
6236    #[cfg(feature = "benchmarking")]
6237    fn maybe_record_set_current_spanned_call(&mut self) {
6238        if let Some(counters) = &mut self.benchmark_counters {
6239            counters.parser_set_current_spanned_calls += 1;
6240        }
6241    }
6242
6243    #[cfg(feature = "benchmarking")]
6244    fn maybe_record_advance_raw_call(&mut self) {
6245        if let Some(counters) = &mut self.benchmark_counters {
6246            counters.parser_advance_raw_calls += 1;
6247        }
6248    }
6249
6250    #[cfg(feature = "benchmarking")]
6251    fn finish_benchmark_counters(&self) -> ParserBenchmarkCounters {
6252        let mut counters = self.benchmark_counters.unwrap_or_default();
6253        counters.lexer_current_position_calls =
6254            self.lexer.benchmark_counters().current_position_calls;
6255        counters
6256    }
6257
6258    fn advance(&mut self) {
6259        let should_expand = std::mem::take(&mut self.expand_next_word);
6260        self.advance_raw();
6261        if should_expand {
6262            if self
6263                .current_token
6264                .as_ref()
6265                .is_some_and(|token| token.flags.is_synthetic())
6266            {
6267                self.expand_next_word = true;
6268            } else {
6269                self.maybe_expand_current_alias_chain();
6270            }
6271        }
6272    }
6273
6274    /// Peek at the next token without consuming the current one
6275    fn peek_next(&mut self) -> Option<&LexedToken<'a>> {
6276        if self.peeked_token.is_none() {
6277            loop {
6278                match self.next_spanned_token_with_comments() {
6279                    Some(st) if st.kind == TokenKind::Comment => {
6280                        self.maybe_record_comment(&st);
6281                    }
6282                    other => {
6283                        self.peeked_token = other;
6284                        break;
6285                    }
6286                }
6287            }
6288        }
6289        self.peeked_token.as_ref()
6290    }
6291
6292    fn peek_next_kind(&mut self) -> Option<TokenKind> {
6293        self.peek_next()?;
6294        self.peeked_token.as_ref().map(|st| st.kind)
6295    }
6296
6297    fn peek_next_is(&mut self, kind: TokenKind) -> bool {
6298        self.peek_next_kind() == Some(kind)
6299    }
6300
6301    fn at(&self, kind: TokenKind) -> bool {
6302        self.current_token_kind == Some(kind)
6303    }
6304
6305    fn current_token_has_leading_whitespace(&self) -> bool {
6306        self.current_span.start.offset > 0
6307            && self.input[..self.current_span.start.offset]
6308                .chars()
6309                .next_back()
6310                .is_some_and(|ch| matches!(ch, ' ' | '\t' | '\n'))
6311    }
6312
6313    fn current_token_is_tight_to_next_token(&mut self) -> bool {
6314        let current_end = self.current_span.end.offset;
6315        self.peek_next()
6316            .is_some_and(|token| token.span.start.offset == current_end)
6317    }
6318
6319    fn at_in_set(&self, set: TokenSet) -> bool {
6320        self.current_token_kind
6321            .is_some_and(|kind| set.contains(kind))
6322    }
6323
6324    fn at_word_like(&self) -> bool {
6325        self.current_token_kind.is_some_and(TokenKind::is_word_like)
6326    }
6327
6328    fn current_word_str(&self) -> Option<&str> {
6329        self.current_token_kind
6330            .filter(|kind| kind.is_word_like())
6331            .and(self.current_token.as_ref())
6332            .and_then(LexedToken::word_text)
6333    }
6334
6335    fn classify_keyword(word: &str) -> Option<Keyword> {
6336        match word.as_bytes() {
6337            b"if" => Some(Keyword::If),
6338            b"for" => Some(Keyword::For),
6339            b"repeat" => Some(Keyword::Repeat),
6340            b"foreach" => Some(Keyword::Foreach),
6341            b"while" => Some(Keyword::While),
6342            b"until" => Some(Keyword::Until),
6343            b"case" => Some(Keyword::Case),
6344            b"select" => Some(Keyword::Select),
6345            b"time" => Some(Keyword::Time),
6346            b"coproc" => Some(Keyword::Coproc),
6347            b"function" => Some(Keyword::Function),
6348            b"always" => Some(Keyword::Always),
6349            b"then" => Some(Keyword::Then),
6350            b"else" => Some(Keyword::Else),
6351            b"elif" => Some(Keyword::Elif),
6352            b"fi" => Some(Keyword::Fi),
6353            b"do" => Some(Keyword::Do),
6354            b"done" => Some(Keyword::Done),
6355            b"esac" => Some(Keyword::Esac),
6356            b"in" => Some(Keyword::In),
6357            _ => None,
6358        }
6359    }
6360
6361    fn current_keyword(&self) -> Option<Keyword> {
6362        self.current_keyword
6363    }
6364
6365    fn looks_like_disabled_repeat_loop(&mut self) -> Result<bool> {
6366        if self.current_keyword() != Some(Keyword::Repeat) {
6367            return Ok(false);
6368        }
6369
6370        let checkpoint = self.checkpoint();
6371        self.advance();
6372        if !self.at_word_like() {
6373            self.restore(checkpoint);
6374            return Ok(false);
6375        }
6376        self.advance();
6377
6378        let result = match self.current_token_kind {
6379            Some(TokenKind::LeftBrace) => Ok(true),
6380            Some(TokenKind::Semicolon) => {
6381                self.advance();
6382                if let Err(error) = self.skip_newlines() {
6383                    self.restore(checkpoint);
6384                    return Err(error);
6385                }
6386                Ok(self.current_keyword() == Some(Keyword::Do))
6387            }
6388            Some(TokenKind::Newline) => {
6389                if let Err(error) = self.skip_newlines() {
6390                    self.restore(checkpoint);
6391                    return Err(error);
6392                }
6393                Ok(self.current_keyword() == Some(Keyword::Do))
6394            }
6395            _ => Ok(false),
6396        };
6397        self.restore(checkpoint);
6398        result
6399    }
6400
6401    fn looks_like_disabled_foreach_loop(&mut self) -> Result<bool> {
6402        if self.current_keyword() != Some(Keyword::Foreach) {
6403            return Ok(false);
6404        }
6405
6406        let checkpoint = self.checkpoint();
6407        self.advance();
6408        if self.current_name_token().is_none() {
6409            self.restore(checkpoint);
6410            return Ok(false);
6411        }
6412        self.advance();
6413
6414        let result = if self.at(TokenKind::LeftParen) {
6415            self.advance();
6416            let mut saw_word = false;
6417            while !self.at(TokenKind::RightParen) {
6418                if !self.at_word_like() {
6419                    self.restore(checkpoint);
6420                    return Ok(false);
6421                }
6422                saw_word = true;
6423                self.advance();
6424            }
6425            if !saw_word {
6426                self.restore(checkpoint);
6427                return Ok(false);
6428            }
6429            self.advance();
6430            Ok(self.at(TokenKind::LeftBrace))
6431        } else {
6432            if self.current_keyword() != Some(Keyword::In) {
6433                self.restore(checkpoint);
6434                return Ok(false);
6435            }
6436            self.advance();
6437
6438            let mut saw_word = false;
6439            let saw_separator = loop {
6440                if self.current_keyword() == Some(Keyword::Do) {
6441                    break false;
6442                }
6443
6444                match self.current_token_kind {
6445                    Some(kind) if kind.is_word_like() => {
6446                        saw_word = true;
6447                        self.advance();
6448                    }
6449                    Some(TokenKind::Semicolon) => {
6450                        self.advance();
6451                        break true;
6452                    }
6453                    Some(TokenKind::Newline) => {
6454                        if let Err(error) = self.skip_newlines() {
6455                            self.restore(checkpoint);
6456                            return Err(error);
6457                        }
6458                        break true;
6459                    }
6460                    _ => break false,
6461                }
6462            };
6463
6464            Ok(saw_word && saw_separator && self.current_keyword() == Some(Keyword::Do))
6465        };
6466        self.restore(checkpoint);
6467        result
6468    }
6469
6470    fn skip_newlines_with_flag(&mut self) -> Result<bool> {
6471        let mut skipped = false;
6472        while self.at(TokenKind::Newline) {
6473            self.tick()?;
6474            self.advance();
6475            skipped = true;
6476        }
6477        Ok(skipped)
6478    }
6479
6480    fn skip_newlines(&mut self) -> Result<()> {
6481        self.skip_newlines_with_flag().map(|_| ())
6482    }
6483}
6484#[cfg(test)]
6485mod tests;