Skip to main content

shuck_parser/parser/
mod.rs

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