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    /// Classify an already-parsed word as a shell assignment and split its value.
2058    ///
2059    /// This is the parser-owned entrypoint for downstream analysis that needs
2060    /// assignment target/value structure from a `Word` that was parsed as a
2061    /// normal command argument.
2062    pub fn parse_assignment_word(
2063        source: &str,
2064        word: Word,
2065        explicit_array_kind: Option<ArrayKind>,
2066        subscript_interpretation: SubscriptInterpretation,
2067    ) -> Option<Assignment> {
2068        let mut parser = Parser::new(source);
2069        parser.parse_assignment_from_word(word, explicit_array_kind, subscript_interpretation)
2070    }
2071
2072    /// Classify a contiguous group of already-parsed words as a shell assignment.
2073    ///
2074    /// Some shell syntax, such as process substitution inside an array subscript,
2075    /// can produce multiple AST words while still occupying one contiguous
2076    /// assignment operand in the source.
2077    pub fn parse_assignment_word_group(
2078        source: &str,
2079        words: &[&Word],
2080        explicit_array_kind: Option<ArrayKind>,
2081        subscript_interpretation: SubscriptInterpretation,
2082    ) -> Option<Assignment> {
2083        let first = words.first()?;
2084        let last = words.last()?;
2085        let span = Span::from_positions(first.span.start, last.span.end);
2086        let raw = span.slice(source);
2087        let mut parser = Parser::new(source);
2088        parser.parse_assignment_from_text(raw, span, explicit_array_kind, subscript_interpretation)
2089    }
2090
2091    /// Parse a word string with caller-configured limits.
2092    /// Prevents bypass of parser limits in parameter expansion contexts.
2093    pub fn parse_word_string_with_limits(input: &str, max_depth: usize, max_fuel: usize) -> Word {
2094        Self::parse_word_string_with_limits_and_dialect(
2095            input,
2096            max_depth,
2097            max_fuel,
2098            ShellDialect::Bash,
2099        )
2100    }
2101
2102    /// Parse a word string with caller-configured limits and shell dialect.
2103    pub fn parse_word_string_with_limits_and_dialect(
2104        input: &str,
2105        max_depth: usize,
2106        max_fuel: usize,
2107        dialect: ShellDialect,
2108    ) -> Word {
2109        let mut parser = Parser::with_limits_and_profile(
2110            input,
2111            max_depth,
2112            max_fuel,
2113            ShellProfile::native(dialect),
2114        );
2115        let start = Position::new();
2116        parser.parse_word_with_context(
2117            input,
2118            Span::from_positions(start, start.advanced_by(input)),
2119            start,
2120            true,
2121        )
2122    }
2123
2124    /// Parse a fragment against the original source span so part offsets stay
2125    /// aligned with the surrounding script.
2126    pub fn parse_word_fragment(source: &str, text: &str, span: Span) -> Word {
2127        let mut parser = Parser::new(text);
2128        let source_backed = span.end.offset <= source.len() && span.slice(source) == text;
2129        let start = Position::new();
2130        let fragment_span = Span::from_positions(start, start.advanced_by(text));
2131        let mut word = parser.parse_word_with_context(text, fragment_span, start, source_backed);
2132        if !source_backed {
2133            Self::materialize_word_source_backing(&mut word, text);
2134        }
2135        Self::rebase_word(&mut word, span.start);
2136        word.span = span;
2137        word
2138    }
2139
2140    fn maybe_record_comment(&mut self, token: &LexedToken<'_>) {
2141        if token.kind == TokenKind::Comment && !token.flags.is_synthetic() {
2142            self.comments.push(Comment {
2143                range: token.span.to_range(),
2144            });
2145        }
2146    }
2147
2148    fn record_zsh_brace_if_span(&mut self, span: Span) {
2149        if !self.syntax_facts.zsh_brace_if_spans.contains(&span) {
2150            self.syntax_facts.zsh_brace_if_spans.push(span);
2151        }
2152    }
2153
2154    fn record_zsh_always_span(&mut self, span: Span) {
2155        if !self.syntax_facts.zsh_always_spans.contains(&span) {
2156            self.syntax_facts.zsh_always_spans.push(span);
2157        }
2158    }
2159
2160    fn record_zsh_case_group_part(&mut self, pattern_part_index: usize, span: Span) {
2161        if !self
2162            .syntax_facts
2163            .zsh_case_group_parts
2164            .iter()
2165            .any(|fact| fact.pattern_part_index == pattern_part_index && fact.span == span)
2166        {
2167            self.syntax_facts
2168                .zsh_case_group_parts
2169                .push(ZshCaseGroupPart {
2170                    pattern_part_index,
2171                    span,
2172                });
2173        }
2174    }
2175
2176    fn word_text_needs_parse(text: &str) -> bool {
2177        memchr3(b'$', b'`', b'\0', text.as_bytes()).is_some()
2178    }
2179
2180    fn word_with_parts(&self, parts: Vec<WordPartNode>, span: Span) -> Word {
2181        let brace_syntax = self.brace_syntax_from_parts(&parts, span.start.offset);
2182        Word {
2183            parts,
2184            span,
2185            brace_syntax,
2186        }
2187    }
2188
2189    fn word_with_part_buffer(&self, parts: WordPartBuffer, span: Span) -> Word {
2190        let brace_syntax = self.brace_syntax_from_parts(&parts, span.start.offset);
2191        let parts = if parts.spilled() {
2192            parts.into_vec()
2193        } else {
2194            let mut vec = Vec::with_capacity(parts.len());
2195            vec.extend(parts);
2196            vec
2197        };
2198        Word {
2199            parts,
2200            span,
2201            brace_syntax,
2202        }
2203    }
2204
2205    fn word_with_single_part(&self, part: WordPartNode, span: Span) -> Word {
2206        let mut parts = WordPartBuffer::new();
2207        parts.push(part);
2208        self.word_with_part_buffer(parts, span)
2209    }
2210
2211    fn heredoc_body_with_parts(
2212        &self,
2213        parts: Vec<HeredocBodyPartNode>,
2214        span: Span,
2215        mode: HeredocBodyMode,
2216        source_backed: bool,
2217    ) -> HeredocBody {
2218        HeredocBody {
2219            mode,
2220            source_backed,
2221            parts,
2222            span,
2223        }
2224    }
2225
2226    fn heredoc_body_part_from_word_part_node(part: WordPartNode) -> HeredocBodyPartNode {
2227        let kind = match part.kind {
2228            WordPart::Literal(text) => HeredocBodyPart::Literal(text),
2229            WordPart::Variable(name) => HeredocBodyPart::Variable(name),
2230            WordPart::CommandSubstitution { body, syntax } => {
2231                HeredocBodyPart::CommandSubstitution { body, syntax }
2232            }
2233            WordPart::ArithmeticExpansion {
2234                expression,
2235                expression_ast,
2236                expression_word_ast,
2237                syntax,
2238            } => HeredocBodyPart::ArithmeticExpansion {
2239                expression,
2240                expression_ast,
2241                expression_word_ast,
2242                syntax,
2243            },
2244            WordPart::Parameter(parameter) => HeredocBodyPart::Parameter(Box::new(parameter)),
2245            other => panic!("unsupported heredoc body part: {other:?}"),
2246        };
2247
2248        HeredocBodyPartNode::new(kind, part.span)
2249    }
2250
2251    fn brace_syntax_from_parts(&self, parts: &[WordPartNode], offset: usize) -> Vec<BraceSyntax> {
2252        if !self.brace_syntax_enabled_at(offset) {
2253            return Vec::new();
2254        }
2255        let mut brace_syntax = Vec::new();
2256        self.collect_brace_syntax_from_parts(parts, BraceQuoteContext::Unquoted, &mut brace_syntax);
2257        brace_syntax.sort_by_key(|brace| (brace.span.start.offset, brace.span.end.offset));
2258        brace_syntax.dedup_by_key(|brace| {
2259            (
2260                brace.span.start.offset,
2261                brace.span.end.offset,
2262                brace.quote_context,
2263                brace.kind,
2264            )
2265        });
2266        brace_syntax
2267    }
2268
2269    fn collect_brace_syntax_from_parts(
2270        &self,
2271        parts: &[WordPartNode],
2272        quote_context: BraceQuoteContext,
2273        out: &mut Vec<BraceSyntax>,
2274    ) {
2275        for part in parts {
2276            match &part.kind {
2277                WordPart::Literal(text) => Self::scan_brace_syntax_text(
2278                    text.syntax_str(self.input, part.span),
2279                    part.span.start,
2280                    quote_context,
2281                    out,
2282                ),
2283                WordPart::SingleQuoted { .. } => Self::scan_brace_syntax_text(
2284                    part.span.slice(self.input),
2285                    part.span.start,
2286                    BraceQuoteContext::SingleQuoted,
2287                    out,
2288                ),
2289                WordPart::DoubleQuoted { parts, .. } => self.collect_brace_syntax_from_parts(
2290                    parts,
2291                    BraceQuoteContext::DoubleQuoted,
2292                    out,
2293                ),
2294                WordPart::ZshQualifiedGlob(glob) => {
2295                    self.collect_brace_syntax_from_zsh_qualified_glob(glob, quote_context, out)
2296                }
2297                WordPart::Variable(_)
2298                | WordPart::CommandSubstitution { .. }
2299                | WordPart::ArithmeticExpansion { .. }
2300                | WordPart::Parameter(_)
2301                | WordPart::ParameterExpansion { .. }
2302                | WordPart::Length(_)
2303                | WordPart::ArrayAccess(_)
2304                | WordPart::ArrayLength(_)
2305                | WordPart::ArrayIndices(_)
2306                | WordPart::Substring { .. }
2307                | WordPart::ArraySlice { .. }
2308                | WordPart::IndirectExpansion { .. }
2309                | WordPart::PrefixMatch { .. }
2310                | WordPart::ProcessSubstitution { .. }
2311                | WordPart::Transformation { .. } => {}
2312            }
2313        }
2314
2315        if self.needs_cross_part_brace_scan(parts) {
2316            let mut chars = Vec::new();
2317            self.collect_brace_scan_chars_from_parts(parts, &mut chars);
2318            Self::scan_brace_syntax_chars(&chars, quote_context, out);
2319        }
2320    }
2321
2322    fn needs_cross_part_brace_scan(&self, parts: &[WordPartNode]) -> bool {
2323        if parts.len() < 2 {
2324            return false;
2325        }
2326
2327        let mut cursor = parts[0].span.start.offset;
2328        for part in parts {
2329            if part.span.start.offset > cursor
2330                && self.input[cursor..part.span.start.offset].contains(['{', '}'])
2331            {
2332                return true;
2333            }
2334
2335            let has_brace_text = match &part.kind {
2336                WordPart::Literal(text) => {
2337                    text.syntax_str(self.input, part.span).contains(['{', '}'])
2338                }
2339                WordPart::SingleQuoted { .. }
2340                | WordPart::DoubleQuoted { .. }
2341                | WordPart::ZshQualifiedGlob(_) => part.span.slice(self.input).contains(['{', '}']),
2342                WordPart::Variable(_)
2343                | WordPart::CommandSubstitution { .. }
2344                | WordPart::ArithmeticExpansion { .. }
2345                | WordPart::Parameter(_)
2346                | WordPart::ParameterExpansion { .. }
2347                | WordPart::Length(_)
2348                | WordPart::ArrayAccess(_)
2349                | WordPart::ArrayLength(_)
2350                | WordPart::ArrayIndices(_)
2351                | WordPart::Substring { .. }
2352                | WordPart::ArraySlice { .. }
2353                | WordPart::IndirectExpansion { .. }
2354                | WordPart::PrefixMatch { .. }
2355                | WordPart::ProcessSubstitution { .. }
2356                | WordPart::Transformation { .. } => false,
2357            };
2358            if has_brace_text {
2359                return true;
2360            }
2361
2362            cursor = part.span.end.offset;
2363        }
2364
2365        false
2366    }
2367
2368    fn collect_brace_scan_chars_from_parts(
2369        &self,
2370        parts: &[WordPartNode],
2371        out: &mut Vec<(char, Position)>,
2372    ) {
2373        for part in parts {
2374            match &part.kind {
2375                WordPart::Literal(text) => {
2376                    Self::push_brace_scan_text(
2377                        text.syntax_str(self.input, part.span),
2378                        part.span.start,
2379                        out,
2380                    );
2381                }
2382                WordPart::SingleQuoted { .. } => {
2383                    Self::push_brace_scan_text(part.span.slice(self.input), part.span.start, out);
2384                }
2385                WordPart::DoubleQuoted { parts, .. } => {
2386                    self.collect_brace_scan_chars_from_double_quoted_part(part.span, parts, out);
2387                }
2388                WordPart::ZshQualifiedGlob(_) => {}
2389                WordPart::Variable(_)
2390                | WordPart::CommandSubstitution { .. }
2391                | WordPart::ArithmeticExpansion { .. }
2392                | WordPart::Parameter(_)
2393                | WordPart::ParameterExpansion { .. }
2394                | WordPart::Length(_)
2395                | WordPart::ArrayAccess(_)
2396                | WordPart::ArrayLength(_)
2397                | WordPart::ArrayIndices(_)
2398                | WordPart::Substring { .. }
2399                | WordPart::ArraySlice { .. }
2400                | WordPart::IndirectExpansion { .. }
2401                | WordPart::PrefixMatch { .. }
2402                | WordPart::ProcessSubstitution { .. }
2403                | WordPart::Transformation { .. } => {
2404                    Self::push_brace_scan_boundary(part.span.start, out);
2405                }
2406            }
2407        }
2408    }
2409
2410    fn collect_brace_scan_chars_from_double_quoted_part(
2411        &self,
2412        span: Span,
2413        parts: &[WordPartNode],
2414        out: &mut Vec<(char, Position)>,
2415    ) {
2416        let mut cursor_offset = span.start.offset;
2417        let mut cursor_position = span.start;
2418
2419        for part in parts {
2420            if part.span.start.offset > cursor_offset
2421                && let Some(raw) = self.input.get(cursor_offset..part.span.start.offset)
2422            {
2423                Self::push_brace_scan_text(raw, cursor_position, out);
2424            }
2425
2426            match &part.kind {
2427                WordPart::Literal(text) => {
2428                    Self::push_brace_scan_text(
2429                        text.syntax_str(self.input, part.span),
2430                        part.span.start,
2431                        out,
2432                    );
2433                }
2434                WordPart::SingleQuoted { .. } => {
2435                    Self::push_brace_scan_text(part.span.slice(self.input), part.span.start, out);
2436                }
2437                WordPart::DoubleQuoted { parts, .. } => {
2438                    self.collect_brace_scan_chars_from_double_quoted_part(part.span, parts, out);
2439                }
2440                WordPart::ZshQualifiedGlob(_) => {}
2441                WordPart::Variable(_)
2442                | WordPart::CommandSubstitution { .. }
2443                | WordPart::ArithmeticExpansion { .. }
2444                | WordPart::Parameter(_)
2445                | WordPart::ParameterExpansion { .. }
2446                | WordPart::Length(_)
2447                | WordPart::ArrayAccess(_)
2448                | WordPart::ArrayLength(_)
2449                | WordPart::ArrayIndices(_)
2450                | WordPart::Substring { .. }
2451                | WordPart::ArraySlice { .. }
2452                | WordPart::IndirectExpansion { .. }
2453                | WordPart::PrefixMatch { .. }
2454                | WordPart::ProcessSubstitution { .. }
2455                | WordPart::Transformation { .. } => {
2456                    Self::push_brace_scan_boundary(part.span.start, out);
2457                }
2458            }
2459
2460            cursor_offset = part.span.end.offset;
2461            cursor_position = part.span.end;
2462        }
2463
2464        if cursor_offset < span.end.offset
2465            && let Some(raw) = self.input.get(cursor_offset..span.end.offset)
2466        {
2467            Self::push_brace_scan_text(raw, cursor_position, out);
2468        }
2469    }
2470
2471    fn push_brace_scan_text(text: &str, start: Position, out: &mut Vec<(char, Position)>) {
2472        let mut position = start;
2473        for ch in text.chars() {
2474            out.push((ch, position));
2475            position.advance(ch);
2476        }
2477    }
2478
2479    fn push_brace_scan_boundary(position: Position, out: &mut Vec<(char, Position)>) {
2480        out.push(('\0', position));
2481    }
2482
2483    fn scan_brace_syntax_chars(
2484        chars: &[(char, Position)],
2485        initial_quote_context: BraceQuoteContext,
2486        out: &mut Vec<BraceSyntax>,
2487    ) {
2488        #[derive(Clone, Copy, PartialEq, Eq)]
2489        enum QuoteState {
2490            Single,
2491            AnsiSingle,
2492            Double,
2493        }
2494
2495        #[derive(Clone, Copy)]
2496        struct Candidate {
2497            start: Position,
2498            quote_context: BraceQuoteContext,
2499            has_comma: bool,
2500            has_dot_dot: bool,
2501            saw_unquoted_whitespace: bool,
2502            saw_quote_boundary: bool,
2503            prev_char: Option<char>,
2504        }
2505
2506        fn quote_state(context: BraceQuoteContext) -> Option<QuoteState> {
2507            match context {
2508                BraceQuoteContext::Unquoted => None,
2509                BraceQuoteContext::SingleQuoted => Some(QuoteState::Single),
2510                BraceQuoteContext::DoubleQuoted => Some(QuoteState::Double),
2511            }
2512        }
2513
2514        fn quote_context(state: Option<QuoteState>) -> BraceQuoteContext {
2515            match state {
2516                None => BraceQuoteContext::Unquoted,
2517                Some(QuoteState::Single | QuoteState::AnsiSingle) => {
2518                    BraceQuoteContext::SingleQuoted
2519                }
2520                Some(QuoteState::Double) => BraceQuoteContext::DoubleQuoted,
2521            }
2522        }
2523
2524        fn quote_context_is_active(context: BraceQuoteContext, state: Option<QuoteState>) -> bool {
2525            match context {
2526                BraceQuoteContext::Unquoted => state.is_none(),
2527                BraceQuoteContext::SingleQuoted => {
2528                    matches!(state, Some(QuoteState::Single | QuoteState::AnsiSingle))
2529                }
2530                BraceQuoteContext::DoubleQuoted => matches!(state, Some(QuoteState::Double)),
2531            }
2532        }
2533
2534        fn last_active_candidate_index(
2535            stack: &[Candidate],
2536            state: Option<QuoteState>,
2537        ) -> Option<usize> {
2538            stack
2539                .iter()
2540                .rposition(|candidate| quote_context_is_active(candidate.quote_context, state))
2541        }
2542
2543        fn template_placeholder_end(
2544            chars: &[(char, Position)],
2545            start: usize,
2546            quote_context: BraceQuoteContext,
2547        ) -> Option<usize> {
2548            if chars.get(start)?.0 != '{' || chars.get(start + 1)?.0 != '{' {
2549                return None;
2550            }
2551
2552            let mut index = start + 2;
2553            let mut depth = 1usize;
2554
2555            while index < chars.len() {
2556                if matches!(quote_context, BraceQuoteContext::Unquoted) && chars[index].0 == '\\' {
2557                    index += 1;
2558                    if index < chars.len() {
2559                        index += 1;
2560                    }
2561                    continue;
2562                }
2563
2564                if chars.get(index).is_some_and(|(ch, _)| *ch == '{')
2565                    && chars.get(index + 1).is_some_and(|(ch, _)| *ch == '{')
2566                {
2567                    depth += 1;
2568                    index += 2;
2569                    continue;
2570                }
2571
2572                if chars.get(index).is_some_and(|(ch, _)| *ch == '}')
2573                    && chars.get(index + 1).is_some_and(|(ch, _)| *ch == '}')
2574                {
2575                    depth -= 1;
2576                    index += 2;
2577                    if depth == 0 {
2578                        return Some(index);
2579                    }
2580                    continue;
2581                }
2582
2583                index += 1;
2584            }
2585
2586            None
2587        }
2588
2589        fn brace_span(start: Position, end: (char, Position)) -> Span {
2590            let mut end_position = end.1;
2591            end_position.advance(end.0);
2592            Span::from_positions(start, end_position)
2593        }
2594
2595        let mut index = 0usize;
2596        let mut quote_state = quote_state(initial_quote_context);
2597        let mut stack = Vec::<Candidate>::new();
2598
2599        while index < chars.len() {
2600            let ch = chars[index].0;
2601
2602            if matches!(
2603                quote_state,
2604                None | Some(QuoteState::AnsiSingle) | Some(QuoteState::Double)
2605            ) && ch == '\\'
2606            {
2607                if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state) {
2608                    stack[candidate_index].prev_char = None;
2609                }
2610                index += 1;
2611                if index < chars.len() {
2612                    index += 1;
2613                }
2614                continue;
2615            }
2616
2617            let current_quote_context = quote_context(quote_state);
2618            if ch == '{'
2619                && let Some(end_index) =
2620                    template_placeholder_end(chars, index, current_quote_context)
2621            {
2622                out.push(BraceSyntax {
2623                    kind: BraceSyntaxKind::TemplatePlaceholder,
2624                    span: brace_span(chars[index].1, chars[end_index - 1]),
2625                    quote_context: current_quote_context,
2626                });
2627                if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state) {
2628                    stack[candidate_index].prev_char = None;
2629                }
2630                index = end_index;
2631                continue;
2632            }
2633
2634            match quote_state {
2635                None => match ch {
2636                    '\'' => {
2637                        quote_state = if index > 0 && chars[index - 1].0 == '$' {
2638                            Some(QuoteState::AnsiSingle)
2639                        } else {
2640                            Some(QuoteState::Single)
2641                        };
2642                        for candidate in &mut stack {
2643                            if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
2644                                candidate.saw_quote_boundary = true;
2645                            }
2646                        }
2647                    }
2648                    '"' => {
2649                        quote_state = Some(QuoteState::Double);
2650                        for candidate in &mut stack {
2651                            if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
2652                                candidate.saw_quote_boundary = true;
2653                            }
2654                        }
2655                    }
2656                    _ => {}
2657                },
2658                Some(QuoteState::Single) => {
2659                    if ch == '\'' {
2660                        quote_state = None;
2661                        for candidate in &mut stack {
2662                            if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
2663                                candidate.saw_quote_boundary = true;
2664                            }
2665                        }
2666                    }
2667                }
2668                Some(QuoteState::AnsiSingle) => {
2669                    if ch == '\'' {
2670                        quote_state = None;
2671                        for candidate in &mut stack {
2672                            if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
2673                                candidate.saw_quote_boundary = true;
2674                            }
2675                        }
2676                    }
2677                }
2678                Some(QuoteState::Double) => {
2679                    if ch == '"' {
2680                        quote_state = None;
2681                        for candidate in &mut stack {
2682                            if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
2683                                candidate.saw_quote_boundary = true;
2684                            }
2685                        }
2686                    }
2687                }
2688            }
2689
2690            match ch {
2691                '{' => {
2692                    stack.push(Candidate {
2693                        start: chars[index].1,
2694                        quote_context: current_quote_context,
2695                        has_comma: false,
2696                        has_dot_dot: false,
2697                        saw_unquoted_whitespace: false,
2698                        saw_quote_boundary: false,
2699                        prev_char: Some(ch),
2700                    });
2701                    index += 1;
2702                    continue;
2703                }
2704                '}' => {
2705                    if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state)
2706                    {
2707                        let candidate = stack.remove(candidate_index);
2708                        let kind = if matches!(candidate.quote_context, BraceQuoteContext::Unquoted)
2709                            && candidate.saw_unquoted_whitespace
2710                        {
2711                            BraceSyntaxKind::Literal
2712                        } else if candidate.has_comma {
2713                            BraceSyntaxKind::Expansion(BraceExpansionKind::CommaList)
2714                        } else if candidate.has_dot_dot {
2715                            BraceSyntaxKind::Expansion(BraceExpansionKind::Sequence)
2716                        } else {
2717                            BraceSyntaxKind::Literal
2718                        };
2719                        if !matches!(kind, BraceSyntaxKind::Literal)
2720                            || !candidate.saw_quote_boundary
2721                        {
2722                            out.push(BraceSyntax {
2723                                kind,
2724                                span: brace_span(candidate.start, chars[index]),
2725                                quote_context: candidate.quote_context,
2726                            });
2727                        }
2728                    }
2729                }
2730                _ => {
2731                    if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state)
2732                    {
2733                        let candidate = &mut stack[candidate_index];
2734                        let counts_as_top_level =
2735                            quote_context_is_active(candidate.quote_context, quote_state);
2736
2737                        if counts_as_top_level {
2738                            match ch {
2739                                ',' => candidate.has_comma = true,
2740                                '.' if candidate.prev_char == Some('.') => {
2741                                    candidate.has_dot_dot = true;
2742                                }
2743                                c if matches!(
2744                                    candidate.quote_context,
2745                                    BraceQuoteContext::Unquoted
2746                                ) && quote_state.is_none()
2747                                    && c.is_whitespace() =>
2748                                {
2749                                    candidate.saw_unquoted_whitespace = true;
2750                                }
2751                                _ => {}
2752                            }
2753                        }
2754                    }
2755                }
2756            }
2757
2758            if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state) {
2759                stack[candidate_index].prev_char = Some(ch);
2760            }
2761
2762            index += 1;
2763        }
2764    }
2765
2766    fn collect_brace_syntax_from_pattern(
2767        &self,
2768        pattern: &Pattern,
2769        quote_context: BraceQuoteContext,
2770        out: &mut Vec<BraceSyntax>,
2771    ) {
2772        for (part, span) in pattern.parts_with_spans() {
2773            match part {
2774                PatternPart::Literal(text) => Self::scan_brace_syntax_text(
2775                    text.as_str(self.input, span),
2776                    span.start,
2777                    quote_context,
2778                    out,
2779                ),
2780                PatternPart::CharClass(text) => Self::scan_brace_syntax_text(
2781                    text.slice(self.input),
2782                    text.span().start,
2783                    quote_context,
2784                    out,
2785                ),
2786                PatternPart::Group { patterns, .. } => {
2787                    for pattern in patterns {
2788                        self.collect_brace_syntax_from_pattern(pattern, quote_context, out);
2789                    }
2790                }
2791                PatternPart::Word(word) => {
2792                    self.collect_brace_syntax_from_parts(&word.parts, quote_context, out)
2793                }
2794                PatternPart::AnyString | PatternPart::AnyChar => {}
2795            }
2796        }
2797    }
2798
2799    fn collect_brace_syntax_from_zsh_qualified_glob(
2800        &self,
2801        glob: &ZshQualifiedGlob,
2802        quote_context: BraceQuoteContext,
2803        out: &mut Vec<BraceSyntax>,
2804    ) {
2805        for segment in &glob.segments {
2806            if let ZshGlobSegment::Pattern(pattern) = segment {
2807                self.collect_brace_syntax_from_pattern(pattern, quote_context, out);
2808            }
2809        }
2810    }
2811
2812    fn scan_brace_syntax_text(
2813        text: &str,
2814        base: Position,
2815        quote_context: BraceQuoteContext,
2816        out: &mut Vec<BraceSyntax>,
2817    ) {
2818        if memchr(b'{', text.as_bytes()).is_none() {
2819            return;
2820        }
2821
2822        #[derive(Clone, Copy)]
2823        struct ScanFrame<'a> {
2824            text: &'a str,
2825            index: usize,
2826            position: Position,
2827        }
2828
2829        let mut work = SmallVec::<[ScanFrame<'_>; 2]>::new();
2830        work.push(ScanFrame {
2831            text,
2832            index: 0,
2833            position: base,
2834        });
2835
2836        while let Some(mut frame) = work.pop() {
2837            let bytes = frame.text.as_bytes();
2838
2839            while frame.index < bytes.len() {
2840                let next_special = if matches!(quote_context, BraceQuoteContext::Unquoted) {
2841                    memchr2(b'{', b'\\', &bytes[frame.index..])
2842                        .map(|relative| frame.index + relative)
2843                } else {
2844                    memchr(b'{', &bytes[frame.index..]).map(|relative| frame.index + relative)
2845                };
2846
2847                let Some(next_index) = next_special else {
2848                    break;
2849                };
2850
2851                if next_index > frame.index {
2852                    frame.position = frame
2853                        .position
2854                        .advanced_by(&frame.text[frame.index..next_index]);
2855                    frame.index = next_index;
2856                }
2857
2858                if matches!(quote_context, BraceQuoteContext::Unquoted)
2859                    && bytes[frame.index] == b'\\'
2860                {
2861                    let escaped_start = frame.index;
2862                    frame.index += 1;
2863                    if let Some(next) = frame.text[frame.index..].chars().next() {
2864                        frame.index += next.len_utf8();
2865                    }
2866                    frame.position = frame
2867                        .position
2868                        .advanced_by(&frame.text[escaped_start..frame.index]);
2869                    continue;
2870                }
2871
2872                let brace_start = frame.position;
2873                if let Some(len) =
2874                    Self::template_placeholder_len(frame.text, frame.index, quote_context)
2875                {
2876                    let brace_end =
2877                        brace_start.advanced_by(&frame.text[frame.index..frame.index + len]);
2878                    out.push(BraceSyntax {
2879                        kind: BraceSyntaxKind::TemplatePlaceholder,
2880                        span: Span::from_positions(brace_start, brace_end),
2881                        quote_context,
2882                    });
2883                    frame.position = brace_end;
2884                    frame.index += len;
2885                    continue;
2886                }
2887
2888                if let Some((len, kind)) =
2889                    Self::brace_construct_len(frame.text, frame.index, quote_context)
2890                {
2891                    let brace_end =
2892                        brace_start.advanced_by(&frame.text[frame.index..frame.index + len]);
2893                    out.push(BraceSyntax {
2894                        kind,
2895                        span: Span::from_positions(brace_start, brace_end),
2896                        quote_context,
2897                    });
2898
2899                    frame.position = brace_end;
2900                    frame.index += len;
2901
2902                    if len > 2 {
2903                        let inner_start = frame.index - len + '{'.len_utf8();
2904                        let inner_end = frame.index - '}'.len_utf8();
2905                        if inner_start < inner_end {
2906                            let inner_base = brace_start.advanced_by("{");
2907                            work.push(frame);
2908                            work.push(ScanFrame {
2909                                text: &frame.text[inner_start..inner_end],
2910                                index: 0,
2911                                position: inner_base,
2912                            });
2913                            break;
2914                        }
2915                    }
2916
2917                    continue;
2918                }
2919
2920                frame.position.advance('{');
2921                frame.index += '{'.len_utf8();
2922            }
2923        }
2924    }
2925
2926    fn text_position(base: Position, text: &str, offset: usize) -> Position {
2927        base.advanced_by(&text[..offset])
2928    }
2929
2930    fn template_placeholder_len(
2931        text: &str,
2932        start: usize,
2933        quote_context: BraceQuoteContext,
2934    ) -> Option<usize> {
2935        text.get(start..).filter(|rest| rest.starts_with("{{"))?;
2936
2937        let mut index = start + "{{".len();
2938        let mut depth = 1usize;
2939
2940        while index < text.len() {
2941            if matches!(quote_context, BraceQuoteContext::Unquoted)
2942                && text[index..].starts_with('\\')
2943            {
2944                index += 1;
2945                if let Some(next) = text[index..].chars().next() {
2946                    index += next.len_utf8();
2947                }
2948                continue;
2949            }
2950
2951            if text[index..].starts_with("{{") {
2952                depth += 1;
2953                index += "{{".len();
2954                continue;
2955            }
2956
2957            if text[index..].starts_with("}}") {
2958                depth -= 1;
2959                index += "}}".len();
2960                if depth == 0 {
2961                    return Some(index - start);
2962                }
2963                continue;
2964            }
2965
2966            index += text[index..].chars().next()?.len_utf8();
2967        }
2968
2969        None
2970    }
2971
2972    fn brace_construct_len(
2973        text: &str,
2974        start: usize,
2975        quote_context: BraceQuoteContext,
2976    ) -> Option<(usize, BraceSyntaxKind)> {
2977        text.get(start..).filter(|rest| rest.starts_with('{'))?;
2978
2979        #[derive(Clone, Copy, PartialEq, Eq)]
2980        enum QuoteState {
2981            Single,
2982            Double,
2983        }
2984
2985        let mut index = start + '{'.len_utf8();
2986        let mut depth = 1usize;
2987        let mut has_comma = false;
2988        let mut has_dot_dot = false;
2989        let mut saw_unquoted_whitespace = false;
2990        let mut prev_char = None;
2991        let mut quote_state = None;
2992
2993        while index < text.len() {
2994            if matches!(quote_context, BraceQuoteContext::Unquoted)
2995                && quote_state.is_none()
2996                && text[index..].starts_with('\\')
2997            {
2998                index += 1;
2999                if let Some(next) = text[index..].chars().next() {
3000                    index += next.len_utf8();
3001                }
3002                prev_char = None;
3003                continue;
3004            }
3005
3006            let ch = text[index..].chars().next()?;
3007            index += ch.len_utf8();
3008
3009            if matches!(quote_context, BraceQuoteContext::Unquoted) {
3010                match quote_state {
3011                    None => match ch {
3012                        '\'' => {
3013                            quote_state = Some(QuoteState::Single);
3014                            prev_char = None;
3015                            continue;
3016                        }
3017                        '"' => {
3018                            quote_state = Some(QuoteState::Double);
3019                            prev_char = None;
3020                            continue;
3021                        }
3022                        '{' => depth += 1,
3023                        '}' => {
3024                            depth -= 1;
3025                            if depth == 0 {
3026                                let kind = if saw_unquoted_whitespace {
3027                                    BraceSyntaxKind::Literal
3028                                } else if has_comma {
3029                                    BraceSyntaxKind::Expansion(BraceExpansionKind::CommaList)
3030                                } else if has_dot_dot {
3031                                    BraceSyntaxKind::Expansion(BraceExpansionKind::Sequence)
3032                                } else {
3033                                    BraceSyntaxKind::Literal
3034                                };
3035                                return Some((index - start, kind));
3036                            }
3037                        }
3038                        ',' if depth == 1 => has_comma = true,
3039                        '.' if depth == 1 && prev_char == Some('.') => has_dot_dot = true,
3040                        c if c.is_whitespace() => saw_unquoted_whitespace = true,
3041                        _ => {}
3042                    },
3043                    Some(QuoteState::Single) => {
3044                        if ch == '\'' {
3045                            quote_state = None;
3046                        }
3047                    }
3048                    Some(QuoteState::Double) => match ch {
3049                        '\\' => {
3050                            if let Some(next) = text[index..].chars().next() {
3051                                index += next.len_utf8();
3052                            }
3053                            prev_char = None;
3054                            continue;
3055                        }
3056                        '"' => quote_state = None,
3057                        _ => {}
3058                    },
3059                }
3060            } else {
3061                match ch {
3062                    '{' => depth += 1,
3063                    '}' => {
3064                        depth -= 1;
3065                        if depth == 0 {
3066                            let kind = if has_comma {
3067                                BraceSyntaxKind::Expansion(BraceExpansionKind::CommaList)
3068                            } else if has_dot_dot {
3069                                BraceSyntaxKind::Expansion(BraceExpansionKind::Sequence)
3070                            } else {
3071                                BraceSyntaxKind::Literal
3072                            };
3073                            return Some((index - start, kind));
3074                        }
3075                    }
3076                    ',' if depth == 1 => has_comma = true,
3077                    '.' if depth == 1 && prev_char == Some('.') => has_dot_dot = true,
3078                    _ => {}
3079                }
3080            }
3081
3082            prev_char = Some(ch);
3083        }
3084
3085        None
3086    }
3087
3088    fn maybe_parse_zsh_qualified_glob_word(
3089        &mut self,
3090        text: &str,
3091        span: Span,
3092        source_backed: bool,
3093    ) -> Option<Word> {
3094        if !self.zsh_glob_qualifiers_enabled_at(span.start.offset)
3095            || text.is_empty()
3096            || text.contains('=')
3097            || text.contains(['\x00', '\\', '\'', '"', '$', '`'])
3098            || text.chars().any(char::is_whitespace)
3099        {
3100            return None;
3101        }
3102
3103        let (segments, qualifiers) =
3104            self.parse_zsh_qualified_glob_segments(text, span, source_backed)?;
3105        if !segments.iter().any(|segment| {
3106            matches!(
3107                segment,
3108                ZshGlobSegment::Pattern(pattern) if Self::pattern_has_glob_syntax(pattern)
3109            )
3110        }) {
3111            return None;
3112        }
3113
3114        Some(self.word_with_parts(
3115            vec![WordPartNode::new(
3116                WordPart::ZshQualifiedGlob(ZshQualifiedGlob {
3117                    span,
3118                    segments,
3119                    qualifiers,
3120                }),
3121                span,
3122            )],
3123            span,
3124        ))
3125    }
3126
3127    fn parse_zsh_qualified_glob_segments(
3128        &mut self,
3129        text: &str,
3130        span: Span,
3131        source_backed: bool,
3132    ) -> Option<(Vec<ZshGlobSegment>, Option<ZshGlobQualifierGroup>)> {
3133        let mut segments = Vec::new();
3134        let mut qualifiers = None;
3135        let mut pattern_start = 0usize;
3136        let mut index = 0usize;
3137
3138        while index < text.len() {
3139            if text[index..].starts_with("(#") {
3140                if let Some((len, control)) =
3141                    self.parse_zsh_inline_glob_control(text, span.start, index)
3142                {
3143                    self.push_zsh_pattern_segment(
3144                        &mut segments,
3145                        text,
3146                        span.start,
3147                        pattern_start,
3148                        index,
3149                        source_backed,
3150                    );
3151                    segments.push(ZshGlobSegment::InlineControl(control));
3152                    index += len;
3153                    pattern_start = index;
3154                    continue;
3155                }
3156
3157                let suffix_start = Self::text_position(span.start, text, index);
3158                if let Some(group) = self.parse_zsh_terminal_glob_qualifier_group(
3159                    &text[index..],
3160                    suffix_start,
3161                    source_backed,
3162                ) {
3163                    self.push_zsh_pattern_segment(
3164                        &mut segments,
3165                        text,
3166                        span.start,
3167                        pattern_start,
3168                        index,
3169                        source_backed,
3170                    );
3171                    qualifiers = Some(group);
3172                    index = text.len();
3173                    pattern_start = index;
3174                    break;
3175                }
3176
3177                return None;
3178            }
3179
3180            if text[index..].starts_with('(') {
3181                let suffix_start = Self::text_position(span.start, text, index);
3182                if let Some(group) = self.parse_zsh_terminal_glob_qualifier_group(
3183                    &text[index..],
3184                    suffix_start,
3185                    source_backed,
3186                ) && matches!(group.kind, ZshGlobQualifierKind::Classic)
3187                {
3188                    self.push_zsh_pattern_segment(
3189                        &mut segments,
3190                        text,
3191                        span.start,
3192                        pattern_start,
3193                        index,
3194                        source_backed,
3195                    );
3196                    qualifiers = Some(group);
3197                    index = text.len();
3198                    pattern_start = index;
3199                    break;
3200                }
3201            }
3202
3203            index += text[index..].chars().next()?.len_utf8();
3204        }
3205
3206        self.push_zsh_pattern_segment(
3207            &mut segments,
3208            text,
3209            span.start,
3210            pattern_start,
3211            text.len(),
3212            source_backed,
3213        );
3214
3215        segments
3216            .iter()
3217            .any(|segment| matches!(segment, ZshGlobSegment::Pattern(_)))
3218            .then_some((segments, qualifiers))
3219    }
3220
3221    fn push_zsh_pattern_segment(
3222        &mut self,
3223        segments: &mut Vec<ZshGlobSegment>,
3224        text: &str,
3225        base: Position,
3226        start: usize,
3227        end: usize,
3228        source_backed: bool,
3229    ) {
3230        if start >= end {
3231            return;
3232        }
3233
3234        let start_position = Self::text_position(base, text, start);
3235        let end_position = Self::text_position(base, text, end);
3236        let span = Span::from_positions(start_position, end_position);
3237        let pattern_word =
3238            self.decode_word_text(&text[start..end], span, span.start, source_backed);
3239        segments.push(ZshGlobSegment::Pattern(
3240            self.pattern_from_word(&pattern_word),
3241        ));
3242    }
3243
3244    fn parse_zsh_inline_glob_control(
3245        &self,
3246        text: &str,
3247        base: Position,
3248        start: usize,
3249    ) -> Option<(usize, ZshInlineGlobControl)> {
3250        let (len, control) = if text[start..].starts_with("(#i)") {
3251            (
3252                "(#i)".len(),
3253                ZshInlineGlobControl::CaseInsensitive {
3254                    span: Span::from_positions(
3255                        Self::text_position(base, text, start),
3256                        Self::text_position(base, text, start + "(#i)".len()),
3257                    ),
3258                },
3259            )
3260        } else if text[start..].starts_with("(#b)") {
3261            (
3262                "(#b)".len(),
3263                ZshInlineGlobControl::Backreferences {
3264                    span: Span::from_positions(
3265                        Self::text_position(base, text, start),
3266                        Self::text_position(base, text, start + "(#b)".len()),
3267                    ),
3268                },
3269            )
3270        } else if text[start..].starts_with("(#s)") {
3271            (
3272                "(#s)".len(),
3273                ZshInlineGlobControl::StartAnchor {
3274                    span: Span::from_positions(
3275                        Self::text_position(base, text, start),
3276                        Self::text_position(base, text, start + "(#s)".len()),
3277                    ),
3278                },
3279            )
3280        } else if text[start..].starts_with("(#e)") {
3281            (
3282                "(#e)".len(),
3283                ZshInlineGlobControl::EndAnchor {
3284                    span: Span::from_positions(
3285                        Self::text_position(base, text, start),
3286                        Self::text_position(base, text, start + "(#e)".len()),
3287                    ),
3288                },
3289            )
3290        } else {
3291            return None;
3292        };
3293
3294        Some((len, control))
3295    }
3296
3297    fn parse_zsh_terminal_glob_qualifier_group(
3298        &self,
3299        text: &str,
3300        base: Position,
3301        source_backed: bool,
3302    ) -> Option<ZshGlobQualifierGroup> {
3303        let (kind, prefix_len, inner) = if let Some(inner) = text
3304            .strip_prefix("(#q")
3305            .and_then(|rest| rest.strip_suffix(')'))
3306        {
3307            (ZshGlobQualifierKind::HashQ, "(#q".len(), inner)
3308        } else {
3309            let inner = text.strip_prefix('(')?.strip_suffix(')')?;
3310            (ZshGlobQualifierKind::Classic, "(".len(), inner)
3311        };
3312
3313        let fragments = self.parse_zsh_glob_qualifier_fragments(
3314            inner,
3315            Self::text_position(base, text, prefix_len),
3316            source_backed,
3317        )?;
3318
3319        Some(ZshGlobQualifierGroup {
3320            span: Span::from_positions(base, Self::text_position(base, text, text.len())),
3321            kind,
3322            fragments,
3323        })
3324    }
3325
3326    fn parse_zsh_glob_qualifier_fragments(
3327        &self,
3328        text: &str,
3329        base: Position,
3330        source_backed: bool,
3331    ) -> Option<Vec<ZshGlobQualifier>> {
3332        let mut fragments = Vec::new();
3333        let mut index = 0;
3334        let mut saw_non_letter_fragment = false;
3335
3336        while index < text.len() {
3337            let start = index;
3338            let ch = text[index..].chars().next()?;
3339
3340            match ch {
3341                '^' => {
3342                    index += ch.len_utf8();
3343                    fragments.push(ZshGlobQualifier::Negation {
3344                        span: Span::from_positions(
3345                            Self::text_position(base, text, start),
3346                            Self::text_position(base, text, index),
3347                        ),
3348                    });
3349                    saw_non_letter_fragment = true;
3350                }
3351                '.' | '/' | '-' | 'A'..='Z' => {
3352                    index += ch.len_utf8();
3353                    fragments.push(ZshGlobQualifier::Flag {
3354                        name: ch,
3355                        span: Span::from_positions(
3356                            Self::text_position(base, text, start),
3357                            Self::text_position(base, text, index),
3358                        ),
3359                    });
3360                    saw_non_letter_fragment = true;
3361                }
3362                '[' => {
3363                    index += ch.len_utf8();
3364                    let number_start = index;
3365                    while matches!(text[index..].chars().next(), Some('0'..='9')) {
3366                        index += 1;
3367                    }
3368                    if number_start == index {
3369                        return None;
3370                    }
3371
3372                    let start_text = self.zsh_glob_qualifier_source_text(
3373                        text,
3374                        base,
3375                        number_start,
3376                        index,
3377                        source_backed,
3378                    );
3379                    let end_text = if text[index..].starts_with(',') {
3380                        index += 1;
3381                        let range_start = index;
3382                        while matches!(text[index..].chars().next(), Some('0'..='9')) {
3383                            index += 1;
3384                        }
3385                        if range_start == index {
3386                            return None;
3387                        }
3388                        Some(self.zsh_glob_qualifier_source_text(
3389                            text,
3390                            base,
3391                            range_start,
3392                            index,
3393                            source_backed,
3394                        ))
3395                    } else {
3396                        None
3397                    };
3398
3399                    if !text[index..].starts_with(']') {
3400                        return None;
3401                    }
3402                    index += "]".len();
3403                    fragments.push(ZshGlobQualifier::NumericArgument {
3404                        span: Span::from_positions(
3405                            Self::text_position(base, text, start),
3406                            Self::text_position(base, text, index),
3407                        ),
3408                        start: start_text,
3409                        end: end_text,
3410                    });
3411                    saw_non_letter_fragment = true;
3412                }
3413                'a'..='z' => {
3414                    index += ch.len_utf8();
3415                    while matches!(text[index..].chars().next(), Some('a'..='z' | 'A'..='Z')) {
3416                        index += 1;
3417                    }
3418                    if index - start <= 1 {
3419                        return None;
3420                    }
3421                    fragments.push(ZshGlobQualifier::LetterSequence {
3422                        text: self.zsh_glob_qualifier_source_text(
3423                            text,
3424                            base,
3425                            start,
3426                            index,
3427                            source_backed,
3428                        ),
3429                        span: Span::from_positions(
3430                            Self::text_position(base, text, start),
3431                            Self::text_position(base, text, index),
3432                        ),
3433                    });
3434                }
3435                _ => return None,
3436            }
3437        }
3438
3439        (!fragments.is_empty() && saw_non_letter_fragment).then_some(fragments)
3440    }
3441
3442    fn zsh_glob_qualifier_source_text(
3443        &self,
3444        text: &str,
3445        base: Position,
3446        start: usize,
3447        end: usize,
3448        source_backed: bool,
3449    ) -> SourceText {
3450        let span = Span::from_positions(
3451            Self::text_position(base, text, start),
3452            Self::text_position(base, text, end),
3453        );
3454        if source_backed {
3455            SourceText::source(span)
3456        } else {
3457            SourceText::cooked(span, text[start..end].to_string())
3458        }
3459    }
3460
3461    fn pattern_has_glob_syntax(pattern: &Pattern) -> bool {
3462        pattern.parts.iter().any(|part| match &part.kind {
3463            PatternPart::AnyString | PatternPart::AnyChar | PatternPart::CharClass(_) => true,
3464            PatternPart::Group { .. } => true,
3465            PatternPart::Word(word) => Self::pattern_has_glob_word(word),
3466            PatternPart::Literal(_) => false,
3467        })
3468    }
3469
3470    fn pattern_has_glob_word(word: &Word) -> bool {
3471        word.parts
3472            .iter()
3473            .any(|part| !matches!(part.kind, WordPart::Literal(_)))
3474    }
3475
3476    fn simple_word_from_token(&mut self, token: &LexedToken<'_>, span: Span) -> Option<Word> {
3477        let word = token.word()?;
3478        let source_backed = !token.flags.is_synthetic();
3479
3480        if self.zsh_glob_qualifiers_enabled_at(span.start.offset)
3481            && let Some(segment) = word.single_segment()
3482            && segment.kind() == LexedWordSegmentKind::Plain
3483            && let Some(word) = self.maybe_parse_zsh_qualified_glob_word(
3484                segment.as_str(),
3485                span,
3486                segment.span().is_some() && source_backed && segment.text_is_source_backed(),
3487            )
3488        {
3489            return Some(word);
3490        }
3491        let mut parts = Self::word_part_buffer_with_capacity(word.segments().size_hint().0);
3492
3493        for segment in word.segments() {
3494            let source_backed = segment.span().is_some() && !token.flags.is_synthetic();
3495            let content_span = Self::segment_content_span(segment, span);
3496            let raw_text = segment.as_str();
3497            let use_source_slice = source_backed
3498                && match segment.kind() {
3499                    LexedWordSegmentKind::Plain => {
3500                        segment.text_is_source_backed()
3501                            || raw_text.contains("${") && raw_text.contains('/')
3502                            || !raw_text.contains("$(")
3503                    }
3504                    _ => segment.text_is_source_backed(),
3505                };
3506            let text = if use_source_slice {
3507                content_span.slice(self.input)
3508            } else {
3509                raw_text
3510            };
3511            match segment.kind() {
3512                LexedWordSegmentKind::Plain
3513                | LexedWordSegmentKind::DoubleQuoted
3514                | LexedWordSegmentKind::DollarDoubleQuoted
3515                    if Self::word_text_needs_parse(text) =>
3516                {
3517                    return None;
3518                }
3519                LexedWordSegmentKind::Plain
3520                | LexedWordSegmentKind::SingleQuoted
3521                | LexedWordSegmentKind::DollarSingleQuoted
3522                | LexedWordSegmentKind::DoubleQuoted
3523                | LexedWordSegmentKind::DollarDoubleQuoted => {}
3524                LexedWordSegmentKind::Composite => return None,
3525            }
3526
3527            let wrapper_span = Self::segment_wrapper_span(segment, span);
3528            let part = match segment.kind() {
3529                LexedWordSegmentKind::Plain => {
3530                    self.literal_part_from_text(text, content_span, source_backed)
3531                }
3532                LexedWordSegmentKind::SingleQuoted => {
3533                    self.single_quoted_part_from_text(text, content_span, wrapper_span, false)
3534                }
3535                LexedWordSegmentKind::DollarSingleQuoted => {
3536                    self.single_quoted_part_from_text(text, content_span, wrapper_span, true)
3537                }
3538                LexedWordSegmentKind::DoubleQuoted => self.double_quoted_literal_part_from_text(
3539                    text,
3540                    content_span,
3541                    wrapper_span,
3542                    source_backed,
3543                    false,
3544                ),
3545                LexedWordSegmentKind::DollarDoubleQuoted => self
3546                    .double_quoted_literal_part_from_text(
3547                        text,
3548                        content_span,
3549                        wrapper_span,
3550                        source_backed,
3551                        true,
3552                    ),
3553                LexedWordSegmentKind::Composite => unreachable!(),
3554            };
3555            Self::push_word_part_node(&mut parts, part);
3556        }
3557
3558        Some(self.word_with_part_buffer(parts, span))
3559    }
3560
3561    fn segment_content_span(segment: &LexedWordSegment<'_>, fallback: Span) -> Span {
3562        segment
3563            .span()
3564            .or_else(|| segment.wrapper_span())
3565            .unwrap_or(fallback)
3566    }
3567
3568    fn segment_wrapper_span(segment: &LexedWordSegment<'_>, fallback: Span) -> Span {
3569        segment
3570            .wrapper_span()
3571            .or_else(|| segment.span())
3572            .unwrap_or(fallback)
3573    }
3574
3575    fn literal_part_from_text(&self, text: &str, span: Span, source_backed: bool) -> WordPartNode {
3576        WordPartNode::new(
3577            WordPart::Literal(self.literal_text_from_str(
3578                text,
3579                span.start,
3580                span.end,
3581                source_backed,
3582            )),
3583            span,
3584        )
3585    }
3586
3587    fn single_quoted_part_from_text(
3588        &self,
3589        text: &str,
3590        content_span: Span,
3591        wrapper_span: Span,
3592        dollar: bool,
3593    ) -> WordPartNode {
3594        WordPartNode::new(
3595            WordPart::SingleQuoted {
3596                value: self.source_text_from_str(text, content_span.start, content_span.end),
3597                dollar,
3598            },
3599            wrapper_span,
3600        )
3601    }
3602
3603    fn double_quoted_literal_part_from_text(
3604        &self,
3605        text: &str,
3606        content_span: Span,
3607        wrapper_span: Span,
3608        source_backed: bool,
3609        dollar: bool,
3610    ) -> WordPartNode {
3611        WordPartNode::new(
3612            WordPart::DoubleQuoted {
3613                parts: vec![self.literal_part_from_text(text, content_span, source_backed)],
3614                dollar,
3615            },
3616            wrapper_span,
3617        )
3618    }
3619
3620    fn decode_word_from_token(&mut self, token: &LexedToken<'_>, span: Span) -> Option<Word> {
3621        let word = token.word()?;
3622
3623        if word.single_segment().is_none()
3624            && !token.flags.is_synthetic()
3625            && let Some(source_text) = token.source_slice(self.input)
3626        {
3627            return Some(self.parse_word_with_context(source_text, span, span.start, true));
3628        }
3629
3630        if let Some(segment) = word.single_segment() {
3631            let content_span = Self::segment_content_span(segment, span);
3632            let wrapper_span = Self::segment_wrapper_span(segment, span);
3633            let source_backed = segment.span().is_some() && !token.flags.is_synthetic();
3634            let raw_text = segment.as_str();
3635            let use_source_slice = source_backed
3636                && match segment.kind() {
3637                    LexedWordSegmentKind::Plain => {
3638                        segment.text_is_source_backed()
3639                            || raw_text.contains("${") && raw_text.contains('/')
3640                            || !raw_text.contains("$(")
3641                    }
3642                    _ => segment.text_is_source_backed(),
3643                };
3644            let text = if use_source_slice {
3645                content_span.slice(self.input)
3646            } else {
3647                raw_text
3648            };
3649            let decode_text = if source_backed
3650                && !self.source_matches(content_span, text)
3651                && matches!(
3652                    segment.kind(),
3653                    LexedWordSegmentKind::DoubleQuoted | LexedWordSegmentKind::DollarDoubleQuoted
3654                )
3655                && (!text.contains("$(") || text.contains("$(("))
3656            {
3657                content_span.slice(self.input)
3658            } else {
3659                text
3660            };
3661            let preserve_escaped_expansion_literals =
3662                source_backed && self.source_matches(content_span, decode_text);
3663
3664            return match segment.kind() {
3665                LexedWordSegmentKind::SingleQuoted => Some(self.word_with_single_part(
3666                    self.single_quoted_part_from_text(text, content_span, wrapper_span, false),
3667                    span,
3668                )),
3669                LexedWordSegmentKind::DollarSingleQuoted => Some(self.word_with_single_part(
3670                    self.single_quoted_part_from_text(text, content_span, wrapper_span, true),
3671                    span,
3672                )),
3673                LexedWordSegmentKind::Plain if Self::word_text_needs_parse(text) => Some(
3674                    self.decode_word_text_preserving_quotes_if_needed_with_escape_mode(
3675                        text,
3676                        span,
3677                        content_span.start,
3678                        source_backed,
3679                        preserve_escaped_expansion_literals,
3680                    ),
3681                ),
3682                LexedWordSegmentKind::DoubleQuoted | LexedWordSegmentKind::DollarDoubleQuoted
3683                    if Self::word_text_needs_parse(text) =>
3684                {
3685                    let inner = self.decode_quoted_segment_text(
3686                        decode_text,
3687                        content_span,
3688                        content_span.start,
3689                        source_backed,
3690                    );
3691                    Some(self.word_with_single_part(
3692                        WordPartNode::new(
3693                            WordPart::DoubleQuoted {
3694                                parts: inner.parts,
3695                                dollar: matches!(
3696                                    segment.kind(),
3697                                    LexedWordSegmentKind::DollarDoubleQuoted
3698                                ),
3699                            },
3700                            wrapper_span,
3701                        ),
3702                        span,
3703                    ))
3704                }
3705                LexedWordSegmentKind::Plain => Some(self.word_with_single_part(
3706                    self.literal_part_from_text(text, content_span, source_backed),
3707                    span,
3708                )),
3709                LexedWordSegmentKind::DoubleQuoted => Some(self.word_with_single_part(
3710                    self.double_quoted_literal_part_from_text(
3711                        text,
3712                        content_span,
3713                        wrapper_span,
3714                        source_backed,
3715                        false,
3716                    ),
3717                    span,
3718                )),
3719                LexedWordSegmentKind::DollarDoubleQuoted => Some(self.word_with_single_part(
3720                    self.double_quoted_literal_part_from_text(
3721                        text,
3722                        content_span,
3723                        wrapper_span,
3724                        source_backed,
3725                        true,
3726                    ),
3727                    span,
3728                )),
3729                LexedWordSegmentKind::Composite => None,
3730            };
3731        }
3732
3733        let mut parts = Self::word_part_buffer_with_capacity(word.segments().size_hint().0);
3734        let mut cursor = span.start;
3735
3736        for segment in word.segments() {
3737            let raw_text = segment.as_str();
3738            let content_span = if let Some(segment_span) = segment.span() {
3739                cursor = segment_span.end;
3740                segment_span
3741            } else {
3742                let start = cursor;
3743                let end = start.advanced_by(raw_text);
3744                cursor = end;
3745                Span::from_positions(start, end)
3746            };
3747            let wrapper_span = segment.wrapper_span().unwrap_or(content_span);
3748            let source_backed = segment.span().is_some() && !token.flags.is_synthetic();
3749            let use_source_slice = source_backed
3750                && match segment.kind() {
3751                    LexedWordSegmentKind::Plain => {
3752                        segment.text_is_source_backed()
3753                            || raw_text.contains("${") && raw_text.contains('/')
3754                            || !raw_text.contains("$(")
3755                    }
3756                    _ => segment.text_is_source_backed(),
3757                };
3758            let text = if use_source_slice {
3759                content_span.slice(self.input)
3760            } else {
3761                raw_text
3762            };
3763            let preserve_escaped_expansion_literals = source_backed;
3764
3765            match segment.kind() {
3766                LexedWordSegmentKind::SingleQuoted => Self::push_word_part_node(
3767                    &mut parts,
3768                    self.single_quoted_part_from_text(text, content_span, wrapper_span, false),
3769                ),
3770                LexedWordSegmentKind::DollarSingleQuoted => Self::push_word_part_node(
3771                    &mut parts,
3772                    self.single_quoted_part_from_text(text, content_span, wrapper_span, true),
3773                ),
3774                LexedWordSegmentKind::Plain => {
3775                    if Self::word_text_needs_parse(text) {
3776                        parts.extend(
3777                            self.decode_word_text_preserving_quotes_if_needed_with_escape_mode(
3778                                text,
3779                                content_span,
3780                                content_span.start,
3781                                source_backed,
3782                                preserve_escaped_expansion_literals,
3783                            )
3784                            .parts,
3785                        );
3786                    } else {
3787                        Self::push_word_part_node(
3788                            &mut parts,
3789                            self.literal_part_from_text(text, content_span, source_backed),
3790                        );
3791                    }
3792                }
3793                LexedWordSegmentKind::DoubleQuoted | LexedWordSegmentKind::DollarDoubleQuoted => {
3794                    if Self::word_text_needs_parse(text) {
3795                        let inner = self.decode_quoted_segment_text(
3796                            text,
3797                            content_span,
3798                            content_span.start,
3799                            source_backed,
3800                        );
3801                        Self::push_word_part_node(
3802                            &mut parts,
3803                            WordPartNode::new(
3804                                WordPart::DoubleQuoted {
3805                                    parts: inner.parts,
3806                                    dollar: matches!(
3807                                        segment.kind(),
3808                                        LexedWordSegmentKind::DollarDoubleQuoted
3809                                    ),
3810                                },
3811                                wrapper_span,
3812                            ),
3813                        );
3814                    } else {
3815                        Self::push_word_part_node(
3816                            &mut parts,
3817                            self.double_quoted_literal_part_from_text(
3818                                text,
3819                                content_span,
3820                                wrapper_span,
3821                                source_backed,
3822                                matches!(segment.kind(), LexedWordSegmentKind::DollarDoubleQuoted),
3823                            ),
3824                        );
3825                    }
3826                }
3827                LexedWordSegmentKind::Composite => return None,
3828            }
3829        }
3830
3831        Some(self.word_with_part_buffer(parts, span))
3832    }
3833
3834    fn current_word_ref(&mut self) -> Option<&Word> {
3835        if self.current_word_cache.is_none() {
3836            self.current_word_cache = self.current_word();
3837        }
3838
3839        self.current_word_cache.as_ref()
3840    }
3841
3842    fn current_word(&mut self) -> Option<Word> {
3843        if let Some(word) = self.current_word_cache.as_ref() {
3844            return Some(word.clone());
3845        }
3846
3847        if let Some(word) = self.current_zsh_glob_word_from_source() {
3848            self.current_word_cache = Some(word.clone());
3849            return Some(word);
3850        }
3851
3852        let span = self.current_span;
3853        if let Some(token) = self.current_token.clone()
3854            && let Some(word) = self.simple_word_from_token(&token, span)
3855        {
3856            return Some(word);
3857        }
3858
3859        let token = self.current_token.take()?;
3860        let word = self.decode_word_from_token(&token, span);
3861        self.current_token = Some(token);
3862        if let Some(word) = word.as_ref() {
3863            self.current_word_cache = Some(word.clone());
3864        }
3865        word
3866    }
3867
3868    fn take_current_word(&mut self) -> Option<Word> {
3869        if let Some(word) = self.current_word_cache.take() {
3870            return Some(word);
3871        }
3872
3873        if let Some(word) = self.current_zsh_glob_word_from_source() {
3874            return Some(word);
3875        }
3876
3877        let span = self.current_span;
3878        if let Some(token) = self.current_token.clone()
3879            && let Some(word) = self.simple_word_from_token(&token, span)
3880        {
3881            return Some(word);
3882        }
3883
3884        let token = self.current_token.take()?;
3885        let word = self.decode_word_from_token(&token, span);
3886        self.current_token = Some(token);
3887        word
3888    }
3889
3890    fn take_current_word_and_advance(&mut self) -> Option<Word> {
3891        let word = self.take_current_word()?;
3892        self.advance_past_word(&word);
3893        Some(word)
3894    }
3895
3896    fn current_zsh_glob_word_from_source(&mut self) -> Option<Word> {
3897        if !matches!(
3898            self.current_token_kind,
3899            Some(TokenKind::LeftParen | TokenKind::Word)
3900        ) {
3901            return None;
3902        }
3903
3904        let start = self.current_span.start;
3905        if !self.source_word_contains_zsh_glob_control(start) {
3906            return None;
3907        }
3908        let (text, end) = self.scan_source_word(start)?;
3909        if !text.contains("(#") {
3910            return None;
3911        }
3912        let span = Span::from_positions(start, end);
3913        if self.zsh_glob_qualifiers_enabled_at(span.start.offset)
3914            && let Some(word) = self.maybe_parse_zsh_qualified_glob_word(&text, span, true)
3915        {
3916            return Some(word);
3917        }
3918
3919        Some(self.parse_word_with_context(&text, span, start, true))
3920    }
3921
3922    fn source_word_contains_zsh_glob_control(&self, start: Position) -> bool {
3923        if start.offset >= self.input.len() {
3924            return false;
3925        }
3926
3927        let source = &self.input[start.offset..];
3928        let mut chars = source.chars().peekable();
3929        let mut cursor = start;
3930        let mut paren_depth = 0_i32;
3931        let mut brace_depth = 0_i32;
3932        let mut in_single = false;
3933        let mut in_double = false;
3934        let mut in_backtick = false;
3935        let mut escaped = false;
3936        let mut prev_char = None;
3937
3938        while let Some(&ch) = chars.peek() {
3939            if !in_single
3940                && !in_double
3941                && !in_backtick
3942                && paren_depth == 0
3943                && brace_depth == 0
3944                && matches!(ch, ' ' | '\t' | '\n' | ';' | '|' | '&' | '>' | '<' | ')')
3945            {
3946                break;
3947            }
3948
3949            let ch = Self::next_word_char_unwrap(&mut chars, &mut cursor);
3950            if !in_single && !in_double && !in_backtick && prev_char == Some('(') && ch == '#' {
3951                return true;
3952            }
3953
3954            if escaped {
3955                escaped = false;
3956                prev_char = Some(ch);
3957                continue;
3958            }
3959
3960            match ch {
3961                '\\' if !in_single => escaped = true,
3962                '\'' if !in_double => in_single = !in_single,
3963                '"' if !in_single => in_double = !in_double,
3964                '`' if !in_single => in_backtick = !in_backtick,
3965                '(' if !in_single && !in_double => paren_depth += 1,
3966                ')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
3967                '{' if !in_single && !in_double => brace_depth += 1,
3968                '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
3969                _ => {}
3970            }
3971
3972            prev_char = Some(ch);
3973        }
3974
3975        false
3976    }
3977
3978    fn scan_source_word(&self, start: Position) -> Option<(String, Position)> {
3979        if start.offset >= self.input.len() {
3980            return None;
3981        }
3982
3983        let source = &self.input[start.offset..];
3984        let mut chars = source.chars().peekable();
3985        let mut cursor = start;
3986        let mut text = String::new();
3987        let mut paren_depth = 0_i32;
3988        let mut brace_depth = 0_i32;
3989        let mut in_single = false;
3990        let mut in_double = false;
3991        let mut in_backtick = false;
3992        let mut escaped = false;
3993
3994        while let Some(&ch) = chars.peek() {
3995            if !in_single
3996                && !in_double
3997                && !in_backtick
3998                && paren_depth == 0
3999                && brace_depth == 0
4000                && matches!(ch, ' ' | '\t' | '\n' | ';' | '|' | '&' | '>' | '<' | ')')
4001            {
4002                break;
4003            }
4004
4005            let ch = Self::next_word_char_unwrap(&mut chars, &mut cursor);
4006            text.push(ch);
4007
4008            if escaped {
4009                escaped = false;
4010                continue;
4011            }
4012
4013            match ch {
4014                '\\' if !in_single => escaped = true,
4015                '\'' if !in_double => in_single = !in_single,
4016                '"' if !in_single => in_double = !in_double,
4017                '`' if !in_single => in_backtick = !in_backtick,
4018                '(' if !in_single && !in_double => paren_depth += 1,
4019                ')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
4020                '{' if !in_single && !in_double => brace_depth += 1,
4021                '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
4022                _ => {}
4023            }
4024        }
4025
4026        (!text.is_empty()).then_some((text, cursor))
4027    }
4028
4029    fn advance_past_word(&mut self, word: &Word) {
4030        let stop_after_synthetic = self
4031            .current_token
4032            .as_ref()
4033            .is_some_and(|token| token.flags.is_synthetic());
4034        while self.current_token.is_some() && self.current_span.start.offset < word.span.end.offset
4035        {
4036            self.advance();
4037            if stop_after_synthetic
4038                && self
4039                    .current_token
4040                    .as_ref()
4041                    .is_none_or(|token| !token.flags.is_synthetic())
4042            {
4043                break;
4044            }
4045        }
4046    }
4047
4048    fn token_source_like_word_text(&self, token: &LexedToken<'a>) -> Option<Cow<'a, str>> {
4049        token
4050            .source_slice(self.input)
4051            .map(Cow::Borrowed)
4052            .or_else(|| {
4053                (token.span.start.offset <= token.span.end.offset
4054                    && token.span.end.offset <= self.input.len())
4055                .then(|| Cow::Borrowed(&self.input[token.span.start.offset..token.span.end.offset]))
4056            })
4057            .or_else(|| token.word_string().map(Cow::Owned))
4058    }
4059
4060    fn current_source_like_word_text(&self) -> Option<Cow<'a, str>> {
4061        self.current_token_kind
4062            .filter(|kind| kind.is_word_like())
4063            .and(self.current_token.as_ref())
4064            .and_then(|token| self.token_source_like_word_text(token))
4065    }
4066
4067    fn current_source_like_word_text_or_error(
4068        &self,
4069        context: &'static str,
4070    ) -> Result<Cow<'a, str>> {
4071        self.current_source_like_word_text().ok_or_else(|| {
4072            self.error(format!(
4073                "internal parser error: missing source text for {context}"
4074            ))
4075        })
4076    }
4077
4078    fn keyword_from_token(token: &LexedToken<'_>) -> Option<Keyword> {
4079        (token.kind == TokenKind::Word)
4080            .then(|| token.word_text())
4081            .flatten()
4082            .and_then(Self::classify_keyword)
4083    }
4084
4085    fn current_conditional_literal_word(&self) -> Option<Word> {
4086        match self.current_token_kind? {
4087            TokenKind::LeftBrace | TokenKind::RightBrace => Some(Word::literal_with_span(
4088                self.input[self.current_span.start.offset..self.current_span.end.offset]
4089                    .to_string(),
4090                self.current_span,
4091            )),
4092            _ => None,
4093        }
4094    }
4095
4096    fn current_name_token(&self) -> Option<(Name, Span)> {
4097        self.current_token_kind
4098            .filter(|kind| kind.is_word_like())
4099            .and_then(|_| self.current_word_str())
4100            .map(|word| (Name::from(word), self.current_span))
4101    }
4102
4103    fn current_static_token_text(&self) -> Option<(String, bool)> {
4104        let token = self.current_token.as_ref()?;
4105        let raw_text = token.word_string()?;
4106        let text_had_escape_markers = raw_text.contains('\x00');
4107        let text = if text_had_escape_markers {
4108            raw_text.replace('\x00', "")
4109        } else {
4110            raw_text
4111        };
4112
4113        match token.kind {
4114            TokenKind::LiteralWord => Some((text, true)),
4115            TokenKind::QuotedWord if !Self::word_text_needs_parse(&text) => Some((text, true)),
4116            TokenKind::Word if !Self::word_text_needs_parse(&text) => Some((
4117                text,
4118                token.flags.has_cooked_text() || text_had_escape_markers,
4119            )),
4120            _ => None,
4121        }
4122    }
4123
4124    fn nested_stmt_seq_from_source(&mut self, source: &str, base: Position) -> StmtSeq {
4125        let remaining_depth = self.max_depth.saturating_sub(self.current_depth);
4126        let nested_profile = self
4127            .current_zsh_options()
4128            .cloned()
4129            .map(|options| ShellProfile::with_zsh_options(self.dialect, options))
4130            .unwrap_or_else(|| self.shell_profile.clone());
4131        let inner_parser =
4132            Parser::with_limits_and_profile(source, remaining_depth, self.fuel, nested_profile);
4133        let mut output = inner_parser.parse();
4134        if output.is_ok() {
4135            Self::materialize_stmt_seq_source_backing(&mut output.file.body, source);
4136            Self::rebase_file(&mut output.file, base);
4137            output.file.body
4138        } else {
4139            StmtSeq {
4140                leading_comments: Vec::new(),
4141                stmts: Vec::new(),
4142                trailing_comments: Vec::new(),
4143                span: Span::from_positions(base, base),
4144            }
4145        }
4146    }
4147
4148    fn nested_stmt_seq_from_current_input(&mut self, start: Position, end: Position) -> StmtSeq {
4149        if start.offset > end.offset || end.offset > self.input.len() {
4150            return StmtSeq {
4151                leading_comments: Vec::new(),
4152                stmts: Vec::new(),
4153                trailing_comments: Vec::new(),
4154                span: Span::from_positions(start, start),
4155            };
4156        }
4157        let source = &self.input[start.offset..end.offset];
4158        self.nested_stmt_seq_from_source(source, start)
4159    }
4160
4161    fn merge_optional_span(primary: Span, other: Span) -> Span {
4162        if other == Span::new() {
4163            primary
4164        } else {
4165            primary.merge(other)
4166        }
4167    }
4168
4169    fn redirect_span(operator_span: Span, target: &Word) -> Span {
4170        Self::merge_optional_span(operator_span, target.span)
4171    }
4172
4173    fn optional_span(start: Position, end: Position) -> Option<Span> {
4174        (start.offset < end.offset).then(|| Span::from_positions(start, end))
4175    }
4176
4177    fn split_nested_arithmetic_close(&mut self, context: &'static str) -> Result<Span> {
4178        let right_paren_start = self.current_span.start.advanced_by(")");
4179        self.advance();
4180
4181        if self.at(TokenKind::RightParen) {
4182            let right_paren_span = Span::from_positions(right_paren_start, self.current_span.end);
4183            self.advance();
4184            Ok(right_paren_span)
4185        } else {
4186            Err(Error::parse(format!(
4187                "expected ')' after '))' in {context}"
4188            )))
4189        }
4190    }
4191
4192    fn split_double_semicolon(span: Span) -> (Span, Span) {
4193        let middle = span.start.advanced_by(";");
4194        (
4195            Span::from_positions(span.start, middle),
4196            Span::from_positions(middle, span.end),
4197        )
4198    }
4199
4200    fn split_double_left_paren(span: Span) -> (Span, Span) {
4201        let middle = span.start.advanced_by("(");
4202        (
4203            Span::from_positions(span.start, middle),
4204            Span::from_positions(middle, span.end),
4205        )
4206    }
4207
4208    fn split_double_right_paren(span: Span) -> (Span, Span) {
4209        let middle = span.start.advanced_by(")");
4210        (
4211            Span::from_positions(span.start, middle),
4212            Span::from_positions(middle, span.end),
4213        )
4214    }
4215
4216    fn record_arithmetic_for_separator(
4217        semicolon_span: Span,
4218        segment_start: &mut Position,
4219        init_span: &mut Option<Span>,
4220        first_semicolon_span: &mut Option<Span>,
4221        condition_span: &mut Option<Span>,
4222        second_semicolon_span: &mut Option<Span>,
4223    ) -> Result<()> {
4224        if first_semicolon_span.is_none() {
4225            *init_span = Self::optional_span(*segment_start, semicolon_span.start);
4226            *first_semicolon_span = Some(semicolon_span);
4227            *segment_start = semicolon_span.end;
4228            return Ok(());
4229        }
4230
4231        if second_semicolon_span.is_none() {
4232            *condition_span = Self::optional_span(*segment_start, semicolon_span.start);
4233            *second_semicolon_span = Some(semicolon_span);
4234            *segment_start = semicolon_span.end;
4235            return Ok(());
4236        }
4237
4238        Err(Error::parse(
4239            "unexpected ';' in arithmetic for header".to_string(),
4240        ))
4241    }
4242
4243    fn rebase_file(file: &mut File, base: Position) {
4244        file.span = file.span.rebased(base);
4245        Self::rebase_stmt_seq(&mut file.body, base);
4246    }
4247
4248    fn rebase_comments(comments: &mut [Comment], base: Position) {
4249        let base_offset = TextSize::new(base.offset as u32);
4250        for comment in comments {
4251            comment.range = comment.range.offset_by(base_offset);
4252        }
4253    }
4254
4255    fn rebase_stmt_seq(sequence: &mut StmtSeq, base: Position) {
4256        sequence.span = sequence.span.rebased(base);
4257        Self::rebase_comments(&mut sequence.leading_comments, base);
4258        for stmt in &mut sequence.stmts {
4259            Self::rebase_stmt(stmt, base);
4260        }
4261        Self::rebase_comments(&mut sequence.trailing_comments, base);
4262    }
4263
4264    fn rebase_stmt(stmt: &mut Stmt, base: Position) {
4265        stmt.span = stmt.span.rebased(base);
4266        Self::rebase_comments(&mut stmt.leading_comments, base);
4267        stmt.terminator_span = stmt.terminator_span.map(|span| span.rebased(base));
4268        if let Some(comment) = &mut stmt.inline_comment {
4269            let base_offset = TextSize::new(base.offset as u32);
4270            comment.range = comment.range.offset_by(base_offset);
4271        }
4272        Self::rebase_redirects(&mut stmt.redirects, base);
4273        Self::rebase_ast_command(&mut stmt.command, base);
4274    }
4275
4276    fn rebase_ast_command(command: &mut AstCommand, base: Position) {
4277        match command {
4278            AstCommand::Simple(simple) => {
4279                simple.span = simple.span.rebased(base);
4280                Self::rebase_word(&mut simple.name, base);
4281                Self::rebase_words(&mut simple.args, base);
4282                Self::rebase_assignments(&mut simple.assignments, base);
4283            }
4284            AstCommand::Builtin(builtin) => match builtin {
4285                AstBuiltinCommand::Break(command) => {
4286                    command.span = command.span.rebased(base);
4287                    if let Some(depth) = &mut command.depth {
4288                        Self::rebase_word(depth, base);
4289                    }
4290                    Self::rebase_words(&mut command.extra_args, base);
4291                    Self::rebase_assignments(&mut command.assignments, base);
4292                }
4293                AstBuiltinCommand::Continue(command) => {
4294                    command.span = command.span.rebased(base);
4295                    if let Some(depth) = &mut command.depth {
4296                        Self::rebase_word(depth, base);
4297                    }
4298                    Self::rebase_words(&mut command.extra_args, base);
4299                    Self::rebase_assignments(&mut command.assignments, base);
4300                }
4301                AstBuiltinCommand::Return(command) => {
4302                    command.span = command.span.rebased(base);
4303                    if let Some(code) = &mut command.code {
4304                        Self::rebase_word(code, base);
4305                    }
4306                    Self::rebase_words(&mut command.extra_args, base);
4307                    Self::rebase_assignments(&mut command.assignments, base);
4308                }
4309                AstBuiltinCommand::Exit(command) => {
4310                    command.span = command.span.rebased(base);
4311                    if let Some(code) = &mut command.code {
4312                        Self::rebase_word(code, base);
4313                    }
4314                    Self::rebase_words(&mut command.extra_args, base);
4315                    Self::rebase_assignments(&mut command.assignments, base);
4316                }
4317            },
4318            AstCommand::Decl(decl) => {
4319                decl.span = decl.span.rebased(base);
4320                decl.variant_span = decl.variant_span.rebased(base);
4321                Self::rebase_assignments(&mut decl.assignments, base);
4322                for operand in &mut decl.operands {
4323                    match operand {
4324                        DeclOperand::Flag(word) | DeclOperand::Dynamic(word) => {
4325                            Self::rebase_word(word, base);
4326                        }
4327                        DeclOperand::Name(name) => Self::rebase_var_ref(name, base),
4328                        DeclOperand::Assignment(assignment) => {
4329                            Self::rebase_assignments(std::slice::from_mut(assignment), base);
4330                        }
4331                    }
4332                }
4333            }
4334            AstCommand::Binary(binary) => {
4335                binary.span = binary.span.rebased(base);
4336                binary.op_span = binary.op_span.rebased(base);
4337                Self::rebase_stmt(binary.left.as_mut(), base);
4338                Self::rebase_stmt(binary.right.as_mut(), base);
4339            }
4340            AstCommand::Compound(compound) => Self::rebase_compound(compound, base),
4341            AstCommand::Function(function) => {
4342                function.span = function.span.rebased(base);
4343                if let Some(span) = &mut function.header.function_keyword_span {
4344                    *span = span.rebased(base);
4345                }
4346                if let Some(span) = &mut function.header.trailing_parens_span {
4347                    *span = span.rebased(base);
4348                }
4349                for entry in &mut function.header.entries {
4350                    Self::rebase_word(&mut entry.word, base);
4351                }
4352                Self::rebase_stmt(function.body.as_mut(), base);
4353            }
4354            AstCommand::AnonymousFunction(function) => {
4355                function.span = function.span.rebased(base);
4356                function.surface = match function.surface {
4357                    AnonymousFunctionSurface::FunctionKeyword {
4358                        function_keyword_span,
4359                    } => AnonymousFunctionSurface::FunctionKeyword {
4360                        function_keyword_span: function_keyword_span.rebased(base),
4361                    },
4362                    AnonymousFunctionSurface::Parens { parens_span } => {
4363                        AnonymousFunctionSurface::Parens {
4364                            parens_span: parens_span.rebased(base),
4365                        }
4366                    }
4367                };
4368                Self::rebase_stmt(function.body.as_mut(), base);
4369                Self::rebase_words(&mut function.args, base);
4370            }
4371        }
4372    }
4373
4374    fn rebase_subscript(subscript: &mut Subscript, base: Position) {
4375        subscript.text.rebased(base);
4376        if let Some(raw) = &mut subscript.raw {
4377            raw.rebased(base);
4378        }
4379        if let Some(word) = &mut subscript.word_ast {
4380            Self::rebase_word(word, base);
4381        }
4382        if let Some(expr) = &mut subscript.arithmetic_ast {
4383            Self::rebase_arithmetic_expr(expr, base);
4384        }
4385    }
4386
4387    fn rebase_var_ref(reference: &mut VarRef, base: Position) {
4388        reference.span = reference.span.rebased(base);
4389        reference.name_span = reference.name_span.rebased(base);
4390        if let Some(subscript) = &mut reference.subscript {
4391            Self::rebase_subscript(subscript, base);
4392        }
4393    }
4394
4395    fn rebase_array_expr(array: &mut ArrayExpr, base: Position) {
4396        array.span = array.span.rebased(base);
4397        for element in &mut array.elements {
4398            match element {
4399                ArrayElem::Sequential(word) => Self::rebase_word(word, base),
4400                ArrayElem::Keyed { key, value } | ArrayElem::KeyedAppend { key, value } => {
4401                    Self::rebase_subscript(key, base);
4402                    Self::rebase_word(value, base);
4403                }
4404            }
4405        }
4406    }
4407
4408    fn rebase_compound(compound: &mut CompoundCommand, base: Position) {
4409        match compound {
4410            CompoundCommand::If(command) => {
4411                command.span = command.span.rebased(base);
4412                command.syntax = match command.syntax {
4413                    IfSyntax::ThenFi { then_span, fi_span } => IfSyntax::ThenFi {
4414                        then_span: then_span.rebased(base),
4415                        fi_span: fi_span.rebased(base),
4416                    },
4417                    IfSyntax::Brace {
4418                        left_brace_span,
4419                        right_brace_span,
4420                    } => IfSyntax::Brace {
4421                        left_brace_span: left_brace_span.rebased(base),
4422                        right_brace_span: right_brace_span.rebased(base),
4423                    },
4424                };
4425                Self::rebase_stmt_seq(&mut command.condition, base);
4426                Self::rebase_stmt_seq(&mut command.then_branch, base);
4427                for (condition, body) in &mut command.elif_branches {
4428                    Self::rebase_stmt_seq(condition, base);
4429                    Self::rebase_stmt_seq(body, base);
4430                }
4431                if let Some(else_branch) = &mut command.else_branch {
4432                    Self::rebase_stmt_seq(else_branch, base);
4433                }
4434            }
4435            CompoundCommand::For(command) => {
4436                command.span = command.span.rebased(base);
4437                for target in &mut command.targets {
4438                    target.span = target.span.rebased(base);
4439                }
4440                if let Some(words) = &mut command.words {
4441                    Self::rebase_words(words, base);
4442                }
4443                command.syntax = match command.syntax {
4444                    ForSyntax::InDoDone {
4445                        in_span,
4446                        do_span,
4447                        done_span,
4448                    } => ForSyntax::InDoDone {
4449                        in_span: in_span.map(|span| span.rebased(base)),
4450                        do_span: do_span.rebased(base),
4451                        done_span: done_span.rebased(base),
4452                    },
4453                    ForSyntax::InDirect { in_span } => ForSyntax::InDirect {
4454                        in_span: in_span.map(|span| span.rebased(base)),
4455                    },
4456                    ForSyntax::InBrace {
4457                        in_span,
4458                        left_brace_span,
4459                        right_brace_span,
4460                    } => ForSyntax::InBrace {
4461                        in_span: in_span.map(|span| span.rebased(base)),
4462                        left_brace_span: left_brace_span.rebased(base),
4463                        right_brace_span: right_brace_span.rebased(base),
4464                    },
4465                    ForSyntax::ParenDoDone {
4466                        left_paren_span,
4467                        right_paren_span,
4468                        do_span,
4469                        done_span,
4470                    } => ForSyntax::ParenDoDone {
4471                        left_paren_span: left_paren_span.rebased(base),
4472                        right_paren_span: right_paren_span.rebased(base),
4473                        do_span: do_span.rebased(base),
4474                        done_span: done_span.rebased(base),
4475                    },
4476                    ForSyntax::ParenDirect {
4477                        left_paren_span,
4478                        right_paren_span,
4479                    } => ForSyntax::ParenDirect {
4480                        left_paren_span: left_paren_span.rebased(base),
4481                        right_paren_span: right_paren_span.rebased(base),
4482                    },
4483                    ForSyntax::ParenBrace {
4484                        left_paren_span,
4485                        right_paren_span,
4486                        left_brace_span,
4487                        right_brace_span,
4488                    } => ForSyntax::ParenBrace {
4489                        left_paren_span: left_paren_span.rebased(base),
4490                        right_paren_span: right_paren_span.rebased(base),
4491                        left_brace_span: left_brace_span.rebased(base),
4492                        right_brace_span: right_brace_span.rebased(base),
4493                    },
4494                };
4495                Self::rebase_stmt_seq(&mut command.body, base);
4496            }
4497            CompoundCommand::Repeat(command) => {
4498                command.span = command.span.rebased(base);
4499                Self::rebase_word(&mut command.count, base);
4500                command.syntax = match command.syntax {
4501                    RepeatSyntax::DoDone { do_span, done_span } => RepeatSyntax::DoDone {
4502                        do_span: do_span.rebased(base),
4503                        done_span: done_span.rebased(base),
4504                    },
4505                    RepeatSyntax::Direct => RepeatSyntax::Direct,
4506                    RepeatSyntax::Brace {
4507                        left_brace_span,
4508                        right_brace_span,
4509                    } => RepeatSyntax::Brace {
4510                        left_brace_span: left_brace_span.rebased(base),
4511                        right_brace_span: right_brace_span.rebased(base),
4512                    },
4513                };
4514                Self::rebase_stmt_seq(&mut command.body, base);
4515            }
4516            CompoundCommand::Foreach(command) => {
4517                command.span = command.span.rebased(base);
4518                command.variable_span = command.variable_span.rebased(base);
4519                Self::rebase_words(&mut command.words, base);
4520                command.syntax = match command.syntax {
4521                    ForeachSyntax::ParenBrace {
4522                        left_paren_span,
4523                        right_paren_span,
4524                        left_brace_span,
4525                        right_brace_span,
4526                    } => ForeachSyntax::ParenBrace {
4527                        left_paren_span: left_paren_span.rebased(base),
4528                        right_paren_span: right_paren_span.rebased(base),
4529                        left_brace_span: left_brace_span.rebased(base),
4530                        right_brace_span: right_brace_span.rebased(base),
4531                    },
4532                    ForeachSyntax::InDoDone {
4533                        in_span,
4534                        do_span,
4535                        done_span,
4536                    } => ForeachSyntax::InDoDone {
4537                        in_span: in_span.rebased(base),
4538                        do_span: do_span.rebased(base),
4539                        done_span: done_span.rebased(base),
4540                    },
4541                };
4542                Self::rebase_stmt_seq(&mut command.body, base);
4543            }
4544            CompoundCommand::ArithmeticFor(command) => {
4545                command.span = command.span.rebased(base);
4546                command.left_paren_span = command.left_paren_span.rebased(base);
4547                command.init_span = command.init_span.map(|span| span.rebased(base));
4548                if let Some(expr) = &mut command.init_ast {
4549                    Self::rebase_arithmetic_expr(expr, base);
4550                }
4551                command.first_semicolon_span = command.first_semicolon_span.rebased(base);
4552                command.condition_span = command.condition_span.map(|span| span.rebased(base));
4553                if let Some(expr) = &mut command.condition_ast {
4554                    Self::rebase_arithmetic_expr(expr, base);
4555                }
4556                command.second_semicolon_span = command.second_semicolon_span.rebased(base);
4557                command.step_span = command.step_span.map(|span| span.rebased(base));
4558                if let Some(expr) = &mut command.step_ast {
4559                    Self::rebase_arithmetic_expr(expr, base);
4560                }
4561                command.right_paren_span = command.right_paren_span.rebased(base);
4562                Self::rebase_stmt_seq(&mut command.body, base);
4563            }
4564            CompoundCommand::While(command) => {
4565                command.span = command.span.rebased(base);
4566                Self::rebase_stmt_seq(&mut command.condition, base);
4567                Self::rebase_stmt_seq(&mut command.body, base);
4568            }
4569            CompoundCommand::Until(command) => {
4570                command.span = command.span.rebased(base);
4571                Self::rebase_stmt_seq(&mut command.condition, base);
4572                Self::rebase_stmt_seq(&mut command.body, base);
4573            }
4574            CompoundCommand::Case(command) => {
4575                command.span = command.span.rebased(base);
4576                Self::rebase_word(&mut command.word, base);
4577                for case in &mut command.cases {
4578                    Self::rebase_patterns(&mut case.patterns, base);
4579                    Self::rebase_stmt_seq(&mut case.body, base);
4580                }
4581            }
4582            CompoundCommand::Select(command) => {
4583                command.span = command.span.rebased(base);
4584                command.variable_span = command.variable_span.rebased(base);
4585                Self::rebase_words(&mut command.words, base);
4586                Self::rebase_stmt_seq(&mut command.body, base);
4587            }
4588            CompoundCommand::Subshell(commands) | CompoundCommand::BraceGroup(commands) => {
4589                Self::rebase_stmt_seq(commands, base);
4590            }
4591            CompoundCommand::Arithmetic(command) => {
4592                command.span = command.span.rebased(base);
4593                command.left_paren_span = command.left_paren_span.rebased(base);
4594                command.expr_span = command.expr_span.map(|span| span.rebased(base));
4595                if let Some(expr) = &mut command.expr_ast {
4596                    Self::rebase_arithmetic_expr(expr, base);
4597                }
4598                command.right_paren_span = command.right_paren_span.rebased(base);
4599            }
4600            CompoundCommand::Time(command) => {
4601                command.span = command.span.rebased(base);
4602                if let Some(inner) = &mut command.command {
4603                    Self::rebase_stmt(inner.as_mut(), base);
4604                }
4605            }
4606            CompoundCommand::Conditional(command) => {
4607                command.span = command.span.rebased(base);
4608                command.left_bracket_span = command.left_bracket_span.rebased(base);
4609                command.right_bracket_span = command.right_bracket_span.rebased(base);
4610                Self::rebase_conditional_expr(&mut command.expression, base);
4611            }
4612            CompoundCommand::Coproc(command) => {
4613                command.span = command.span.rebased(base);
4614                command.name_span = command.name_span.map(|span| span.rebased(base));
4615                Self::rebase_stmt(command.body.as_mut(), base);
4616            }
4617            CompoundCommand::Always(command) => {
4618                command.span = command.span.rebased(base);
4619                Self::rebase_stmt_seq(&mut command.body, base);
4620                Self::rebase_stmt_seq(&mut command.always_body, base);
4621            }
4622        }
4623    }
4624
4625    fn materialize_stmt_seq_source_backing(sequence: &mut StmtSeq, source: &str) {
4626        for stmt in &mut sequence.stmts {
4627            Self::materialize_stmt_source_backing(stmt, source);
4628        }
4629    }
4630
4631    fn materialize_stmt_source_backing(stmt: &mut Stmt, source: &str) {
4632        Self::materialize_ast_command_source_backing(&mut stmt.command, source);
4633    }
4634
4635    fn materialize_ast_command_source_backing(command: &mut AstCommand, source: &str) {
4636        match command {
4637            AstCommand::Simple(simple) => {
4638                Self::materialize_word_source_backing(&mut simple.name, source);
4639            }
4640            AstCommand::Builtin(_) | AstCommand::Decl(_) => {}
4641            AstCommand::Binary(binary) => {
4642                Self::materialize_stmt_source_backing(binary.left.as_mut(), source);
4643                Self::materialize_stmt_source_backing(binary.right.as_mut(), source);
4644            }
4645            AstCommand::Compound(compound) => {
4646                Self::materialize_compound_source_backing(compound, source);
4647            }
4648            AstCommand::Function(function) => {
4649                Self::materialize_stmt_source_backing(function.body.as_mut(), source);
4650            }
4651            AstCommand::AnonymousFunction(function) => {
4652                Self::materialize_stmt_source_backing(function.body.as_mut(), source);
4653            }
4654        }
4655    }
4656
4657    fn materialize_compound_source_backing(compound: &mut CompoundCommand, source: &str) {
4658        match compound {
4659            CompoundCommand::If(command) => {
4660                Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
4661                Self::materialize_stmt_seq_source_backing(&mut command.then_branch, source);
4662                for (condition, body) in &mut command.elif_branches {
4663                    Self::materialize_stmt_seq_source_backing(condition, source);
4664                    Self::materialize_stmt_seq_source_backing(body, source);
4665                }
4666                if let Some(else_branch) = &mut command.else_branch {
4667                    Self::materialize_stmt_seq_source_backing(else_branch, source);
4668                }
4669            }
4670            CompoundCommand::For(command) => {
4671                Self::materialize_stmt_seq_source_backing(&mut command.body, source)
4672            }
4673            CompoundCommand::Repeat(command) => {
4674                Self::materialize_stmt_seq_source_backing(&mut command.body, source)
4675            }
4676            CompoundCommand::Foreach(command) => {
4677                Self::materialize_stmt_seq_source_backing(&mut command.body, source)
4678            }
4679            CompoundCommand::ArithmeticFor(command) => {
4680                Self::materialize_stmt_seq_source_backing(&mut command.body, source)
4681            }
4682            CompoundCommand::While(command) => {
4683                Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
4684                Self::materialize_stmt_seq_source_backing(&mut command.body, source);
4685            }
4686            CompoundCommand::Until(command) => {
4687                Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
4688                Self::materialize_stmt_seq_source_backing(&mut command.body, source);
4689            }
4690            CompoundCommand::Case(command) => {
4691                for case in &mut command.cases {
4692                    Self::materialize_stmt_seq_source_backing(&mut case.body, source);
4693                }
4694            }
4695            CompoundCommand::Select(command) => {
4696                Self::materialize_stmt_seq_source_backing(&mut command.body, source)
4697            }
4698            CompoundCommand::Subshell(commands) | CompoundCommand::BraceGroup(commands) => {
4699                Self::materialize_stmt_seq_source_backing(commands, source);
4700            }
4701            CompoundCommand::Arithmetic(_) => {}
4702            CompoundCommand::Time(command) => {
4703                if let Some(inner) = &mut command.command {
4704                    Self::materialize_stmt_source_backing(inner.as_mut(), source);
4705                }
4706            }
4707            CompoundCommand::Conditional(_) => {}
4708            CompoundCommand::Coproc(command) => {
4709                Self::materialize_stmt_source_backing(command.body.as_mut(), source);
4710            }
4711            CompoundCommand::Always(command) => {
4712                Self::materialize_stmt_seq_source_backing(&mut command.body, source);
4713                Self::materialize_stmt_seq_source_backing(&mut command.always_body, source);
4714            }
4715        }
4716    }
4717
4718    fn rebase_words(words: &mut [Word], base: Position) {
4719        for word in words {
4720            Self::rebase_word(word, base);
4721        }
4722    }
4723
4724    fn rebase_patterns(patterns: &mut [Pattern], base: Position) {
4725        for pattern in patterns {
4726            Self::rebase_pattern(pattern, base);
4727        }
4728    }
4729
4730    fn materialize_literal_text_source_backing(text: &mut LiteralText, span: Span, source: &str) {
4731        match text {
4732            LiteralText::Source => {
4733                *text = LiteralText::owned(span.slice(source).to_string());
4734            }
4735            LiteralText::CookedSource(cooked) => {
4736                *text = LiteralText::owned(cooked.to_string());
4737            }
4738            LiteralText::Owned(_) => {}
4739        }
4740    }
4741
4742    fn materialize_source_text_source_backing(text: &mut SourceText, source: &str) {
4743        if text.is_source_backed() {
4744            let span = text.span();
4745            let cooked = text.slice(source).to_string();
4746            *text = SourceText::cooked(span, cooked);
4747        }
4748    }
4749
4750    fn materialize_word_source_backing(word: &mut Word, source: &str) {
4751        for part in &mut word.parts {
4752            Self::materialize_word_part_source_backing(part, source);
4753        }
4754    }
4755
4756    fn materialize_pattern_source_backing(pattern: &mut Pattern, source: &str) {
4757        for part in &mut pattern.parts {
4758            Self::materialize_pattern_part_source_backing(part, source);
4759        }
4760    }
4761
4762    fn materialize_pattern_part_source_backing(part: &mut PatternPartNode, source: &str) {
4763        match &mut part.kind {
4764            PatternPart::Literal(text) => {
4765                Self::materialize_literal_text_source_backing(text, part.span, source);
4766            }
4767            PatternPart::CharClass(text) => {
4768                Self::materialize_source_text_source_backing(text, source);
4769            }
4770            PatternPart::Group { patterns, .. } => {
4771                for pattern in patterns {
4772                    Self::materialize_pattern_source_backing(pattern, source);
4773                }
4774            }
4775            PatternPart::Word(word) => Self::materialize_word_source_backing(word, source),
4776            PatternPart::AnyString | PatternPart::AnyChar => {}
4777        }
4778    }
4779
4780    fn materialize_word_part_source_backing(part: &mut WordPartNode, source: &str) {
4781        match &mut part.kind {
4782            WordPart::Literal(text) => {
4783                Self::materialize_literal_text_source_backing(text, part.span, source);
4784            }
4785            WordPart::ZshQualifiedGlob(glob) => {
4786                Self::materialize_zsh_qualified_glob_source_backing(glob, source);
4787            }
4788            WordPart::SingleQuoted { value, .. } => {
4789                Self::materialize_source_text_source_backing(value, source);
4790            }
4791            WordPart::DoubleQuoted { parts, .. } => {
4792                for part in parts {
4793                    Self::materialize_word_part_source_backing(part, source);
4794                }
4795            }
4796            WordPart::Parameter(parameter) => {
4797                Self::materialize_source_text_source_backing(&mut parameter.raw_body, source);
4798                Self::materialize_parameter_expansion_syntax_source_backing(
4799                    &mut parameter.syntax,
4800                    source,
4801                );
4802            }
4803            WordPart::ParameterExpansion {
4804                reference,
4805                operator,
4806                operand,
4807                operand_word_ast,
4808                ..
4809            } => {
4810                Self::materialize_var_ref_source_backing(reference, source);
4811                Self::materialize_parameter_operator_source_backing(operator, source);
4812                if let Some(operand) = operand {
4813                    Self::materialize_source_text_source_backing(operand, source);
4814                }
4815                if let Some(word_ast) = operand_word_ast {
4816                    Self::materialize_word_source_backing(word_ast, source);
4817                }
4818            }
4819            WordPart::ArrayAccess(reference)
4820            | WordPart::Length(reference)
4821            | WordPart::ArrayLength(reference)
4822            | WordPart::ArrayIndices(reference)
4823            | WordPart::Transformation { reference, .. } => {
4824                Self::materialize_var_ref_source_backing(reference, source);
4825            }
4826            WordPart::Substring {
4827                reference,
4828                offset,
4829                offset_ast,
4830                offset_word_ast,
4831                length,
4832                length_ast,
4833                length_word_ast,
4834                ..
4835            }
4836            | WordPart::ArraySlice {
4837                reference,
4838                offset,
4839                offset_ast,
4840                offset_word_ast,
4841                length,
4842                length_ast,
4843                length_word_ast,
4844                ..
4845            } => {
4846                Self::materialize_var_ref_source_backing(reference, source);
4847                Self::materialize_source_text_source_backing(offset, source);
4848                Self::materialize_word_source_backing(offset_word_ast, source);
4849                if let Some(expr) = offset_ast {
4850                    Self::materialize_arithmetic_expr_source_backing(expr, source);
4851                }
4852                if let Some(length) = length {
4853                    Self::materialize_source_text_source_backing(length, source);
4854                }
4855                if let Some(word_ast) = length_word_ast {
4856                    Self::materialize_word_source_backing(word_ast, source);
4857                }
4858                if let Some(expr) = length_ast {
4859                    Self::materialize_arithmetic_expr_source_backing(expr, source);
4860                }
4861            }
4862            WordPart::IndirectExpansion {
4863                reference,
4864                operator,
4865                operand,
4866                operand_word_ast,
4867                ..
4868            } => {
4869                Self::materialize_var_ref_source_backing(reference, source);
4870                if let Some(operator) = operator {
4871                    Self::materialize_parameter_operator_source_backing(operator, source);
4872                }
4873                if let Some(operand) = operand {
4874                    Self::materialize_source_text_source_backing(operand, source);
4875                }
4876                if let Some(word_ast) = operand_word_ast {
4877                    Self::materialize_word_source_backing(word_ast, source);
4878                }
4879            }
4880            WordPart::ArithmeticExpansion {
4881                expression,
4882                expression_ast,
4883                expression_word_ast,
4884                ..
4885            } => {
4886                Self::materialize_source_text_source_backing(expression, source);
4887                Self::materialize_word_source_backing(expression_word_ast, source);
4888                if let Some(expr) = expression_ast {
4889                    Self::materialize_arithmetic_expr_source_backing(expr, source);
4890                }
4891            }
4892            WordPart::CommandSubstitution { .. }
4893            | WordPart::ProcessSubstitution { .. }
4894            | WordPart::Variable(_)
4895            | WordPart::PrefixMatch { .. } => {}
4896        }
4897    }
4898
4899    fn materialize_var_ref_source_backing(reference: &mut VarRef, source: &str) {
4900        if let Some(subscript) = &mut reference.subscript {
4901            Self::materialize_subscript_source_backing(subscript, source);
4902        }
4903    }
4904
4905    fn materialize_subscript_source_backing(subscript: &mut Subscript, source: &str) {
4906        Self::materialize_source_text_source_backing(&mut subscript.text, source);
4907        if let Some(raw) = &mut subscript.raw {
4908            Self::materialize_source_text_source_backing(raw, source);
4909        }
4910        if let Some(word_ast) = &mut subscript.word_ast {
4911            Self::materialize_word_source_backing(word_ast, source);
4912        }
4913        if let Some(expr) = &mut subscript.arithmetic_ast {
4914            Self::materialize_arithmetic_expr_source_backing(expr, source);
4915        }
4916    }
4917
4918    fn materialize_zsh_qualified_glob_source_backing(glob: &mut ZshQualifiedGlob, source: &str) {
4919        for segment in &mut glob.segments {
4920            match segment {
4921                ZshGlobSegment::Pattern(pattern) => {
4922                    Self::materialize_pattern_source_backing(pattern, source);
4923                }
4924                ZshGlobSegment::InlineControl(_) => {}
4925            }
4926        }
4927        if let Some(qualifiers) = &mut glob.qualifiers {
4928            for fragment in &mut qualifiers.fragments {
4929                match fragment {
4930                    ZshGlobQualifier::LetterSequence { text, .. } => {
4931                        Self::materialize_source_text_source_backing(text, source);
4932                    }
4933                    ZshGlobQualifier::NumericArgument { start, end, .. } => {
4934                        Self::materialize_source_text_source_backing(start, source);
4935                        if let Some(end) = end {
4936                            Self::materialize_source_text_source_backing(end, source);
4937                        }
4938                    }
4939                    ZshGlobQualifier::Negation { .. } | ZshGlobQualifier::Flag { .. } => {}
4940                }
4941            }
4942        }
4943    }
4944
4945    fn materialize_parameter_expansion_syntax_source_backing(
4946        syntax: &mut ParameterExpansionSyntax,
4947        source: &str,
4948    ) {
4949        match syntax {
4950            ParameterExpansionSyntax::Bourne(syntax) => match syntax {
4951                BourneParameterExpansion::Access { reference }
4952                | BourneParameterExpansion::Length { reference }
4953                | BourneParameterExpansion::Indices { reference }
4954                | BourneParameterExpansion::Transformation { reference, .. } => {
4955                    Self::materialize_var_ref_source_backing(reference, source);
4956                }
4957                BourneParameterExpansion::Indirect {
4958                    reference,
4959                    operator,
4960                    operand,
4961                    operand_word_ast,
4962                    ..
4963                } => {
4964                    Self::materialize_var_ref_source_backing(reference, source);
4965                    if let Some(operator) = operator {
4966                        Self::materialize_parameter_operator_source_backing(operator, source);
4967                    }
4968                    if let Some(operand) = operand {
4969                        Self::materialize_source_text_source_backing(operand, source);
4970                    }
4971                    if let Some(word_ast) = operand_word_ast {
4972                        Self::materialize_word_source_backing(word_ast, source);
4973                    }
4974                }
4975                BourneParameterExpansion::PrefixMatch { .. } => {}
4976                BourneParameterExpansion::Slice {
4977                    reference,
4978                    offset,
4979                    offset_ast,
4980                    offset_word_ast,
4981                    length,
4982                    length_ast,
4983                    length_word_ast,
4984                } => {
4985                    Self::materialize_var_ref_source_backing(reference, source);
4986                    Self::materialize_source_text_source_backing(offset, source);
4987                    Self::materialize_word_source_backing(offset_word_ast, source);
4988                    if let Some(expr) = offset_ast {
4989                        Self::materialize_arithmetic_expr_source_backing(expr, source);
4990                    }
4991                    if let Some(length) = length {
4992                        Self::materialize_source_text_source_backing(length, source);
4993                    }
4994                    if let Some(word_ast) = length_word_ast {
4995                        Self::materialize_word_source_backing(word_ast, source);
4996                    }
4997                    if let Some(expr) = length_ast {
4998                        Self::materialize_arithmetic_expr_source_backing(expr, source);
4999                    }
5000                }
5001                BourneParameterExpansion::Operation {
5002                    reference,
5003                    operator,
5004                    operand,
5005                    operand_word_ast,
5006                    ..
5007                } => {
5008                    Self::materialize_var_ref_source_backing(reference, source);
5009                    Self::materialize_parameter_operator_source_backing(operator, source);
5010                    if let Some(operand) = operand {
5011                        Self::materialize_source_text_source_backing(operand, source);
5012                    }
5013                    if let Some(word_ast) = operand_word_ast {
5014                        Self::materialize_word_source_backing(word_ast, source);
5015                    }
5016                }
5017            },
5018            ParameterExpansionSyntax::Zsh(syntax) => {
5019                match &mut syntax.target {
5020                    ZshExpansionTarget::Reference(reference) => {
5021                        Self::materialize_var_ref_source_backing(reference, source);
5022                    }
5023                    ZshExpansionTarget::Word(word) => {
5024                        Self::materialize_word_source_backing(word, source);
5025                    }
5026                    ZshExpansionTarget::Nested(parameter) => {
5027                        Self::materialize_source_text_source_backing(
5028                            &mut parameter.raw_body,
5029                            source,
5030                        );
5031                        Self::materialize_parameter_expansion_syntax_source_backing(
5032                            &mut parameter.syntax,
5033                            source,
5034                        );
5035                    }
5036                    ZshExpansionTarget::Empty => {}
5037                }
5038                for modifier in &mut syntax.modifiers {
5039                    if let Some(argument) = &mut modifier.argument {
5040                        Self::materialize_source_text_source_backing(argument, source);
5041                    }
5042                    if let Some(argument_word_ast) = &mut modifier.argument_word_ast {
5043                        Self::materialize_word_source_backing(argument_word_ast, source);
5044                    }
5045                }
5046                if let Some(operation) = &mut syntax.operation {
5047                    match operation {
5048                        ZshExpansionOperation::PatternOperation {
5049                            operand,
5050                            operand_word_ast,
5051                            ..
5052                        }
5053                        | ZshExpansionOperation::Defaulting {
5054                            operand,
5055                            operand_word_ast,
5056                            ..
5057                        }
5058                        | ZshExpansionOperation::TrimOperation {
5059                            operand,
5060                            operand_word_ast,
5061                            ..
5062                        } => {
5063                            Self::materialize_source_text_source_backing(operand, source);
5064                            Self::materialize_word_source_backing(operand_word_ast, source);
5065                        }
5066                        ZshExpansionOperation::Unknown { text, word_ast } => {
5067                            Self::materialize_source_text_source_backing(text, source);
5068                            Self::materialize_word_source_backing(word_ast, source);
5069                        }
5070                        ZshExpansionOperation::ReplacementOperation {
5071                            pattern,
5072                            pattern_word_ast,
5073                            replacement,
5074                            replacement_word_ast,
5075                            ..
5076                        } => {
5077                            Self::materialize_source_text_source_backing(pattern, source);
5078                            Self::materialize_word_source_backing(pattern_word_ast, source);
5079                            if let Some(replacement) = replacement {
5080                                Self::materialize_source_text_source_backing(replacement, source);
5081                            }
5082                            if let Some(replacement_word_ast) = replacement_word_ast {
5083                                Self::materialize_word_source_backing(replacement_word_ast, source);
5084                            }
5085                        }
5086                        ZshExpansionOperation::Slice {
5087                            offset,
5088                            offset_word_ast,
5089                            length,
5090                            length_word_ast,
5091                        } => {
5092                            Self::materialize_source_text_source_backing(offset, source);
5093                            Self::materialize_word_source_backing(offset_word_ast, source);
5094                            if let Some(length) = length {
5095                                Self::materialize_source_text_source_backing(length, source);
5096                            }
5097                            if let Some(length_word_ast) = length_word_ast {
5098                                Self::materialize_word_source_backing(length_word_ast, source);
5099                            }
5100                        }
5101                    }
5102                }
5103            }
5104        }
5105    }
5106
5107    fn materialize_parameter_operator_source_backing(operator: &mut ParameterOp, source: &str) {
5108        match operator {
5109            ParameterOp::RemovePrefixShort { pattern }
5110            | ParameterOp::RemovePrefixLong { pattern }
5111            | ParameterOp::RemoveSuffixShort { pattern }
5112            | ParameterOp::RemoveSuffixLong { pattern } => {
5113                Self::materialize_pattern_source_backing(pattern, source);
5114            }
5115            ParameterOp::ReplaceFirst {
5116                pattern,
5117                replacement,
5118                replacement_word_ast,
5119            }
5120            | ParameterOp::ReplaceAll {
5121                pattern,
5122                replacement,
5123                replacement_word_ast,
5124            } => {
5125                Self::materialize_pattern_source_backing(pattern, source);
5126                Self::materialize_source_text_source_backing(replacement, source);
5127                Self::materialize_word_source_backing(replacement_word_ast, source);
5128            }
5129            ParameterOp::UseDefault
5130            | ParameterOp::AssignDefault
5131            | ParameterOp::UseReplacement
5132            | ParameterOp::Error
5133            | ParameterOp::UpperFirst
5134            | ParameterOp::UpperAll
5135            | ParameterOp::LowerFirst
5136            | ParameterOp::LowerAll => {}
5137        }
5138    }
5139
5140    fn materialize_arithmetic_expr_source_backing(expr: &mut ArithmeticExprNode, source: &str) {
5141        match &mut expr.kind {
5142            ArithmeticExpr::Number(text) => {
5143                Self::materialize_source_text_source_backing(text, source);
5144            }
5145            ArithmeticExpr::Variable(_) => {}
5146            ArithmeticExpr::Indexed { index, .. } => {
5147                Self::materialize_arithmetic_expr_source_backing(index, source);
5148            }
5149            ArithmeticExpr::ShellWord(word) => {
5150                Self::materialize_word_source_backing(word, source);
5151            }
5152            ArithmeticExpr::Parenthesized { expression } => {
5153                Self::materialize_arithmetic_expr_source_backing(expression, source);
5154            }
5155            ArithmeticExpr::Unary { expr, .. } | ArithmeticExpr::Postfix { expr, .. } => {
5156                Self::materialize_arithmetic_expr_source_backing(expr, source);
5157            }
5158            ArithmeticExpr::Binary { left, right, .. } => {
5159                Self::materialize_arithmetic_expr_source_backing(left, source);
5160                Self::materialize_arithmetic_expr_source_backing(right, source);
5161            }
5162            ArithmeticExpr::Conditional {
5163                condition,
5164                then_expr,
5165                else_expr,
5166            } => {
5167                Self::materialize_arithmetic_expr_source_backing(condition, source);
5168                Self::materialize_arithmetic_expr_source_backing(then_expr, source);
5169                Self::materialize_arithmetic_expr_source_backing(else_expr, source);
5170            }
5171            ArithmeticExpr::Assignment { target, value, .. } => {
5172                Self::materialize_arithmetic_lvalue_source_backing(target, source);
5173                Self::materialize_arithmetic_expr_source_backing(value, source);
5174            }
5175        }
5176    }
5177
5178    fn materialize_arithmetic_lvalue_source_backing(target: &mut ArithmeticLvalue, source: &str) {
5179        match target {
5180            ArithmeticLvalue::Variable(_) => {}
5181            ArithmeticLvalue::Indexed { index, .. } => {
5182                Self::materialize_arithmetic_expr_source_backing(index, source);
5183            }
5184        }
5185    }
5186
5187    fn rebase_word(word: &mut Word, base: Position) {
5188        word.span = word.span.rebased(base);
5189        for brace in &mut word.brace_syntax {
5190            brace.span = brace.span.rebased(base);
5191        }
5192        Self::rebase_word_parts(&mut word.parts, base);
5193    }
5194
5195    fn rebase_heredoc_body(body: &mut HeredocBody, base: Position) {
5196        body.span = body.span.rebased(base);
5197        for part in &mut body.parts {
5198            Self::rebase_heredoc_body_part(part, base);
5199        }
5200    }
5201
5202    fn rebase_pattern(pattern: &mut Pattern, base: Position) {
5203        pattern.span = pattern.span.rebased(base);
5204        Self::rebase_pattern_parts(&mut pattern.parts, base);
5205    }
5206
5207    fn rebase_word_parts(parts: &mut [WordPartNode], base: Position) {
5208        for part in parts {
5209            Self::rebase_word_part(part, base);
5210        }
5211    }
5212
5213    fn rebase_pattern_parts(parts: &mut [PatternPartNode], base: Position) {
5214        for part in parts {
5215            part.span = part.span.rebased(base);
5216            match &mut part.kind {
5217                PatternPart::CharClass(text) => text.rebased(base),
5218                PatternPart::Group { patterns, .. } => Self::rebase_patterns(patterns, base),
5219                PatternPart::Word(word) => Self::rebase_word(word, base),
5220                PatternPart::Literal(_) | PatternPart::AnyString | PatternPart::AnyChar => {}
5221            }
5222        }
5223    }
5224
5225    fn rebase_heredoc_body_part(part: &mut HeredocBodyPartNode, base: Position) {
5226        part.span = part.span.rebased(base);
5227        match &mut part.kind {
5228            HeredocBodyPart::Literal(_) | HeredocBodyPart::Variable(_) => {}
5229            HeredocBodyPart::CommandSubstitution { body, .. } => Self::rebase_stmt_seq(body, base),
5230            HeredocBodyPart::ArithmeticExpansion {
5231                expression,
5232                expression_ast,
5233                expression_word_ast,
5234                ..
5235            } => {
5236                expression.rebased(base);
5237                Self::rebase_word(expression_word_ast, base);
5238                if let Some(expr) = expression_ast {
5239                    Self::rebase_arithmetic_expr(expr, base);
5240                }
5241            }
5242            HeredocBodyPart::Parameter(parameter) => {
5243                parameter.span = parameter.span.rebased(base);
5244                parameter.raw_body.rebased(base);
5245                Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
5246            }
5247        }
5248    }
5249
5250    fn rebase_word_part(part: &mut WordPartNode, base: Position) {
5251        part.span = part.span.rebased(base);
5252        match &mut part.kind {
5253            WordPart::ZshQualifiedGlob(glob) => Self::rebase_zsh_qualified_glob(glob, base),
5254            WordPart::SingleQuoted { value, .. } => value.rebased(base),
5255            WordPart::DoubleQuoted { parts, .. } => Self::rebase_word_parts(parts, base),
5256            WordPart::Parameter(parameter) => {
5257                parameter.span = parameter.span.rebased(base);
5258                parameter.raw_body.rebased(base);
5259                Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
5260            }
5261            WordPart::ParameterExpansion {
5262                reference,
5263                operator,
5264                operand,
5265                operand_word_ast,
5266                ..
5267            } => {
5268                Self::rebase_var_ref(reference, base);
5269                match operator {
5270                    ParameterOp::RemovePrefixShort { pattern }
5271                    | ParameterOp::RemovePrefixLong { pattern }
5272                    | ParameterOp::RemoveSuffixShort { pattern }
5273                    | ParameterOp::RemoveSuffixLong { pattern } => {
5274                        Self::rebase_pattern(pattern, base);
5275                    }
5276                    ParameterOp::ReplaceFirst {
5277                        pattern,
5278                        replacement,
5279                        ..
5280                    }
5281                    | ParameterOp::ReplaceAll {
5282                        pattern,
5283                        replacement,
5284                        ..
5285                    } => {
5286                        Self::rebase_pattern(pattern, base);
5287                        replacement.rebased(base);
5288                    }
5289                    ParameterOp::UseDefault
5290                    | ParameterOp::AssignDefault
5291                    | ParameterOp::UseReplacement
5292                    | ParameterOp::Error
5293                    | ParameterOp::UpperFirst
5294                    | ParameterOp::UpperAll
5295                    | ParameterOp::LowerFirst
5296                    | ParameterOp::LowerAll => {}
5297                }
5298                if let Some(operand) = operand {
5299                    operand.rebased(base);
5300                }
5301                if let Some(word_ast) = operand_word_ast {
5302                    Self::rebase_word(word_ast, base);
5303                }
5304            }
5305            WordPart::ArrayAccess(reference)
5306            | WordPart::Length(reference)
5307            | WordPart::ArrayLength(reference)
5308            | WordPart::ArrayIndices(reference)
5309            | WordPart::Transformation { reference, .. } => Self::rebase_var_ref(reference, base),
5310            WordPart::Substring {
5311                reference,
5312                offset,
5313                offset_ast,
5314                offset_word_ast,
5315                length,
5316                length_ast,
5317                length_word_ast,
5318                ..
5319            }
5320            | WordPart::ArraySlice {
5321                reference,
5322                offset,
5323                offset_ast,
5324                offset_word_ast,
5325                length,
5326                length_ast,
5327                length_word_ast,
5328                ..
5329            } => {
5330                Self::rebase_var_ref(reference, base);
5331                offset.rebased(base);
5332                Self::rebase_word(offset_word_ast, base);
5333                if let Some(expr) = offset_ast {
5334                    Self::rebase_arithmetic_expr(expr, base);
5335                }
5336                if let Some(length) = length {
5337                    length.rebased(base);
5338                }
5339                if let Some(word_ast) = length_word_ast {
5340                    Self::rebase_word(word_ast, base);
5341                }
5342                if let Some(expr) = length_ast {
5343                    Self::rebase_arithmetic_expr(expr, base);
5344                }
5345            }
5346            WordPart::IndirectExpansion {
5347                reference,
5348                operator,
5349                operand,
5350                operand_word_ast,
5351                ..
5352            } => {
5353                Self::rebase_var_ref(reference, base);
5354                if let Some(operator) = operator {
5355                    Self::rebase_parameter_operator(operator, base);
5356                }
5357                if let Some(operand) = operand {
5358                    operand.rebased(base);
5359                }
5360                if let Some(word_ast) = operand_word_ast {
5361                    Self::rebase_word(word_ast, base);
5362                }
5363            }
5364            WordPart::ArithmeticExpansion {
5365                expression,
5366                expression_ast,
5367                expression_word_ast,
5368                ..
5369            } => {
5370                expression.rebased(base);
5371                Self::rebase_word(expression_word_ast, base);
5372                if let Some(expr) = expression_ast {
5373                    Self::rebase_arithmetic_expr(expr, base);
5374                }
5375            }
5376            WordPart::CommandSubstitution { body, .. }
5377            | WordPart::ProcessSubstitution { body, .. } => Self::rebase_stmt_seq(body, base),
5378            WordPart::Literal(_) | WordPart::Variable(_) | WordPart::PrefixMatch { .. } => {}
5379        }
5380    }
5381
5382    fn rebase_zsh_qualified_glob(glob: &mut ZshQualifiedGlob, base: Position) {
5383        glob.span = glob.span.rebased(base);
5384        for segment in &mut glob.segments {
5385            Self::rebase_zsh_glob_segment(segment, base);
5386        }
5387        if let Some(qualifiers) = &mut glob.qualifiers {
5388            Self::rebase_zsh_glob_qualifier_group(qualifiers, base);
5389        }
5390    }
5391
5392    fn rebase_zsh_glob_segment(segment: &mut ZshGlobSegment, base: Position) {
5393        match segment {
5394            ZshGlobSegment::Pattern(pattern) => Self::rebase_pattern(pattern, base),
5395            ZshGlobSegment::InlineControl(control) => {
5396                Self::rebase_zsh_inline_glob_control(control, base)
5397            }
5398        }
5399    }
5400
5401    fn rebase_zsh_inline_glob_control(control: &mut ZshInlineGlobControl, base: Position) {
5402        match control {
5403            ZshInlineGlobControl::CaseInsensitive { span }
5404            | ZshInlineGlobControl::Backreferences { span }
5405            | ZshInlineGlobControl::StartAnchor { span }
5406            | ZshInlineGlobControl::EndAnchor { span } => {
5407                *span = span.rebased(base);
5408            }
5409        }
5410    }
5411
5412    fn rebase_zsh_glob_qualifier_group(group: &mut ZshGlobQualifierGroup, base: Position) {
5413        group.span = group.span.rebased(base);
5414        for fragment in &mut group.fragments {
5415            match fragment {
5416                ZshGlobQualifier::Negation { span } | ZshGlobQualifier::Flag { span, .. } => {
5417                    *span = span.rebased(base);
5418                }
5419                ZshGlobQualifier::LetterSequence { text, span } => {
5420                    *span = span.rebased(base);
5421                    text.rebased(base);
5422                }
5423                ZshGlobQualifier::NumericArgument { span, start, end } => {
5424                    *span = span.rebased(base);
5425                    start.rebased(base);
5426                    if let Some(end) = end {
5427                        end.rebased(base);
5428                    }
5429                }
5430            }
5431        }
5432    }
5433
5434    fn rebase_parameter_expansion_syntax(syntax: &mut ParameterExpansionSyntax, base: Position) {
5435        match syntax {
5436            ParameterExpansionSyntax::Bourne(syntax) => match syntax {
5437                BourneParameterExpansion::Access { reference }
5438                | BourneParameterExpansion::Length { reference }
5439                | BourneParameterExpansion::Indices { reference }
5440                | BourneParameterExpansion::Transformation { reference, .. } => {
5441                    Self::rebase_var_ref(reference, base);
5442                }
5443                BourneParameterExpansion::Indirect {
5444                    reference,
5445                    operand,
5446                    operator,
5447                    operand_word_ast,
5448                    ..
5449                } => {
5450                    Self::rebase_var_ref(reference, base);
5451                    if let Some(operator) = operator {
5452                        Self::rebase_parameter_operator(operator, base);
5453                    }
5454                    if let Some(operand) = operand {
5455                        operand.rebased(base);
5456                    }
5457                    if let Some(word_ast) = operand_word_ast {
5458                        Self::rebase_word(word_ast, base);
5459                    }
5460                }
5461                BourneParameterExpansion::PrefixMatch { .. } => {}
5462                BourneParameterExpansion::Slice {
5463                    reference,
5464                    offset,
5465                    offset_ast,
5466                    offset_word_ast,
5467                    length,
5468                    length_ast,
5469                    length_word_ast,
5470                } => {
5471                    Self::rebase_var_ref(reference, base);
5472                    offset.rebased(base);
5473                    Self::rebase_word(offset_word_ast, base);
5474                    if let Some(expr) = offset_ast {
5475                        Self::rebase_arithmetic_expr(expr, base);
5476                    }
5477                    if let Some(length) = length {
5478                        length.rebased(base);
5479                    }
5480                    if let Some(word_ast) = length_word_ast {
5481                        Self::rebase_word(word_ast, base);
5482                    }
5483                    if let Some(expr) = length_ast {
5484                        Self::rebase_arithmetic_expr(expr, base);
5485                    }
5486                }
5487                BourneParameterExpansion::Operation {
5488                    reference,
5489                    operator,
5490                    operand,
5491                    operand_word_ast,
5492                    ..
5493                } => {
5494                    Self::rebase_var_ref(reference, base);
5495                    Self::rebase_parameter_operator(operator, base);
5496                    if let Some(operand) = operand {
5497                        operand.rebased(base);
5498                    }
5499                    if let Some(word_ast) = operand_word_ast {
5500                        Self::rebase_word(word_ast, base);
5501                    }
5502                }
5503            },
5504            ParameterExpansionSyntax::Zsh(syntax) => {
5505                match &mut syntax.target {
5506                    ZshExpansionTarget::Reference(reference) => {
5507                        Self::rebase_var_ref(reference, base)
5508                    }
5509                    ZshExpansionTarget::Word(word) => Self::rebase_word(word, base),
5510                    ZshExpansionTarget::Nested(parameter) => {
5511                        parameter.span = parameter.span.rebased(base);
5512                        parameter.raw_body.rebased(base);
5513                        Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
5514                    }
5515                    ZshExpansionTarget::Empty => {}
5516                }
5517                for modifier in &mut syntax.modifiers {
5518                    modifier.span = modifier.span.rebased(base);
5519                    if let Some(argument) = &mut modifier.argument {
5520                        argument.rebased(base);
5521                    }
5522                    if let Some(argument_word_ast) = &mut modifier.argument_word_ast {
5523                        Self::rebase_word(argument_word_ast, base);
5524                    }
5525                }
5526                if let Some(length_prefix) = &mut syntax.length_prefix {
5527                    *length_prefix = length_prefix.rebased(base);
5528                }
5529                if let Some(operation) = &mut syntax.operation {
5530                    match operation {
5531                        ZshExpansionOperation::PatternOperation {
5532                            operand,
5533                            operand_word_ast,
5534                            ..
5535                        }
5536                        | ZshExpansionOperation::Defaulting {
5537                            operand,
5538                            operand_word_ast,
5539                            ..
5540                        }
5541                        | ZshExpansionOperation::TrimOperation {
5542                            operand,
5543                            operand_word_ast,
5544                            ..
5545                        } => {
5546                            operand.rebased(base);
5547                            Self::rebase_word(operand_word_ast, base);
5548                        }
5549                        ZshExpansionOperation::Unknown { text, word_ast } => {
5550                            text.rebased(base);
5551                            Self::rebase_word(word_ast, base);
5552                        }
5553                        ZshExpansionOperation::ReplacementOperation {
5554                            pattern,
5555                            pattern_word_ast,
5556                            replacement,
5557                            replacement_word_ast,
5558                            ..
5559                        } => {
5560                            pattern.rebased(base);
5561                            Self::rebase_word(pattern_word_ast, base);
5562                            if let Some(replacement) = replacement {
5563                                replacement.rebased(base);
5564                            }
5565                            if let Some(replacement_word_ast) = replacement_word_ast {
5566                                Self::rebase_word(replacement_word_ast, base);
5567                            }
5568                        }
5569                        ZshExpansionOperation::Slice {
5570                            offset,
5571                            offset_word_ast,
5572                            length,
5573                            length_word_ast,
5574                        } => {
5575                            offset.rebased(base);
5576                            Self::rebase_word(offset_word_ast, base);
5577                            if let Some(length) = length {
5578                                length.rebased(base);
5579                            }
5580                            if let Some(length_word_ast) = length_word_ast {
5581                                Self::rebase_word(length_word_ast, base);
5582                            }
5583                        }
5584                    }
5585                }
5586            }
5587        }
5588    }
5589
5590    fn rebase_parameter_operator(operator: &mut ParameterOp, base: Position) {
5591        match operator {
5592            ParameterOp::RemovePrefixShort { pattern }
5593            | ParameterOp::RemovePrefixLong { pattern }
5594            | ParameterOp::RemoveSuffixShort { pattern }
5595            | ParameterOp::RemoveSuffixLong { pattern } => {
5596                Self::rebase_pattern(pattern, base);
5597            }
5598            ParameterOp::ReplaceFirst {
5599                pattern,
5600                replacement,
5601                replacement_word_ast,
5602            }
5603            | ParameterOp::ReplaceAll {
5604                pattern,
5605                replacement,
5606                replacement_word_ast,
5607            } => {
5608                Self::rebase_pattern(pattern, base);
5609                replacement.rebased(base);
5610                Self::rebase_word(replacement_word_ast, base);
5611            }
5612            ParameterOp::UseDefault
5613            | ParameterOp::AssignDefault
5614            | ParameterOp::UseReplacement
5615            | ParameterOp::Error
5616            | ParameterOp::UpperFirst
5617            | ParameterOp::UpperAll
5618            | ParameterOp::LowerFirst
5619            | ParameterOp::LowerAll => {}
5620        }
5621    }
5622
5623    fn rebase_conditional_expr(expr: &mut ConditionalExpr, base: Position) {
5624        match expr {
5625            ConditionalExpr::Binary(binary) => {
5626                binary.op_span = binary.op_span.rebased(base);
5627                Self::rebase_conditional_expr(&mut binary.left, base);
5628                Self::rebase_conditional_expr(&mut binary.right, base);
5629            }
5630            ConditionalExpr::Unary(unary) => {
5631                unary.op_span = unary.op_span.rebased(base);
5632                Self::rebase_conditional_expr(&mut unary.expr, base);
5633            }
5634            ConditionalExpr::Parenthesized(paren) => {
5635                paren.left_paren_span = paren.left_paren_span.rebased(base);
5636                paren.right_paren_span = paren.right_paren_span.rebased(base);
5637                Self::rebase_conditional_expr(&mut paren.expr, base);
5638            }
5639            ConditionalExpr::Word(word) | ConditionalExpr::Regex(word) => {
5640                Self::rebase_word(word, base);
5641            }
5642            ConditionalExpr::Pattern(pattern) => Self::rebase_pattern(pattern, base),
5643            ConditionalExpr::VarRef(var_ref) => Self::rebase_var_ref(var_ref, base),
5644        }
5645    }
5646
5647    fn rebase_arithmetic_expr(expr: &mut ArithmeticExprNode, base: Position) {
5648        expr.span = expr.span.rebased(base);
5649        match &mut expr.kind {
5650            ArithmeticExpr::Number(text) => text.rebased(base),
5651            ArithmeticExpr::Variable(_) => {}
5652            ArithmeticExpr::Indexed { index, .. } => Self::rebase_arithmetic_expr(index, base),
5653            ArithmeticExpr::ShellWord(word) => Self::rebase_word(word, base),
5654            ArithmeticExpr::Parenthesized { expression } => {
5655                Self::rebase_arithmetic_expr(expression, base)
5656            }
5657            ArithmeticExpr::Unary { expr, .. } | ArithmeticExpr::Postfix { expr, .. } => {
5658                Self::rebase_arithmetic_expr(expr, base)
5659            }
5660            ArithmeticExpr::Binary { left, right, .. } => {
5661                Self::rebase_arithmetic_expr(left, base);
5662                Self::rebase_arithmetic_expr(right, base);
5663            }
5664            ArithmeticExpr::Conditional {
5665                condition,
5666                then_expr,
5667                else_expr,
5668            } => {
5669                Self::rebase_arithmetic_expr(condition, base);
5670                Self::rebase_arithmetic_expr(then_expr, base);
5671                Self::rebase_arithmetic_expr(else_expr, base);
5672            }
5673            ArithmeticExpr::Assignment { target, value, .. } => {
5674                Self::rebase_arithmetic_lvalue(target, base);
5675                Self::rebase_arithmetic_expr(value, base);
5676            }
5677        }
5678    }
5679
5680    fn rebase_arithmetic_lvalue(target: &mut ArithmeticLvalue, base: Position) {
5681        match target {
5682            ArithmeticLvalue::Variable(_) => {}
5683            ArithmeticLvalue::Indexed { index, .. } => Self::rebase_arithmetic_expr(index, base),
5684        }
5685    }
5686
5687    fn push_word_part(parts: &mut WordPartBuffer, part: WordPart, start: Position, end: Position) {
5688        Self::push_word_part_node(
5689            parts,
5690            WordPartNode::new(part, Span::from_positions(start, end)),
5691        );
5692    }
5693
5694    fn push_word_part_node(parts: &mut WordPartBuffer, part: WordPartNode) {
5695        parts.push(part);
5696    }
5697
5698    fn flush_literal_part(
5699        &self,
5700        parts: &mut WordPartBuffer,
5701        current: &mut String,
5702        current_start: Position,
5703        end: Position,
5704        source_backed: bool,
5705    ) {
5706        if !current.is_empty() {
5707            Self::push_word_part(
5708                parts,
5709                WordPart::Literal(self.literal_text(
5710                    std::mem::take(current),
5711                    current_start,
5712                    end,
5713                    source_backed,
5714                )),
5715                current_start,
5716                end,
5717            );
5718        }
5719    }
5720
5721    fn word_part_buffer_with_capacity(capacity: usize) -> WordPartBuffer {
5722        if capacity <= 2 {
5723            WordPartBuffer::new()
5724        } else {
5725            WordPartBuffer::with_capacity(capacity)
5726        }
5727    }
5728
5729    fn literal_text(
5730        &self,
5731        text: String,
5732        start: Position,
5733        end: Position,
5734        source_backed: bool,
5735    ) -> LiteralText {
5736        let span = Span::from_positions(start, end);
5737        if self.source_matches(span, &text) {
5738            LiteralText::source()
5739        } else if source_backed {
5740            LiteralText::cooked_source(text)
5741        } else {
5742            LiteralText::owned(text)
5743        }
5744    }
5745
5746    fn literal_text_from_str(
5747        &self,
5748        text: &str,
5749        start: Position,
5750        end: Position,
5751        source_backed: bool,
5752    ) -> LiteralText {
5753        self.literal_text_impl(text, None, start, end, source_backed)
5754    }
5755
5756    fn literal_text_impl(
5757        &self,
5758        text: &str,
5759        owned: Option<String>,
5760        start: Position,
5761        end: Position,
5762        source_backed: bool,
5763    ) -> LiteralText {
5764        let span = Span::from_positions(start, end);
5765        if self.source_matches(span, text) {
5766            LiteralText::source()
5767        } else if source_backed {
5768            LiteralText::cooked_source(owned.unwrap_or_else(|| text.to_owned()))
5769        } else {
5770            LiteralText::owned(owned.unwrap_or_else(|| text.to_owned()))
5771        }
5772    }
5773
5774    fn source_text(&self, text: String, start: Position, end: Position) -> SourceText {
5775        let span = Span::from_positions(start, end);
5776        if self.source_matches(span, &text) {
5777            SourceText::source(span)
5778        } else {
5779            SourceText::cooked(span, text)
5780        }
5781    }
5782
5783    fn source_text_from_str(&self, text: &str, start: Position, end: Position) -> SourceText {
5784        self.source_text_impl(text, None, start, end)
5785    }
5786
5787    fn source_text_impl(
5788        &self,
5789        text: &str,
5790        owned: Option<String>,
5791        start: Position,
5792        end: Position,
5793    ) -> SourceText {
5794        let span = Span::from_positions(start, end);
5795        if self.source_matches(span, text) {
5796            SourceText::source(span)
5797        } else {
5798            SourceText::cooked(span, owned.unwrap_or_else(|| text.to_owned()))
5799        }
5800    }
5801
5802    fn empty_source_text(&self, pos: Position) -> SourceText {
5803        SourceText::source(Span::from_positions(pos, pos))
5804    }
5805
5806    fn input_prefix_ends_with(&self, end_offset: usize, ch: char) -> bool {
5807        self.input
5808            .get(..end_offset)
5809            .is_some_and(|prefix| prefix.ends_with(ch))
5810    }
5811
5812    fn input_span_ends_with(&self, start: Position, end: Position, ch: char) -> bool {
5813        self.input
5814            .get(start.offset..end.offset)
5815            .is_some_and(|slice| slice.ends_with(ch))
5816    }
5817
5818    fn input_suffix_starts_with(&self, start_offset: usize, ch: char) -> bool {
5819        self.input
5820            .get(start_offset..)
5821            .is_some_and(|suffix| suffix.starts_with(ch))
5822    }
5823
5824    fn subscript_source_text(&self, raw: &str, span: Span) -> (SourceText, Option<SourceText>) {
5825        if raw.len() >= 2
5826            && ((raw.starts_with('"') && raw.ends_with('"'))
5827                || (raw.starts_with('\'') && raw.ends_with('\'')))
5828        {
5829            let raw_text = raw.to_string();
5830            let raw = if self.source_matches(span, raw) {
5831                SourceText::source(span)
5832            } else {
5833                SourceText::cooked(span, raw_text.clone())
5834            };
5835            let cooked = raw_text[1..raw_text.len() - 1].to_string();
5836            return (self.source_text(cooked, span.start, span.end), Some(raw));
5837        }
5838
5839        let text = if self.source_matches(span, raw) {
5840            SourceText::source(span)
5841        } else {
5842            SourceText::cooked(span, raw.to_string())
5843        };
5844        (text, None)
5845    }
5846
5847    fn subscript_from_source_text(
5848        &self,
5849        text: SourceText,
5850        raw: Option<SourceText>,
5851        interpretation: SubscriptInterpretation,
5852    ) -> Subscript {
5853        let kind = match text.slice(self.input).trim() {
5854            "@" => SubscriptKind::Selector(SubscriptSelector::At),
5855            "*" => SubscriptKind::Selector(SubscriptSelector::Star),
5856            _ => SubscriptKind::Ordinary,
5857        };
5858        let word_ast = if matches!(kind, SubscriptKind::Ordinary) {
5859            Some(self.parse_source_text_as_word(raw.as_ref().unwrap_or(&text)))
5860        } else {
5861            None
5862        };
5863        let arithmetic_ast = if matches!(kind, SubscriptKind::Ordinary) {
5864            self.simple_subscript_arithmetic_ast(&text)
5865                .or_else(|| self.maybe_parse_source_text_as_arithmetic(&text))
5866        } else {
5867            None
5868        };
5869        Subscript {
5870            text,
5871            raw,
5872            kind,
5873            interpretation,
5874            word_ast,
5875            arithmetic_ast,
5876        }
5877    }
5878
5879    fn simple_subscript_arithmetic_ast(&self, text: &SourceText) -> Option<ArithmeticExprNode> {
5880        if !text.is_source_backed() {
5881            return None;
5882        }
5883
5884        let raw = text.slice(self.input);
5885        if raw.is_empty() || raw.trim() != raw {
5886            return None;
5887        }
5888
5889        let span = text.span();
5890        if raw.bytes().all(|byte| byte.is_ascii_digit()) {
5891            return Some(ArithmeticExprNode::new(
5892                ArithmeticExpr::Number(SourceText::source(span)),
5893                span,
5894            ));
5895        }
5896
5897        if Self::is_valid_identifier(raw) {
5898            return Some(ArithmeticExprNode::new(
5899                ArithmeticExpr::Variable(Name::from(raw)),
5900                span,
5901            ));
5902        }
5903
5904        None
5905    }
5906
5907    fn subscript_from_text(
5908        &self,
5909        raw: &str,
5910        span: Span,
5911        interpretation: SubscriptInterpretation,
5912    ) -> Subscript {
5913        let (text, raw) = self.subscript_source_text(raw, span);
5914        self.subscript_from_source_text(text, raw, interpretation)
5915    }
5916
5917    fn var_ref(
5918        &self,
5919        name: impl Into<Name>,
5920        name_span: Span,
5921        subscript: Option<Subscript>,
5922        span: Span,
5923    ) -> VarRef {
5924        VarRef {
5925            name: name.into(),
5926            name_span,
5927            subscript: subscript.map(Box::new),
5928            span,
5929        }
5930    }
5931
5932    fn parameter_var_ref(
5933        &self,
5934        part_start: Position,
5935        prefix: &str,
5936        name: &str,
5937        subscript: Option<Subscript>,
5938        part_end: Position,
5939    ) -> VarRef {
5940        let name_start = part_start.advanced_by(prefix);
5941        let name_span = Span::from_positions(name_start, name_start.advanced_by(name));
5942        self.var_ref(
5943            Name::from(name),
5944            name_span,
5945            subscript,
5946            Span::from_positions(part_start, part_end),
5947        )
5948    }
5949
5950    fn parameter_word_part_from_legacy(
5951        &self,
5952        part: WordPart,
5953        part_start: Position,
5954        part_end: Position,
5955        source_backed: bool,
5956    ) -> WordPart {
5957        let span = Span::from_positions(part_start, part_end);
5958        let raw_body = self.parameter_raw_body_from_legacy(&part, span, source_backed);
5959        let raw_body_text = raw_body.slice(self.input).to_string();
5960
5961        let syntax = match part {
5962            WordPart::ParameterExpansion {
5963                reference,
5964                operator,
5965                operand,
5966                operand_word_ast,
5967                colon_variant,
5968            } => Some(BourneParameterExpansion::Operation {
5969                reference,
5970                operator: self.enrich_parameter_operator(operator),
5971                operand,
5972                operand_word_ast,
5973                colon_variant,
5974            }),
5975            WordPart::Length(reference) | WordPart::ArrayLength(reference) => {
5976                Some(BourneParameterExpansion::Length { reference })
5977            }
5978            WordPart::ArrayAccess(reference) => {
5979                Some(BourneParameterExpansion::Access { reference })
5980            }
5981            WordPart::ArrayIndices(reference) => {
5982                Some(BourneParameterExpansion::Indices { reference })
5983            }
5984            WordPart::Substring {
5985                reference,
5986                offset,
5987                offset_ast,
5988                offset_word_ast,
5989                length,
5990                length_ast,
5991                length_word_ast,
5992            }
5993            | WordPart::ArraySlice {
5994                reference,
5995                offset,
5996                offset_ast,
5997                offset_word_ast,
5998                length,
5999                length_ast,
6000                length_word_ast,
6001            } => Some(BourneParameterExpansion::Slice {
6002                reference,
6003                offset,
6004                offset_ast,
6005                offset_word_ast,
6006                length,
6007                length_ast,
6008                length_word_ast,
6009            }),
6010            WordPart::IndirectExpansion {
6011                reference,
6012                operator,
6013                operand,
6014                operand_word_ast,
6015                colon_variant,
6016            } => Some(BourneParameterExpansion::Indirect {
6017                reference,
6018                operator: operator.map(|operator| self.enrich_parameter_operator(operator)),
6019                operand,
6020                operand_word_ast,
6021                colon_variant,
6022            }),
6023            WordPart::PrefixMatch { prefix, kind } => {
6024                Some(BourneParameterExpansion::PrefixMatch { prefix, kind })
6025            }
6026            WordPart::Transformation {
6027                reference,
6028                operator,
6029            } => Some(BourneParameterExpansion::Transformation {
6030                reference,
6031                operator,
6032            }),
6033            WordPart::Variable(name) if raw_body_text == name.as_str() => {
6034                Some(BourneParameterExpansion::Access {
6035                    reference: self.parameter_var_ref(
6036                        part_start,
6037                        "${",
6038                        name.as_str(),
6039                        None,
6040                        part_end,
6041                    ),
6042                })
6043            }
6044            other => return other,
6045        };
6046
6047        let Some(syntax) = syntax else {
6048            unreachable!("matched Some above");
6049        };
6050        WordPart::Parameter(ParameterExpansion {
6051            syntax: ParameterExpansionSyntax::Bourne(syntax),
6052            span,
6053            raw_body,
6054        })
6055    }
6056
6057    fn enrich_parameter_operator(&self, operator: ParameterOp) -> ParameterOp {
6058        match operator {
6059            ParameterOp::ReplaceFirst {
6060                pattern,
6061                replacement,
6062                ..
6063            } => ParameterOp::ReplaceFirst {
6064                pattern,
6065                replacement_word_ast: self.parse_source_text_as_word(&replacement),
6066                replacement,
6067            },
6068            ParameterOp::ReplaceAll {
6069                pattern,
6070                replacement,
6071                ..
6072            } => ParameterOp::ReplaceAll {
6073                pattern,
6074                replacement_word_ast: self.parse_source_text_as_word(&replacement),
6075                replacement,
6076            },
6077            ParameterOp::UseDefault
6078            | ParameterOp::AssignDefault
6079            | ParameterOp::UseReplacement
6080            | ParameterOp::Error
6081            | ParameterOp::RemovePrefixShort { .. }
6082            | ParameterOp::RemovePrefixLong { .. }
6083            | ParameterOp::RemoveSuffixShort { .. }
6084            | ParameterOp::RemoveSuffixLong { .. }
6085            | ParameterOp::UpperFirst
6086            | ParameterOp::UpperAll
6087            | ParameterOp::LowerFirst
6088            | ParameterOp::LowerAll => operator,
6089        }
6090    }
6091
6092    fn parameter_raw_body_from_legacy(
6093        &self,
6094        part: &WordPart,
6095        span: Span,
6096        source_backed: bool,
6097    ) -> SourceText {
6098        if source_backed && span.end.offset <= self.input.len() {
6099            let syntax = span.slice(self.input);
6100            if let Some(body) = syntax
6101                .strip_prefix("${")
6102                .and_then(|syntax| syntax.strip_suffix('}'))
6103            {
6104                let start = span.start.advanced_by("${");
6105                let end = start.advanced_by(body);
6106                return SourceText::source(Span::from_positions(start, end));
6107            }
6108        }
6109
6110        let mut syntax = String::new();
6111        self.push_word_part_syntax(&mut syntax, part, span);
6112        let body = syntax
6113            .strip_prefix("${")
6114            .and_then(|syntax| syntax.strip_suffix('}'))
6115            .unwrap_or(syntax.as_str())
6116            .to_string();
6117        SourceText::from(body)
6118    }
6119
6120    fn zsh_parameter_word_part(
6121        &mut self,
6122        raw_body: SourceText,
6123        part_start: Position,
6124        part_end: Position,
6125    ) -> WordPart {
6126        let syntax = self.parse_zsh_parameter_syntax(&raw_body, raw_body.span().start);
6127        WordPart::Parameter(ParameterExpansion {
6128            syntax: ParameterExpansionSyntax::Zsh(syntax),
6129            span: Span::from_positions(part_start, part_end),
6130            raw_body,
6131        })
6132    }
6133
6134    fn parse_zsh_modifier_group(
6135        &self,
6136        text: &str,
6137        base: Position,
6138        start: usize,
6139    ) -> Option<(usize, Vec<ZshModifier>)> {
6140        let rest = text.get(start..)?;
6141        if !rest.starts_with('(') {
6142            return None;
6143        }
6144
6145        let close_rel = rest[1..].find(')')?;
6146        let close = start + 1 + close_rel;
6147        let group_text = &text[start..=close];
6148        let inner = &text[start + 1..close];
6149        let group_start = base.advanced_by(&text[..start]);
6150        let group_span = Span::from_positions(group_start, group_start.advanced_by(group_text));
6151        let mut modifiers = Vec::new();
6152        let mut index = 0usize;
6153
6154        while index < inner.len() {
6155            let name = inner[index..].chars().next()?;
6156            index += name.len_utf8();
6157
6158            let mut argument_delimiter = None;
6159            let mut argument = None;
6160            if matches!(name, 's' | 'j')
6161                && let Some(delimiter) = inner[index..].chars().next()
6162            {
6163                index += delimiter.len_utf8();
6164                let argument_start = index;
6165                while index < inner.len() {
6166                    let ch = inner[index..].chars().next()?;
6167                    if ch == delimiter {
6168                        let argument_text = &inner[argument_start..index];
6169                        let argument_base =
6170                            group_start.advanced_by(&group_text[..1 + argument_start]);
6171                        let argument_end = argument_base.advanced_by(argument_text);
6172                        argument_delimiter = Some(delimiter);
6173                        argument = Some(self.source_text(
6174                            argument_text.to_string(),
6175                            argument_base,
6176                            argument_end,
6177                        ));
6178                        index += delimiter.len_utf8();
6179                        break;
6180                    }
6181                    index += ch.len_utf8();
6182                }
6183            }
6184
6185            let argument_word_ast = argument
6186                .as_ref()
6187                .map(|argument| self.parse_source_text_as_word(argument));
6188
6189            modifiers.push(ZshModifier {
6190                name,
6191                argument,
6192                argument_word_ast,
6193                argument_delimiter,
6194                span: group_span,
6195            });
6196        }
6197
6198        Some((close + 1, modifiers))
6199    }
6200
6201    fn parse_zsh_parameter_syntax(
6202        &mut self,
6203        raw_body: &SourceText,
6204        base: Position,
6205    ) -> ZshParameterExpansion {
6206        let text = raw_body.slice(self.input);
6207        let mut index = 0;
6208        let mut modifiers = Vec::new();
6209        let mut length_prefix = None;
6210        let source_backed = raw_body.is_source_backed();
6211
6212        while text[index..].starts_with('(')
6213            && let Some((next_index, group_modifiers)) =
6214                self.parse_zsh_modifier_group(text, base, index)
6215        {
6216            modifiers.extend(group_modifiers);
6217            index = next_index;
6218        }
6219
6220        while index < text.len() {
6221            let Some(flag) = text[index..].chars().next() else {
6222                break;
6223            };
6224            match flag {
6225                '=' | '~' | '^' => {
6226                    let modifier_start = base.advanced_by(&text[..index]);
6227                    let modifier_end =
6228                        modifier_start.advanced_by(&text[index..index + flag.len_utf8()]);
6229                    modifiers.push(ZshModifier {
6230                        name: flag,
6231                        argument: None,
6232                        argument_word_ast: None,
6233                        argument_delimiter: None,
6234                        span: Span::from_positions(modifier_start, modifier_end),
6235                    });
6236                    index += flag.len_utf8();
6237                }
6238                '#' if length_prefix.is_none() => {
6239                    let prefix_start = base.advanced_by(&text[..index]);
6240                    let prefix_end = prefix_start.advanced_by("#");
6241                    length_prefix = Some(Span::from_positions(prefix_start, prefix_end));
6242                    index += '#'.len_utf8();
6243                }
6244                _ => break,
6245            }
6246        }
6247
6248        let (target, operation_index) = if text[index..].starts_with("${") {
6249            let end = self
6250                .find_matching_parameter_end(&text[index..])
6251                .unwrap_or(text.len() - index);
6252            let nested_text = &text[index..index + end];
6253            let target =
6254                self.parse_nested_parameter_target(nested_text, base.advanced_by(&text[..index]));
6255            (target, index + end)
6256        } else if text[index..].starts_with(':') || text[index..].is_empty() {
6257            (ZshExpansionTarget::Empty, index)
6258        } else {
6259            let end = self
6260                .find_zsh_operation_start(&text[index..])
6261                .map(|offset| index + offset)
6262                .unwrap_or(text.len());
6263            let raw_target = &text[index..end];
6264            let trimmed = raw_target.trim();
6265            let target = if trimmed.is_empty() {
6266                ZshExpansionTarget::Empty
6267            } else {
6268                let leading = raw_target
6269                    .len()
6270                    .saturating_sub(raw_target.trim_start().len());
6271                let target_base = base.advanced_by(&text[..index + leading]);
6272                self.parse_zsh_target_from_text(
6273                    trimmed,
6274                    target_base,
6275                    source_backed && leading == 0 && trimmed.len() == raw_target.len(),
6276                )
6277            };
6278            (target, end)
6279        };
6280
6281        let operation = (operation_index < text.len()).then(|| {
6282            self.parse_zsh_parameter_operation(
6283                &text[operation_index..],
6284                base.advanced_by(&text[..operation_index]),
6285            )
6286        });
6287
6288        ZshParameterExpansion {
6289            target,
6290            modifiers,
6291            length_prefix,
6292            operation,
6293        }
6294    }
6295
6296    fn parse_zsh_target_from_text(
6297        &mut self,
6298        text: &str,
6299        base: Position,
6300        source_backed: bool,
6301    ) -> ZshExpansionTarget {
6302        let trimmed = text.trim();
6303        if trimmed.is_empty() {
6304            return ZshExpansionTarget::Empty;
6305        }
6306
6307        if trimmed.starts_with("${") && trimmed.ends_with('}') {
6308            return self.parse_nested_parameter_target(trimmed, base);
6309        }
6310
6311        if let Some(reference) = self.maybe_parse_loose_var_ref_target(trimmed) {
6312            return ZshExpansionTarget::Reference(reference);
6313        }
6314
6315        let span = Span::from_positions(base, base.advanced_by(trimmed));
6316        let word = self.parse_word_with_context(trimmed, span, base, source_backed);
6317        if let Some(reference) =
6318            self.parse_var_ref_from_word(&word, SubscriptInterpretation::Contextual)
6319        {
6320            ZshExpansionTarget::Reference(reference)
6321        } else {
6322            ZshExpansionTarget::Word(word)
6323        }
6324    }
6325
6326    fn maybe_parse_loose_var_ref_target(&self, text: &str) -> Option<VarRef> {
6327        let trimmed = text.trim();
6328        Self::looks_like_plain_parameter_access(trimmed).then(|| self.parse_loose_var_ref(trimmed))
6329    }
6330
6331    fn is_plain_special_parameter_name(name: &str) -> bool {
6332        matches!(name, "#" | "$" | "!" | "*" | "@" | "?" | "-") || name == "0"
6333    }
6334
6335    fn looks_like_plain_parameter_access(text: &str) -> bool {
6336        let trimmed = text.trim();
6337        if trimmed.is_empty() {
6338            return false;
6339        }
6340
6341        let name = if let Some(open) = trimmed.find('[') {
6342            if !trimmed.ends_with(']') {
6343                return false;
6344            }
6345            &trimmed[..open]
6346        } else {
6347            trimmed
6348        };
6349
6350        Self::is_valid_identifier(name)
6351            || name.bytes().all(|byte| byte.is_ascii_digit())
6352            || Self::is_plain_special_parameter_name(name)
6353    }
6354
6355    fn parse_nested_parameter_target(&mut self, text: &str, base: Position) -> ZshExpansionTarget {
6356        if !(text.starts_with("${") && text.ends_with('}')) {
6357            return self.parse_zsh_target_from_text(text, base, false);
6358        }
6359
6360        let raw_body_start = base.advanced_by("${");
6361        let raw_body = self.source_text(
6362            text[2..text.len() - 1].to_string(),
6363            raw_body_start,
6364            base.advanced_by(&text[..text.len() - 1]),
6365        );
6366        let raw_body_text = raw_body.slice(self.input);
6367        let has_operation = self.find_zsh_operation_start(raw_body_text).is_some();
6368        let syntax = if Self::looks_like_plain_parameter_access(raw_body_text) && !has_operation {
6369            ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access {
6370                reference: self.parse_loose_var_ref(raw_body_text),
6371            })
6372        } else if raw_body_text.starts_with('(')
6373            || raw_body_text.starts_with(':')
6374            || raw_body_text.starts_with('=')
6375            || raw_body_text.starts_with('^')
6376            || raw_body_text.starts_with('~')
6377            || raw_body_text.starts_with('.')
6378            || raw_body_text.starts_with('#')
6379            || raw_body_text.starts_with('"')
6380            || raw_body_text.starts_with('\'')
6381            || raw_body_text.starts_with('$')
6382            || has_operation
6383        {
6384            ParameterExpansionSyntax::Zsh(
6385                self.parse_zsh_parameter_syntax(&raw_body, raw_body_start),
6386            )
6387        } else {
6388            ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access {
6389                reference: self.parse_loose_var_ref(raw_body_text),
6390            })
6391        };
6392
6393        ZshExpansionTarget::Nested(Box::new(ParameterExpansion {
6394            syntax,
6395            span: Span::from_positions(base, base.advanced_by(text)),
6396            raw_body,
6397        }))
6398    }
6399
6400    fn parse_loose_var_ref(&self, text: &str) -> VarRef {
6401        let trimmed = text.trim();
6402        if let Some(open) = trimmed.find('[')
6403            && trimmed.ends_with(']')
6404        {
6405            let name = &trimmed[..open];
6406            let subscript_text = &trimmed[open + 1..trimmed.len() - 1];
6407            let subscript = self.subscript_from_source_text(
6408                SourceText::from(subscript_text.to_string()),
6409                None,
6410                SubscriptInterpretation::Contextual,
6411            );
6412            return VarRef {
6413                name: Name::from(name),
6414                name_span: Span::new(),
6415                subscript: Some(Box::new(subscript)),
6416                span: Span::new(),
6417            };
6418        }
6419
6420        VarRef {
6421            name: Name::from(trimmed),
6422            name_span: Span::new(),
6423            subscript: None,
6424            span: Span::new(),
6425        }
6426    }
6427
6428    fn find_matching_parameter_end(&self, text: &str) -> Option<usize> {
6429        let mut depth = 0_i32;
6430        let mut chars = text.char_indices().peekable();
6431
6432        while let Some((index, ch)) = chars.next() {
6433            match ch {
6434                '$' if chars.peek().is_some_and(|(_, next)| *next == '{') => {
6435                    depth += 1;
6436                }
6437                '}' => {
6438                    depth -= 1;
6439                    if depth == 0 {
6440                        return Some(index + ch.len_utf8());
6441                    }
6442                }
6443                _ => {}
6444            }
6445        }
6446
6447        None
6448    }
6449
6450    fn find_zsh_operation_start(&self, text: &str) -> Option<usize> {
6451        let mut bracket_depth = 0_usize;
6452        let mut in_single = false;
6453        let mut in_double = false;
6454        let mut escaped = false;
6455
6456        for (index, ch) in text.char_indices() {
6457            if escaped {
6458                escaped = false;
6459                continue;
6460            }
6461
6462            match ch {
6463                '\\' if !in_single => escaped = true,
6464                '\'' if !in_double => in_single = !in_single,
6465                '"' if !in_single => in_double = !in_double,
6466                '[' if !in_single && !in_double => bracket_depth += 1,
6467                ']' if !in_single && !in_double && bracket_depth > 0 => bracket_depth -= 1,
6468                ':' if !in_single && !in_double && bracket_depth == 0 => return Some(index),
6469                '#' | '%' | '/' | '^' | ',' | '~'
6470                    if !in_single && !in_double && bracket_depth == 0 && index > 0 =>
6471                {
6472                    return Some(index);
6473                }
6474                _ => {}
6475            }
6476        }
6477
6478        None
6479    }
6480
6481    fn zsh_operation_source_text(
6482        &self,
6483        text: &str,
6484        base: Position,
6485        start: usize,
6486        end: usize,
6487    ) -> SourceText {
6488        self.source_text(
6489            text[start..end].to_string(),
6490            base.advanced_by(&text[..start]),
6491            base.advanced_by(&text[..end]),
6492        )
6493    }
6494
6495    fn find_zsh_top_level_delimiter(&self, text: &str, delimiter: char) -> Option<usize> {
6496        let mut chars = text.char_indices().peekable();
6497        let mut in_single = false;
6498        let mut in_double = false;
6499        let mut escaped = false;
6500        let mut brace_depth = 0_usize;
6501        let mut paren_depth = 0_usize;
6502
6503        while let Some((index, ch)) = chars.next() {
6504            if escaped {
6505                escaped = false;
6506                continue;
6507            }
6508
6509            match ch {
6510                '\\' if !in_single => escaped = true,
6511                '\'' if !in_double => in_single = !in_single,
6512                '"' if !in_single => in_double = !in_double,
6513                '$' if !in_single => {
6514                    if let Some((_, next)) = chars.peek() {
6515                        if *next == '{' {
6516                            brace_depth += 1;
6517                            chars.next();
6518                        } else if *next == '(' {
6519                            paren_depth += 1;
6520                            chars.next();
6521                            if let Some((_, after)) = chars.peek()
6522                                && *after == '('
6523                            {
6524                                paren_depth += 1;
6525                                chars.next();
6526                            }
6527                        }
6528                    }
6529                }
6530                '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
6531                ')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
6532                _ if ch == delimiter
6533                    && !in_single
6534                    && !in_double
6535                    && brace_depth == 0
6536                    && paren_depth == 0 =>
6537                {
6538                    return Some(index);
6539                }
6540                _ => {}
6541            }
6542        }
6543
6544        None
6545    }
6546
6547    fn zsh_simple_modifier_suffix_segment(segment: &str) -> bool {
6548        let mut chars = segment.chars();
6549        let Some(first) = chars.next() else {
6550            return false;
6551        };
6552
6553        match first {
6554            'a' | 'A' | 'c' | 'e' | 'l' | 'P' | 'q' | 'Q' | 'r' | 'u' => chars.next().is_none(),
6555            'h' | 't' => chars.all(|ch| ch.is_ascii_digit()),
6556            _ => false,
6557        }
6558    }
6559
6560    fn zsh_modifier_suffix_candidate(rest: &str) -> bool {
6561        if rest.is_empty() {
6562            return false;
6563        }
6564
6565        let Some(first) = rest.chars().next() else {
6566            return false;
6567        };
6568        if first.is_ascii_digit()
6569            || first.is_ascii_whitespace()
6570            || matches!(first, '$' | '\'' | '"' | '(' | '{')
6571        {
6572            return false;
6573        }
6574
6575        rest.split(':')
6576            .all(Self::zsh_simple_modifier_suffix_segment)
6577    }
6578
6579    fn zsh_slice_candidate(rest: &str) -> bool {
6580        let Some(first) = rest.chars().next() else {
6581            return false;
6582        };
6583
6584        !Self::zsh_modifier_suffix_candidate(rest)
6585            && (first.is_ascii_alphanumeric()
6586                || first == '_'
6587                || first.is_ascii_whitespace()
6588                || matches!(first, '$' | '\'' | '"' | '(' | '{'))
6589    }
6590
6591    fn parse_zsh_parameter_operation(&self, text: &str, base: Position) -> ZshExpansionOperation {
6592        if let Some(operand) = text.strip_prefix(":#") {
6593            let operand = self.source_text(
6594                operand.to_string(),
6595                base.advanced_by(":#"),
6596                base.advanced_by(text),
6597            );
6598            return ZshExpansionOperation::PatternOperation {
6599                kind: ZshPatternOp::Filter,
6600                operand_word_ast: self.parse_source_text_as_word(&operand),
6601                operand,
6602            };
6603        }
6604
6605        if let Some((kind, operand)) = text
6606            .strip_prefix(":-")
6607            .map(|operand| (ZshDefaultingOp::UseDefault, operand))
6608            .or_else(|| {
6609                text.strip_prefix(":=")
6610                    .map(|operand| (ZshDefaultingOp::AssignDefault, operand))
6611            })
6612            .or_else(|| {
6613                text.strip_prefix(":+")
6614                    .map(|operand| (ZshDefaultingOp::UseReplacement, operand))
6615            })
6616            .or_else(|| {
6617                text.strip_prefix(":?")
6618                    .map(|operand| (ZshDefaultingOp::Error, operand))
6619            })
6620        {
6621            let operand = self.source_text(
6622                operand.to_string(),
6623                base.advanced_by(&text[..2]),
6624                base.advanced_by(text),
6625            );
6626            return ZshExpansionOperation::Defaulting {
6627                kind,
6628                operand_word_ast: self.parse_source_text_as_word(&operand),
6629                operand,
6630                colon_variant: true,
6631            };
6632        }
6633
6634        if let Some((kind, prefix_len)) = [
6635            ("##", ZshTrimOp::RemovePrefixLong),
6636            ("#", ZshTrimOp::RemovePrefixShort),
6637            ("%%", ZshTrimOp::RemoveSuffixLong),
6638            ("%", ZshTrimOp::RemoveSuffixShort),
6639        ]
6640        .into_iter()
6641        .find_map(|(prefix, kind)| text.starts_with(prefix).then_some((kind, prefix.len())))
6642        {
6643            let operand = self.zsh_operation_source_text(text, base, prefix_len, text.len());
6644            return ZshExpansionOperation::TrimOperation {
6645                kind,
6646                operand_word_ast: self.parse_source_text_as_word(&operand),
6647                operand,
6648            };
6649        }
6650
6651        if let Some((kind, prefix_len)) = [
6652            ("//", ZshReplacementOp::ReplaceAll),
6653            ("/#", ZshReplacementOp::ReplacePrefix),
6654            ("/%", ZshReplacementOp::ReplaceSuffix),
6655            ("/", ZshReplacementOp::ReplaceFirst),
6656        ]
6657        .into_iter()
6658        .find_map(|(prefix, kind)| text.starts_with(prefix).then_some((kind, prefix.len())))
6659        {
6660            let rest = &text[prefix_len..];
6661            let separator = self.find_zsh_top_level_delimiter(rest, '/');
6662            let pattern_end = separator.unwrap_or(rest.len());
6663            let pattern =
6664                self.zsh_operation_source_text(text, base, prefix_len, prefix_len + pattern_end);
6665            let replacement = separator.map(|separator| {
6666                self.zsh_operation_source_text(text, base, prefix_len + separator + 1, text.len())
6667            });
6668            return ZshExpansionOperation::ReplacementOperation {
6669                kind,
6670                pattern_word_ast: self.parse_source_text_as_word(&pattern),
6671                replacement_word_ast: self.parse_optional_source_text_as_word(replacement.as_ref()),
6672                pattern,
6673                replacement,
6674            };
6675        }
6676
6677        if let Some(rest) = text.strip_prefix(':') {
6678            if Self::zsh_modifier_suffix_candidate(rest) {
6679                let text = self.source_text(text.to_string(), base, base.advanced_by(text));
6680                return ZshExpansionOperation::Unknown {
6681                    word_ast: self.parse_source_text_as_word(&text),
6682                    text,
6683                };
6684            }
6685
6686            if Self::zsh_slice_candidate(rest) {
6687                let separator = self.find_zsh_top_level_delimiter(rest, ':');
6688                let offset_end = separator.unwrap_or(rest.len());
6689                let offset = self.zsh_operation_source_text(text, base, 1, 1 + offset_end);
6690                let length = separator.map(|separator| {
6691                    self.zsh_operation_source_text(text, base, 1 + separator + 1, text.len())
6692                });
6693                return ZshExpansionOperation::Slice {
6694                    offset_word_ast: self.parse_source_text_as_word(&offset),
6695                    length_word_ast: self.parse_optional_source_text_as_word(length.as_ref()),
6696                    offset,
6697                    length,
6698                };
6699            }
6700        }
6701
6702        let text = self.source_text(text.to_string(), base, base.advanced_by(text));
6703        ZshExpansionOperation::Unknown {
6704            word_ast: self.parse_source_text_as_word(&text),
6705            text,
6706        }
6707    }
6708
6709    fn parse_explicit_arithmetic_span(
6710        &self,
6711        span: Option<Span>,
6712        context: &'static str,
6713    ) -> Result<Option<ArithmeticExprNode>> {
6714        let Some(span) = span else {
6715            return Ok(None);
6716        };
6717        if span.slice(self.input).trim().is_empty() {
6718            return Ok(None);
6719        }
6720        arithmetic::parse_expression(
6721            span.slice(self.input),
6722            span,
6723            self.dialect,
6724            self.max_depth.saturating_sub(self.current_depth),
6725            self.fuel,
6726        )
6727        .map(Some)
6728        .map_err(|error| match error {
6729            Error::Parse { message, .. } => self.error(format!("{context}: {message}")),
6730        })
6731    }
6732
6733    fn parse_source_text_as_arithmetic(&self, text: &SourceText) -> Result<ArithmeticExprNode> {
6734        arithmetic::parse_expression(
6735            text.slice(self.input),
6736            text.span(),
6737            self.dialect,
6738            self.max_depth.saturating_sub(self.current_depth),
6739            self.fuel,
6740        )
6741    }
6742
6743    fn maybe_parse_source_text_as_arithmetic(
6744        &self,
6745        text: &SourceText,
6746    ) -> Option<ArithmeticExprNode> {
6747        if !text.is_source_backed() {
6748            return None;
6749        }
6750        self.parse_source_text_as_arithmetic(text).ok()
6751    }
6752
6753    fn parse_source_text_as_word(&self, text: &SourceText) -> Word {
6754        if let Some(word) = self.simple_source_text_as_word(text) {
6755            return word;
6756        }
6757
6758        let span = text.span();
6759        if !text.is_source_backed()
6760            && span.start.offset <= span.end.offset
6761            && span.end.offset <= self.input.len()
6762        {
6763            let raw = span.slice(self.input);
6764            if raw.contains("\\\"") {
6765                return Self::parse_word_fragment(self.input, raw, span);
6766            }
6767        }
6768
6769        Self::parse_word_fragment(self.input, text.slice(self.input), text.span())
6770    }
6771
6772    fn simple_source_text_as_word(&self, text: &SourceText) -> Option<Word> {
6773        if !text.is_source_backed() {
6774            return None;
6775        }
6776
6777        let span = text.span();
6778        let raw = text.slice(self.input);
6779        if raw.is_empty() {
6780            return Some(Word::literal_with_span("", span));
6781        }
6782
6783        if let Some(word) = self.simple_quoted_source_text_as_word(raw, span) {
6784            return Some(word);
6785        }
6786
6787        if Self::word_text_needs_parse(raw)
6788            || raw.contains(['\'', '"', '\\'])
6789            || self.zsh_glob_qualifiers_enabled_at(span.start.offset)
6790        {
6791            return None;
6792        }
6793
6794        Some(self.word_with_single_part(self.literal_part_from_text(raw, span, true), span))
6795    }
6796
6797    fn simple_quoted_source_text_as_word(&self, raw: &str, span: Span) -> Option<Word> {
6798        if raw.len() < 2 {
6799            return None;
6800        }
6801
6802        let quote = raw.as_bytes()[0];
6803        if quote != b'\'' && quote != b'"' || raw.as_bytes().last().copied() != Some(quote) {
6804            return None;
6805        }
6806
6807        let inner = &raw[1..raw.len() - 1];
6808        if quote == b'\'' && inner.contains('\'') {
6809            return None;
6810        }
6811
6812        let inner_start = span.start.advanced_by(&raw[..1]);
6813        let inner_end = inner_start.advanced_by(inner);
6814        let inner_span = Span::from_positions(inner_start, inner_end);
6815        let part = match quote {
6816            b'\'' => self.single_quoted_part_from_text(inner, inner_span, span, false),
6817            b'"' => {
6818                if Self::word_text_needs_parse(inner) || inner.contains(['\\', '"']) {
6819                    return None;
6820                }
6821                self.double_quoted_literal_part_from_text(inner, inner_span, span, true, false)
6822            }
6823            _ => unreachable!("quote is checked above"),
6824        };
6825        Some(self.word_with_single_part(part, span))
6826    }
6827
6828    fn parse_optional_source_text_as_word(&self, text: Option<&SourceText>) -> Option<Word> {
6829        text.map(|text| self.parse_source_text_as_word(text))
6830    }
6831
6832    fn source_matches(&self, span: Span, text: &str) -> bool {
6833        span.start.offset <= span.end.offset
6834            && self
6835                .input
6836                .get(span.start.offset..span.end.offset)
6837                .is_some_and(|slice| slice == text)
6838    }
6839
6840    fn checkpoint(&self) -> ParserCheckpoint<'a> {
6841        ParserCheckpoint {
6842            lexer: self.lexer.clone(),
6843            synthetic_tokens: self.synthetic_tokens.clone(),
6844            alias_replays: self.alias_replays.clone(),
6845            current_token: self.current_token.clone(),
6846            current_token_kind: self.current_token_kind,
6847            current_keyword: self.current_keyword,
6848            current_span: self.current_span,
6849            peeked_token: self.peeked_token.clone(),
6850            current_depth: self.current_depth,
6851            fuel: self.fuel,
6852            comments: self.comments.clone(),
6853            expand_next_word: self.expand_next_word,
6854            brace_group_depth: self.brace_group_depth,
6855            brace_body_stack: self.brace_body_stack.clone(),
6856            syntax_facts: self.syntax_facts.clone(),
6857            #[cfg(feature = "benchmarking")]
6858            benchmark_counters: self.benchmark_counters,
6859        }
6860    }
6861
6862    fn restore(&mut self, checkpoint: ParserCheckpoint<'a>) {
6863        self.lexer = checkpoint.lexer;
6864        self.synthetic_tokens = checkpoint.synthetic_tokens;
6865        self.alias_replays = checkpoint.alias_replays;
6866        self.current_token = checkpoint.current_token;
6867        // This is a cache over current_token/current_span, so rebuilding it is equivalent.
6868        self.current_word_cache = None;
6869        self.current_token_kind = checkpoint.current_token_kind;
6870        self.current_keyword = checkpoint.current_keyword;
6871        self.current_span = checkpoint.current_span;
6872        self.peeked_token = checkpoint.peeked_token;
6873        self.current_depth = checkpoint.current_depth;
6874        self.fuel = checkpoint.fuel;
6875        self.comments = checkpoint.comments;
6876        self.expand_next_word = checkpoint.expand_next_word;
6877        self.brace_group_depth = checkpoint.brace_group_depth;
6878        self.brace_body_stack = checkpoint.brace_body_stack;
6879        self.syntax_facts = checkpoint.syntax_facts;
6880        #[cfg(feature = "benchmarking")]
6881        {
6882            self.benchmark_counters = checkpoint.benchmark_counters;
6883        }
6884    }
6885
6886    fn set_current_spanned(&mut self, token: LexedToken<'a>) {
6887        #[cfg(feature = "benchmarking")]
6888        self.maybe_record_set_current_spanned_call();
6889        let span = token.span;
6890        self.current_token_kind = Some(token.kind);
6891        self.current_keyword = Self::keyword_from_token(&token);
6892        self.current_token = Some(token);
6893        self.current_word_cache = None;
6894        self.current_span = span;
6895    }
6896
6897    fn set_current_kind(&mut self, kind: TokenKind, span: Span) {
6898        self.current_token_kind = Some(kind);
6899        self.current_keyword = None;
6900        self.current_token = Some(LexedToken::punctuation(kind).with_span(span));
6901        self.current_word_cache = None;
6902        self.current_span = span;
6903    }
6904
6905    fn clear_current_token(&mut self) {
6906        self.current_token = None;
6907        self.current_word_cache = None;
6908        self.current_token_kind = None;
6909        self.current_keyword = None;
6910    }
6911
6912    fn next_pending_token(&mut self) -> Option<LexedToken<'a>> {
6913        if let Some(token) = self.synthetic_tokens.pop_front() {
6914            return Some(token.materialize());
6915        }
6916
6917        loop {
6918            let replay = self.alias_replays.last_mut()?;
6919            if let Some(token) = replay.next_token() {
6920                return Some(token);
6921            }
6922            self.alias_replays.pop();
6923        }
6924    }
6925
6926    fn next_spanned_token_with_comments(&mut self) -> Option<LexedToken<'a>> {
6927        self.next_pending_token()
6928            .or_else(|| self.lexer.next_lexed_token_with_comments())
6929    }
6930
6931    fn compile_alias_definition(&self, value: &str) -> AliasDefinition {
6932        let source = Arc::<str>::from(value.to_string());
6933        let mut lexer = Lexer::with_max_subst_depth(source.as_ref(), self.max_depth);
6934        let mut tokens = Vec::new();
6935
6936        while let Some(token) = lexer.next_lexed_token_with_comments() {
6937            tokens.push(token.into_shared(&source));
6938        }
6939
6940        AliasDefinition {
6941            tokens: tokens.into(),
6942            expands_next_word: value.chars().last().is_some_and(char::is_whitespace),
6943        }
6944    }
6945
6946    fn maybe_expand_current_alias_chain(&mut self) {
6947        if !self.expand_aliases {
6948            self.expand_next_word = false;
6949            return;
6950        }
6951
6952        let mut seen = HashSet::new();
6953        let mut expands_next_word = false;
6954
6955        loop {
6956            if self.current_token_kind != Some(TokenKind::Word) {
6957                break;
6958            }
6959            let Some(name) = self.current_token.as_ref().and_then(LexedToken::word_text) else {
6960                break;
6961            };
6962            if self.current_source_word_starts_posix_function_header(name) {
6963                break;
6964            }
6965            let Some(alias) = self.aliases.get(name).cloned() else {
6966                break;
6967            };
6968            if !seen.insert(name.to_string()) {
6969                break;
6970            }
6971
6972            expands_next_word = alias.expands_next_word;
6973            self.peeked_token = None;
6974            self.alias_replays
6975                .push(AliasReplay::new(&alias, self.current_span.start));
6976            self.advance_raw();
6977        }
6978
6979        self.expand_next_word = expands_next_word;
6980    }
6981
6982    fn current_source_word_starts_posix_function_header(&self, name: &str) -> bool {
6983        if name.contains('=') || name.contains('[') {
6984            return false;
6985        }
6986
6987        if self
6988            .current_token
6989            .as_ref()
6990            .is_some_and(|token| token.flags.is_synthetic())
6991        {
6992            return false;
6993        }
6994
6995        let Some(tail) = self.input.get(self.current_span.end.offset..) else {
6996            return false;
6997        };
6998        let tail = tail.trim_start_matches([' ', '\t']);
6999        let Some(after_left) = tail.strip_prefix('(') else {
7000            return false;
7001        };
7002        let after_left = after_left.trim_start_matches([' ', '\t']);
7003        after_left.starts_with(')')
7004    }
7005
7006    fn next_word_char(
7007        chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
7008        cursor: &mut Position,
7009    ) -> Option<char> {
7010        let ch = chars.next()?;
7011        cursor.advance(ch);
7012        Some(ch)
7013    }
7014
7015    fn next_word_char_unwrap(
7016        chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
7017        cursor: &mut Position,
7018    ) -> char {
7019        let Some(ch) = Self::next_word_char(chars, cursor) else {
7020            unreachable!("word parser should only consume characters that were already peeked");
7021        };
7022        ch
7023    }
7024
7025    fn consume_word_char_if(
7026        chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
7027        cursor: &mut Position,
7028        expected: char,
7029    ) -> bool {
7030        if chars.peek() == Some(&expected) {
7031            Self::next_word_char_unwrap(chars, cursor);
7032            true
7033        } else {
7034            false
7035        }
7036    }
7037
7038    fn read_word_while<F>(
7039        chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
7040        cursor: &mut Position,
7041        mut predicate: F,
7042    ) -> String
7043    where
7044        F: FnMut(char) -> bool,
7045    {
7046        let mut text = String::new();
7047        while let Some(&ch) = chars.peek() {
7048            if !predicate(ch) {
7049                break;
7050            }
7051            text.push(Self::next_word_char_unwrap(chars, cursor));
7052        }
7053        text
7054    }
7055
7056    fn rebase_redirects(redirects: &mut [Redirect], base: Position) {
7057        for redirect in redirects {
7058            redirect.span = redirect.span.rebased(base);
7059            redirect.fd_var_span = redirect.fd_var_span.map(|span| span.rebased(base));
7060            match &mut redirect.target {
7061                RedirectTarget::Word(word) => Self::rebase_word(word, base),
7062                RedirectTarget::Heredoc(heredoc) => {
7063                    heredoc.delimiter.span = heredoc.delimiter.span.rebased(base);
7064                    Self::rebase_word(&mut heredoc.delimiter.raw, base);
7065                    Self::rebase_heredoc_body(&mut heredoc.body, base);
7066                }
7067            }
7068        }
7069    }
7070
7071    fn rebase_assignments(assignments: &mut [Assignment], base: Position) {
7072        for assignment in assignments {
7073            assignment.span = assignment.span.rebased(base);
7074            Self::rebase_var_ref(&mut assignment.target, base);
7075            match &mut assignment.value {
7076                AssignmentValue::Scalar(word) => Self::rebase_word(word, base),
7077                AssignmentValue::Compound(array) => Self::rebase_array_expr(array, base),
7078            }
7079        }
7080    }
7081
7082    /// Create a parse error with the current position.
7083    fn error(&self, message: impl Into<String>) -> Error {
7084        Error::parse_at(
7085            message,
7086            self.current_span.start.line,
7087            self.current_span.start.column,
7088        )
7089    }
7090
7091    fn ensure_feature(
7092        &self,
7093        enabled: bool,
7094        feature: &str,
7095        unsupported_message: &str,
7096    ) -> Result<()> {
7097        if enabled {
7098            Ok(())
7099        } else {
7100            Err(self.error(format!("{feature} {unsupported_message}")))
7101        }
7102    }
7103
7104    fn ensure_double_bracket(&self) -> Result<()> {
7105        self.ensure_feature(
7106            self.dialect.features().double_bracket,
7107            "[[ ]] conditionals",
7108            "are not available in this shell mode",
7109        )
7110    }
7111
7112    fn ensure_arithmetic_for(&self) -> Result<()> {
7113        self.ensure_feature(
7114            self.dialect.features().arithmetic_for,
7115            "c-style for loops",
7116            "are not available in this shell mode",
7117        )
7118    }
7119
7120    fn ensure_coproc(&self) -> Result<()> {
7121        self.ensure_feature(
7122            self.dialect.features().coproc_keyword,
7123            "coprocess commands",
7124            "are not available in this shell mode",
7125        )
7126    }
7127
7128    fn ensure_arithmetic_command(&self) -> Result<()> {
7129        self.ensure_feature(
7130            self.dialect.features().arithmetic_command,
7131            "arithmetic commands",
7132            "are not available in this shell mode",
7133        )
7134    }
7135
7136    fn ensure_select_loop(&self) -> Result<()> {
7137        self.ensure_feature(
7138            self.dialect.features().select_loop,
7139            "select loops",
7140            "are not available in this shell mode",
7141        )
7142    }
7143
7144    fn ensure_repeat_loop(&self) -> Result<()> {
7145        self.ensure_feature(
7146            self.zsh_short_repeat_enabled(),
7147            "repeat loops",
7148            "are not available in this shell mode",
7149        )
7150    }
7151
7152    fn ensure_foreach_loop(&self) -> Result<()> {
7153        self.ensure_feature(
7154            self.zsh_short_loops_enabled(),
7155            "foreach loops",
7156            "are not available in this shell mode",
7157        )
7158    }
7159
7160    fn ensure_function_keyword(&self) -> Result<()> {
7161        self.ensure_feature(
7162            self.dialect.features().function_keyword,
7163            "function keyword definitions",
7164            "are not available in this shell mode",
7165        )
7166    }
7167
7168    /// Consume one unit of fuel, returning an error if exhausted
7169    fn tick(&mut self) -> Result<()> {
7170        if self.fuel == 0 {
7171            let used = self.max_fuel - self.fuel;
7172            return Err(Error::parse(format!(
7173                "parser fuel exhausted ({} operations, max {})",
7174                used, self.max_fuel
7175            )));
7176        }
7177        self.fuel -= 1;
7178        Ok(())
7179    }
7180
7181    /// Push nesting depth and check limit
7182    fn push_depth(&mut self) -> Result<()> {
7183        self.current_depth += 1;
7184        if self.current_depth > self.max_depth {
7185            return Err(Error::parse(format!(
7186                "AST nesting too deep ({} levels, max {})",
7187                self.current_depth, self.max_depth
7188            )));
7189        }
7190        Ok(())
7191    }
7192
7193    /// Pop nesting depth
7194    fn pop_depth(&mut self) {
7195        if self.current_depth > 0 {
7196            self.current_depth -= 1;
7197        }
7198    }
7199
7200    /// Check if current token is an error token and return the error if so
7201    fn check_error_token(&self) -> Result<()> {
7202        if self.current_token_kind == Some(TokenKind::Error) {
7203            let msg = self
7204                .current_token
7205                .as_ref()
7206                .and_then(LexedToken::error_kind)
7207                .map(|kind| kind.message())
7208                .unwrap_or("unknown lexer error");
7209            return Err(self.error(format!("syntax error: {}", msg)));
7210        }
7211        Ok(())
7212    }
7213
7214    fn parse_diagnostic_from_error(&self, error: Error) -> ParseDiagnostic {
7215        let Error::Parse { message, .. } = error;
7216        ParseDiagnostic {
7217            message,
7218            span: self.current_span,
7219        }
7220    }
7221
7222    fn compound_span(compound: &CompoundCommand) -> Span {
7223        match compound {
7224            CompoundCommand::If(command) => command.span,
7225            CompoundCommand::For(command) => command.span,
7226            CompoundCommand::Repeat(command) => command.span,
7227            CompoundCommand::Foreach(command) => command.span,
7228            CompoundCommand::ArithmeticFor(command) => command.span,
7229            CompoundCommand::While(command) => command.span,
7230            CompoundCommand::Until(command) => command.span,
7231            CompoundCommand::Case(command) => command.span,
7232            CompoundCommand::Select(command) => command.span,
7233            CompoundCommand::Subshell(body) | CompoundCommand::BraceGroup(body) => body.span,
7234            CompoundCommand::Arithmetic(command) => command.span,
7235            CompoundCommand::Time(command) => command.span,
7236            CompoundCommand::Conditional(command) => command.span,
7237            CompoundCommand::Coproc(command) => command.span,
7238            CompoundCommand::Always(command) => command.span,
7239        }
7240    }
7241
7242    fn stmt_seq_with_span(span: Span, stmts: Vec<Stmt>) -> StmtSeq {
7243        StmtSeq {
7244            leading_comments: Vec::new(),
7245            stmts,
7246            trailing_comments: Vec::new(),
7247            span,
7248        }
7249    }
7250
7251    fn binary_stmt(left: Stmt, op: BinaryOp, op_span: Span, right: Stmt) -> Stmt {
7252        let span = left.span.merge(right.span);
7253        Stmt {
7254            leading_comments: Vec::new(),
7255            command: AstCommand::Binary(BinaryCommand {
7256                left: Box::new(left),
7257                op,
7258                op_span,
7259                right: Box::new(right),
7260                span,
7261            }),
7262            negated: false,
7263            redirects: Box::default(),
7264            terminator: None,
7265            terminator_span: None,
7266            inline_comment: None,
7267            span,
7268        }
7269    }
7270
7271    fn lower_builtin_command(
7272        builtin: BuiltinCommand,
7273    ) -> (AstBuiltinCommand, SmallVec<[Redirect; 1]>, Span) {
7274        match builtin {
7275            BuiltinCommand::Break(command) => {
7276                let span = command.span;
7277                let redirects = command.redirects;
7278                (
7279                    AstBuiltinCommand::Break(AstBreakCommand {
7280                        depth: command.depth,
7281                        extra_args: command.extra_args.into_vec(),
7282                        assignments: command.assignments.into_boxed_slice(),
7283                        span,
7284                    }),
7285                    redirects,
7286                    span,
7287                )
7288            }
7289            BuiltinCommand::Continue(command) => {
7290                let span = command.span;
7291                let redirects = command.redirects;
7292                (
7293                    AstBuiltinCommand::Continue(AstContinueCommand {
7294                        depth: command.depth,
7295                        extra_args: command.extra_args.into_vec(),
7296                        assignments: command.assignments.into_boxed_slice(),
7297                        span,
7298                    }),
7299                    redirects,
7300                    span,
7301                )
7302            }
7303            BuiltinCommand::Return(command) => {
7304                let span = command.span;
7305                let redirects = command.redirects;
7306                (
7307                    AstBuiltinCommand::Return(AstReturnCommand {
7308                        code: command.code,
7309                        extra_args: command.extra_args.into_vec(),
7310                        assignments: command.assignments.into_boxed_slice(),
7311                        span,
7312                    }),
7313                    redirects,
7314                    span,
7315                )
7316            }
7317            BuiltinCommand::Exit(command) => {
7318                let span = command.span;
7319                let redirects = command.redirects;
7320                (
7321                    AstBuiltinCommand::Exit(AstExitCommand {
7322                        code: command.code,
7323                        extra_args: command.extra_args.into_vec(),
7324                        assignments: command.assignments.into_boxed_slice(),
7325                        span,
7326                    }),
7327                    redirects,
7328                    span,
7329                )
7330            }
7331        }
7332    }
7333
7334    fn lower_non_sequence_command_to_stmt(command: Command) -> Stmt {
7335        match command {
7336            Command::Simple(command) => Stmt {
7337                leading_comments: Vec::new(),
7338                command: AstCommand::Simple(AstSimpleCommand {
7339                    name: command.name,
7340                    args: command.args.into_vec(),
7341                    assignments: command.assignments.into_boxed_slice(),
7342                    span: command.span,
7343                }),
7344                negated: false,
7345                redirects: command.redirects.into_boxed_slice(),
7346                terminator: None,
7347                terminator_span: None,
7348                inline_comment: None,
7349                span: command.span,
7350            },
7351            Command::Builtin(command) => {
7352                let (command, redirects, span) = Self::lower_builtin_command(command);
7353                Stmt {
7354                    leading_comments: Vec::new(),
7355                    command: AstCommand::Builtin(command),
7356                    negated: false,
7357                    redirects: redirects.into_boxed_slice(),
7358                    terminator: None,
7359                    terminator_span: None,
7360                    inline_comment: None,
7361                    span,
7362                }
7363            }
7364            Command::Decl(command) => {
7365                let command = *command;
7366                Stmt {
7367                    leading_comments: Vec::new(),
7368                    command: AstCommand::Decl(AstDeclClause {
7369                        variant: command.variant,
7370                        variant_span: command.variant_span,
7371                        operands: command.operands.into_vec(),
7372                        assignments: command.assignments.into_boxed_slice(),
7373                        span: command.span,
7374                    }),
7375                    negated: false,
7376                    redirects: command.redirects.into_boxed_slice(),
7377                    terminator: None,
7378                    terminator_span: None,
7379                    inline_comment: None,
7380                    span: command.span,
7381                }
7382            }
7383            Command::Compound(compound, redirects) => {
7384                let span = Self::compound_span(&compound);
7385                Stmt {
7386                    leading_comments: Vec::new(),
7387                    command: AstCommand::Compound(*compound),
7388                    negated: false,
7389                    redirects: redirects.into_boxed_slice(),
7390                    terminator: None,
7391                    terminator_span: None,
7392                    inline_comment: None,
7393                    span,
7394                }
7395            }
7396            Command::Function(function) => Stmt {
7397                leading_comments: Vec::new(),
7398                span: function.span,
7399                command: AstCommand::Function(function),
7400                negated: false,
7401                redirects: Box::default(),
7402                terminator: None,
7403                terminator_span: None,
7404                inline_comment: None,
7405            },
7406            Command::AnonymousFunction(function, redirects) => Stmt {
7407                leading_comments: Vec::new(),
7408                span: function.span,
7409                command: AstCommand::AnonymousFunction(function),
7410                negated: false,
7411                redirects: redirects.into_boxed_slice(),
7412                terminator: None,
7413                terminator_span: None,
7414                inline_comment: None,
7415            },
7416        }
7417    }
7418
7419    fn comment_start(comment: Comment) -> usize {
7420        usize::from(comment.range.start())
7421    }
7422
7423    fn is_inline_comment(source: &str, stmt: &Stmt, comment: Comment) -> bool {
7424        let comment_start = Self::comment_start(comment);
7425        if comment_start < stmt.span.end.offset {
7426            return false;
7427        }
7428        source
7429            .get(stmt.span.end.offset..comment_start)
7430            .is_some_and(|gap| !gap.contains('\n'))
7431    }
7432
7433    fn take_comments_before(
7434        comments: &mut VecDeque<Comment>,
7435        end_offset: usize,
7436    ) -> VecDeque<Comment> {
7437        let mut taken = VecDeque::new();
7438        while comments
7439            .front()
7440            .is_some_and(|comment| Self::comment_start(*comment) < end_offset)
7441        {
7442            let Some(comment) = comments.pop_front() else {
7443                unreachable!("front comment should exist while draining");
7444            };
7445            taken.push_back(comment);
7446        }
7447        taken
7448    }
7449
7450    fn attach_comments_to_file(&self, file: &mut File) {
7451        let mut comments = self.comments.iter().copied().collect::<VecDeque<_>>();
7452        Self::attach_comments_to_stmt_seq_with_source(self.input, &mut file.body, &mut comments);
7453        file.body.trailing_comments.extend(comments);
7454    }
7455
7456    fn attach_comments_to_stmt_seq_with_source(
7457        source: &str,
7458        sequence: &mut StmtSeq,
7459        comments: &mut VecDeque<Comment>,
7460    ) {
7461        if sequence.stmts.is_empty() {
7462            sequence
7463                .trailing_comments
7464                .extend(Self::take_comments_before(
7465                    comments,
7466                    sequence.span.end.offset,
7467                ));
7468            return;
7469        }
7470
7471        for (index, stmt) in sequence.stmts.iter_mut().enumerate() {
7472            let leading = Self::take_comments_before(comments, stmt.span.start.offset);
7473            if index == 0 {
7474                sequence.leading_comments.extend(leading);
7475            } else {
7476                stmt.leading_comments.extend(leading);
7477            }
7478
7479            let mut nested = Self::take_comments_before(comments, stmt.span.end.offset);
7480            Self::attach_comments_to_stmt_with_source(source, stmt, &mut nested);
7481            if !nested.is_empty() {
7482                stmt.leading_comments.extend(nested);
7483            }
7484
7485            if stmt.inline_comment.is_none()
7486                && comments
7487                    .front()
7488                    .is_some_and(|comment| Self::is_inline_comment(source, stmt, *comment))
7489            {
7490                stmt.inline_comment = comments.pop_front();
7491            }
7492        }
7493
7494        sequence
7495            .trailing_comments
7496            .extend(Self::take_comments_before(
7497                comments,
7498                sequence.span.end.offset,
7499            ));
7500    }
7501
7502    fn attach_comments_to_stmt_with_source(
7503        source: &str,
7504        stmt: &mut Stmt,
7505        comments: &mut VecDeque<Comment>,
7506    ) {
7507        match &mut stmt.command {
7508            AstCommand::Binary(binary) => {
7509                let mut left_comments =
7510                    Self::take_comments_before(comments, binary.left.span.end.offset);
7511                Self::attach_comments_to_stmt_with_source(
7512                    source,
7513                    binary.left.as_mut(),
7514                    &mut left_comments,
7515                );
7516                if !left_comments.is_empty() {
7517                    binary.left.leading_comments.extend(left_comments);
7518                }
7519
7520                let mut right_comments = std::mem::take(comments);
7521                Self::attach_comments_to_stmt_with_source(
7522                    source,
7523                    binary.right.as_mut(),
7524                    &mut right_comments,
7525                );
7526                if !right_comments.is_empty() {
7527                    binary.right.leading_comments.extend(right_comments);
7528                }
7529            }
7530            AstCommand::Compound(compound) => {
7531                Self::attach_comments_to_compound_with_source(source, compound, comments);
7532            }
7533            AstCommand::Function(function) => {
7534                let mut body_comments = std::mem::take(comments);
7535                Self::attach_comments_to_stmt_with_source(
7536                    source,
7537                    function.body.as_mut(),
7538                    &mut body_comments,
7539                );
7540                if !body_comments.is_empty() {
7541                    function.body.leading_comments.extend(body_comments);
7542                }
7543            }
7544            AstCommand::AnonymousFunction(function) => {
7545                let mut body_comments = std::mem::take(comments);
7546                Self::attach_comments_to_stmt_with_source(
7547                    source,
7548                    function.body.as_mut(),
7549                    &mut body_comments,
7550                );
7551                if !body_comments.is_empty() {
7552                    function.body.leading_comments.extend(body_comments);
7553                }
7554            }
7555            AstCommand::Simple(_) | AstCommand::Builtin(_) | AstCommand::Decl(_) => {}
7556        }
7557    }
7558
7559    fn attach_comments_to_compound_with_source(
7560        source: &str,
7561        command: &mut CompoundCommand,
7562        comments: &mut VecDeque<Comment>,
7563    ) {
7564        match command {
7565            CompoundCommand::If(command) => {
7566                let mut condition =
7567                    Self::take_comments_before(comments, command.condition.span.end.offset);
7568                Self::attach_comments_to_stmt_seq_with_source(
7569                    source,
7570                    &mut command.condition,
7571                    &mut condition,
7572                );
7573                command.condition.trailing_comments.extend(condition);
7574
7575                let mut then_branch =
7576                    Self::take_comments_before(comments, command.then_branch.span.end.offset);
7577                Self::attach_comments_to_stmt_seq_with_source(
7578                    source,
7579                    &mut command.then_branch,
7580                    &mut then_branch,
7581                );
7582                command.then_branch.trailing_comments.extend(then_branch);
7583
7584                for (condition_seq, body_seq) in &mut command.elif_branches {
7585                    let mut elif_condition =
7586                        Self::take_comments_before(comments, condition_seq.span.end.offset);
7587                    Self::attach_comments_to_stmt_seq_with_source(
7588                        source,
7589                        condition_seq,
7590                        &mut elif_condition,
7591                    );
7592                    condition_seq.trailing_comments.extend(elif_condition);
7593
7594                    let mut elif_body =
7595                        Self::take_comments_before(comments, body_seq.span.end.offset);
7596                    Self::attach_comments_to_stmt_seq_with_source(source, body_seq, &mut elif_body);
7597                    body_seq.trailing_comments.extend(elif_body);
7598                }
7599
7600                if let Some(else_branch) = &mut command.else_branch {
7601                    let mut else_comments = std::mem::take(comments);
7602                    Self::attach_comments_to_stmt_seq_with_source(
7603                        source,
7604                        else_branch,
7605                        &mut else_comments,
7606                    );
7607                    else_branch.trailing_comments.extend(else_comments);
7608                }
7609            }
7610            CompoundCommand::For(command) => {
7611                let mut body_comments = std::mem::take(comments);
7612                Self::attach_comments_to_stmt_seq_with_source(
7613                    source,
7614                    &mut command.body,
7615                    &mut body_comments,
7616                );
7617                command.body.trailing_comments.extend(body_comments);
7618            }
7619            CompoundCommand::Repeat(command) => {
7620                let mut body_comments = std::mem::take(comments);
7621                Self::attach_comments_to_stmt_seq_with_source(
7622                    source,
7623                    &mut command.body,
7624                    &mut body_comments,
7625                );
7626                command.body.trailing_comments.extend(body_comments);
7627            }
7628            CompoundCommand::Foreach(command) => {
7629                let mut body_comments = std::mem::take(comments);
7630                Self::attach_comments_to_stmt_seq_with_source(
7631                    source,
7632                    &mut command.body,
7633                    &mut body_comments,
7634                );
7635                command.body.trailing_comments.extend(body_comments);
7636            }
7637            CompoundCommand::ArithmeticFor(command) => {
7638                let mut body_comments = std::mem::take(comments);
7639                Self::attach_comments_to_stmt_seq_with_source(
7640                    source,
7641                    &mut command.body,
7642                    &mut body_comments,
7643                );
7644                command.body.trailing_comments.extend(body_comments);
7645            }
7646            CompoundCommand::While(command) => {
7647                let mut condition =
7648                    Self::take_comments_before(comments, command.condition.span.end.offset);
7649                Self::attach_comments_to_stmt_seq_with_source(
7650                    source,
7651                    &mut command.condition,
7652                    &mut condition,
7653                );
7654                command.condition.trailing_comments.extend(condition);
7655
7656                let mut body_comments = std::mem::take(comments);
7657                Self::attach_comments_to_stmt_seq_with_source(
7658                    source,
7659                    &mut command.body,
7660                    &mut body_comments,
7661                );
7662                command.body.trailing_comments.extend(body_comments);
7663            }
7664            CompoundCommand::Until(command) => {
7665                let mut condition =
7666                    Self::take_comments_before(comments, command.condition.span.end.offset);
7667                Self::attach_comments_to_stmt_seq_with_source(
7668                    source,
7669                    &mut command.condition,
7670                    &mut condition,
7671                );
7672                command.condition.trailing_comments.extend(condition);
7673
7674                let mut body_comments = std::mem::take(comments);
7675                Self::attach_comments_to_stmt_seq_with_source(
7676                    source,
7677                    &mut command.body,
7678                    &mut body_comments,
7679                );
7680                command.body.trailing_comments.extend(body_comments);
7681            }
7682            CompoundCommand::Case(command) => {
7683                for case in &mut command.cases {
7684                    let mut body_comments =
7685                        Self::take_comments_before(comments, case.body.span.end.offset);
7686                    Self::attach_comments_to_stmt_seq_with_source(
7687                        source,
7688                        &mut case.body,
7689                        &mut body_comments,
7690                    );
7691                    case.body.trailing_comments.extend(body_comments);
7692                }
7693            }
7694            CompoundCommand::Select(command) => {
7695                let mut body_comments = std::mem::take(comments);
7696                Self::attach_comments_to_stmt_seq_with_source(
7697                    source,
7698                    &mut command.body,
7699                    &mut body_comments,
7700                );
7701                command.body.trailing_comments.extend(body_comments);
7702            }
7703            CompoundCommand::Subshell(body) | CompoundCommand::BraceGroup(body) => {
7704                let mut body_comments = std::mem::take(comments);
7705                Self::attach_comments_to_stmt_seq_with_source(source, body, &mut body_comments);
7706                body.trailing_comments.extend(body_comments);
7707            }
7708            CompoundCommand::Always(command) => {
7709                let mut body_comments =
7710                    Self::take_comments_before(comments, command.body.span.end.offset);
7711                Self::attach_comments_to_stmt_seq_with_source(
7712                    source,
7713                    &mut command.body,
7714                    &mut body_comments,
7715                );
7716                command.body.trailing_comments.extend(body_comments);
7717
7718                let mut always_comments = std::mem::take(comments);
7719                Self::attach_comments_to_stmt_seq_with_source(
7720                    source,
7721                    &mut command.always_body,
7722                    &mut always_comments,
7723                );
7724                command
7725                    .always_body
7726                    .trailing_comments
7727                    .extend(always_comments);
7728            }
7729            CompoundCommand::Time(command) => {
7730                if let Some(inner) = &mut command.command {
7731                    let mut inner_comments = std::mem::take(comments);
7732                    Self::attach_comments_to_stmt_with_source(
7733                        source,
7734                        inner.as_mut(),
7735                        &mut inner_comments,
7736                    );
7737                    if !inner_comments.is_empty() {
7738                        inner.leading_comments.extend(inner_comments);
7739                    }
7740                }
7741            }
7742            CompoundCommand::Coproc(command) => {
7743                let mut body_comments = std::mem::take(comments);
7744                Self::attach_comments_to_stmt_with_source(
7745                    source,
7746                    command.body.as_mut(),
7747                    &mut body_comments,
7748                );
7749                if !body_comments.is_empty() {
7750                    command.body.leading_comments.extend(body_comments);
7751                }
7752            }
7753            CompoundCommand::Arithmetic(_) | CompoundCommand::Conditional(_) => {}
7754        }
7755    }
7756
7757    fn advance_raw(&mut self) {
7758        #[cfg(feature = "benchmarking")]
7759        self.maybe_record_advance_raw_call();
7760        if let Some(peeked) = self.peeked_token.take() {
7761            self.set_current_spanned(peeked);
7762        } else {
7763            loop {
7764                match self.next_spanned_token_with_comments() {
7765                    Some(st) if st.kind == TokenKind::Comment => {
7766                        self.maybe_record_comment(&st);
7767                    }
7768                    Some(st) => {
7769                        self.set_current_spanned(st);
7770                        break;
7771                    }
7772                    None => {
7773                        self.clear_current_token();
7774                        // Keep the last span for error reporting
7775                        break;
7776                    }
7777                }
7778            }
7779        }
7780    }
7781
7782    #[cfg(feature = "benchmarking")]
7783    fn maybe_record_set_current_spanned_call(&mut self) {
7784        if let Some(counters) = &mut self.benchmark_counters {
7785            counters.parser_set_current_spanned_calls += 1;
7786        }
7787    }
7788
7789    #[cfg(feature = "benchmarking")]
7790    fn maybe_record_advance_raw_call(&mut self) {
7791        if let Some(counters) = &mut self.benchmark_counters {
7792            counters.parser_advance_raw_calls += 1;
7793        }
7794    }
7795
7796    #[cfg(feature = "benchmarking")]
7797    fn finish_benchmark_counters(&self) -> ParserBenchmarkCounters {
7798        let mut counters = self.benchmark_counters.unwrap_or_default();
7799        counters.lexer_current_position_calls =
7800            self.lexer.benchmark_counters().current_position_calls;
7801        counters
7802    }
7803
7804    fn advance(&mut self) {
7805        let should_expand = std::mem::take(&mut self.expand_next_word);
7806        self.advance_raw();
7807        if should_expand {
7808            if self
7809                .current_token
7810                .as_ref()
7811                .is_some_and(|token| token.flags.is_synthetic())
7812            {
7813                self.expand_next_word = true;
7814            } else {
7815                self.maybe_expand_current_alias_chain();
7816            }
7817        }
7818    }
7819
7820    /// Peek at the next token without consuming the current one
7821    fn peek_next(&mut self) -> Option<&LexedToken<'a>> {
7822        if self.peeked_token.is_none() {
7823            loop {
7824                match self.next_spanned_token_with_comments() {
7825                    Some(st) if st.kind == TokenKind::Comment => {
7826                        self.maybe_record_comment(&st);
7827                    }
7828                    other => {
7829                        self.peeked_token = other;
7830                        break;
7831                    }
7832                }
7833            }
7834        }
7835        self.peeked_token.as_ref()
7836    }
7837
7838    fn peek_next_kind(&mut self) -> Option<TokenKind> {
7839        self.peek_next()?;
7840        self.peeked_token.as_ref().map(|st| st.kind)
7841    }
7842
7843    fn peek_next_is(&mut self, kind: TokenKind) -> bool {
7844        self.peek_next_kind() == Some(kind)
7845    }
7846
7847    fn at(&self, kind: TokenKind) -> bool {
7848        self.current_token_kind == Some(kind)
7849    }
7850
7851    fn current_token_has_leading_whitespace(&self) -> bool {
7852        self.current_span.start.offset > 0
7853            && self.input[..self.current_span.start.offset]
7854                .chars()
7855                .next_back()
7856                .is_some_and(|ch| matches!(ch, ' ' | '\t' | '\n'))
7857    }
7858
7859    fn current_token_is_tight_to_next_token(&mut self) -> bool {
7860        let current_end = self.current_span.end.offset;
7861        self.peek_next()
7862            .is_some_and(|token| token.span.start.offset == current_end)
7863    }
7864
7865    fn at_in_set(&self, set: TokenSet) -> bool {
7866        self.current_token_kind
7867            .is_some_and(|kind| set.contains(kind))
7868    }
7869
7870    fn at_word_like(&self) -> bool {
7871        self.current_token_kind.is_some_and(TokenKind::is_word_like)
7872    }
7873
7874    fn current_word_str(&self) -> Option<&str> {
7875        self.current_token_kind
7876            .filter(|kind| kind.is_word_like())
7877            .and(self.current_token.as_ref())
7878            .and_then(LexedToken::word_text)
7879    }
7880
7881    fn classify_keyword(word: &str) -> Option<Keyword> {
7882        match word.as_bytes() {
7883            b"if" => Some(Keyword::If),
7884            b"for" => Some(Keyword::For),
7885            b"repeat" => Some(Keyword::Repeat),
7886            b"foreach" => Some(Keyword::Foreach),
7887            b"while" => Some(Keyword::While),
7888            b"until" => Some(Keyword::Until),
7889            b"case" => Some(Keyword::Case),
7890            b"select" => Some(Keyword::Select),
7891            b"time" => Some(Keyword::Time),
7892            b"coproc" => Some(Keyword::Coproc),
7893            b"function" => Some(Keyword::Function),
7894            b"always" => Some(Keyword::Always),
7895            b"then" => Some(Keyword::Then),
7896            b"else" => Some(Keyword::Else),
7897            b"elif" => Some(Keyword::Elif),
7898            b"fi" => Some(Keyword::Fi),
7899            b"do" => Some(Keyword::Do),
7900            b"done" => Some(Keyword::Done),
7901            b"esac" => Some(Keyword::Esac),
7902            b"in" => Some(Keyword::In),
7903            _ => None,
7904        }
7905    }
7906
7907    fn current_keyword(&self) -> Option<Keyword> {
7908        self.current_keyword
7909    }
7910
7911    fn looks_like_disabled_repeat_loop(&mut self) -> Result<bool> {
7912        if self.current_keyword() != Some(Keyword::Repeat) {
7913            return Ok(false);
7914        }
7915
7916        let checkpoint = self.checkpoint();
7917        self.advance();
7918        if !self.at_word_like() {
7919            self.restore(checkpoint);
7920            return Ok(false);
7921        }
7922        self.advance();
7923
7924        let result = match self.current_token_kind {
7925            Some(TokenKind::LeftBrace) => Ok(true),
7926            Some(TokenKind::Semicolon) => {
7927                self.advance();
7928                if let Err(error) = self.skip_newlines() {
7929                    self.restore(checkpoint);
7930                    return Err(error);
7931                }
7932                Ok(self.current_keyword() == Some(Keyword::Do))
7933            }
7934            Some(TokenKind::Newline) => {
7935                if let Err(error) = self.skip_newlines() {
7936                    self.restore(checkpoint);
7937                    return Err(error);
7938                }
7939                Ok(self.current_keyword() == Some(Keyword::Do))
7940            }
7941            _ => Ok(false),
7942        };
7943        self.restore(checkpoint);
7944        result
7945    }
7946
7947    fn looks_like_disabled_foreach_loop(&mut self) -> Result<bool> {
7948        if self.current_keyword() != Some(Keyword::Foreach) {
7949            return Ok(false);
7950        }
7951
7952        let checkpoint = self.checkpoint();
7953        self.advance();
7954        if self.current_name_token().is_none() {
7955            self.restore(checkpoint);
7956            return Ok(false);
7957        }
7958        self.advance();
7959
7960        let result = if self.at(TokenKind::LeftParen) {
7961            self.advance();
7962            let mut saw_word = false;
7963            while !self.at(TokenKind::RightParen) {
7964                if !self.at_word_like() {
7965                    self.restore(checkpoint);
7966                    return Ok(false);
7967                }
7968                saw_word = true;
7969                self.advance();
7970            }
7971            if !saw_word {
7972                self.restore(checkpoint);
7973                return Ok(false);
7974            }
7975            self.advance();
7976            Ok(self.at(TokenKind::LeftBrace))
7977        } else {
7978            if self.current_keyword() != Some(Keyword::In) {
7979                self.restore(checkpoint);
7980                return Ok(false);
7981            }
7982            self.advance();
7983
7984            let mut saw_word = false;
7985            let saw_separator = loop {
7986                if self.current_keyword() == Some(Keyword::Do) {
7987                    break false;
7988                }
7989
7990                match self.current_token_kind {
7991                    Some(kind) if kind.is_word_like() => {
7992                        saw_word = true;
7993                        self.advance();
7994                    }
7995                    Some(TokenKind::Semicolon) => {
7996                        self.advance();
7997                        break true;
7998                    }
7999                    Some(TokenKind::Newline) => {
8000                        if let Err(error) = self.skip_newlines() {
8001                            self.restore(checkpoint);
8002                            return Err(error);
8003                        }
8004                        break true;
8005                    }
8006                    _ => break false,
8007                }
8008            };
8009
8010            Ok(saw_word && saw_separator && self.current_keyword() == Some(Keyword::Do))
8011        };
8012        self.restore(checkpoint);
8013        result
8014    }
8015
8016    fn skip_newlines_with_flag(&mut self) -> Result<bool> {
8017        let mut skipped = false;
8018        while self.at(TokenKind::Newline) {
8019            self.tick()?;
8020            self.advance();
8021            skipped = true;
8022        }
8023        Ok(skipped)
8024    }
8025
8026    fn skip_newlines(&mut self) -> Result<()> {
8027        self.skip_newlines_with_flag().map(|_| ())
8028    }
8029}
8030#[cfg(test)]
8031mod tests;