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