Skip to main content

shuck_parser/parser/
mod.rs

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