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