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