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