Skip to main content

shuck_parser/parser/
mod.rs

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