Skip to main content

shuck_parser/parser/
mod.rs

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