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