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