1#![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
55const DEFAULT_MAX_AST_DEPTH: usize = 100;
57
58const HARD_MAX_AST_DEPTH: usize = 100;
68
69const DEFAULT_MAX_PARSER_OPERATIONS: usize = 100_000;
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum ParseStatus {
75 Clean,
77 Recovered,
79 Fatal,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct ZshCaseGroupPart {
86 pub pattern_part_index: usize,
88 pub span: Span,
90}
91
92#[derive(Debug, Clone, Default, PartialEq, Eq)]
94pub struct SyntaxFacts {
95 pub zsh_brace_if_spans: Vec<Span>,
97 pub zsh_always_spans: Vec<Span>,
99 pub zsh_case_group_parts: Vec<ZshCaseGroupPart>,
101}
102
103#[derive(Debug, Clone)]
106pub struct ParseResult {
107 pub file: File,
109 pub diagnostics: Vec<ParseDiagnostic>,
111 pub status: ParseStatus,
113 pub terminal_error: Option<Error>,
115 pub syntax_facts: SyntaxFacts,
117}
118
119impl ParseResult {
120 pub fn is_ok(&self) -> bool {
122 self.status == ParseStatus::Clean
123 }
124
125 pub fn is_err(&self) -> bool {
127 !self.is_ok()
128 }
129
130 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 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 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 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 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
188pub 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
217pub 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 pub lexer_current_position_calls: u64,
279 pub parser_set_current_spanned_calls: u64,
281 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
360pub enum ShellDialect {
361 Posix,
363 Mksh,
365 #[default]
367 Bash,
368 Zsh,
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
374pub enum OptionValue {
375 On,
377 Off,
379 #[default]
381 Unknown,
382}
383
384impl OptionValue {
385 pub const fn is_definitely_on(self) -> bool {
387 matches!(self, Self::On)
388 }
389
390 pub const fn is_definitely_off(self) -> bool {
392 matches!(self, Self::Off)
393 }
394
395 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
407pub enum ZshEmulationMode {
408 Zsh,
410 Sh,
412 Ksh,
414 Csh,
416}
417
418#[derive(Debug, Clone, PartialEq, Eq, Hash)]
420pub struct ZshOptionState {
421 pub sh_word_split: OptionValue,
423 pub glob_subst: OptionValue,
425 pub rc_expand_param: OptionValue,
427 pub glob: OptionValue,
429 pub nomatch: OptionValue,
431 pub null_glob: OptionValue,
433 pub csh_null_glob: OptionValue,
435 pub extended_glob: OptionValue,
437 pub ksh_glob: OptionValue,
439 pub sh_glob: OptionValue,
441 pub bare_glob_qual: OptionValue,
443 pub glob_dots: OptionValue,
445 pub equals: OptionValue,
447 pub magic_equal_subst: OptionValue,
449 pub sh_file_expansion: OptionValue,
451 pub glob_assign: OptionValue,
453 pub ignore_braces: OptionValue,
455 pub ignore_close_braces: OptionValue,
457 pub brace_ccl: OptionValue,
459 pub ksh_arrays: OptionValue,
461 pub ksh_zero_subscript: OptionValue,
463 pub short_loops: OptionValue,
465 pub short_repeat: OptionValue,
467 pub rc_quotes: OptionValue,
469 pub interactive_comments: OptionValue,
471 pub c_bases: OptionValue,
473 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 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 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 pub fn apply_setopt(&mut self, name: &str) -> bool {
576 self.apply_named_option(name, true)
577 }
578
579 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 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
709pub struct ShellProfile {
710 pub dialect: ShellDialect,
712 pub options: Option<ZshOptionState>,
714}
715
716impl ShellProfile {
717 pub fn native(dialect: ShellDialect) -> Self {
719 Self {
720 dialect,
721 options: (dialect == ShellDialect::Zsh).then(ZshOptionState::zsh_default),
722 }
723 }
724
725 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 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 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#[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 current_span: Span,
1566 peeked_token: Option<LexedToken<'a>>,
1568 max_depth: usize,
1570 current_depth: usize,
1572 fuel: usize,
1574 max_fuel: usize,
1576 comments: Vec<Comment>,
1578 aliases: HashMap<String, AliasDefinition>,
1580 expand_aliases: bool,
1582 expand_next_word: bool,
1585 brace_group_depth: usize,
1587 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#[derive(Debug, Clone, PartialEq, Eq)]
1622pub struct ParseDiagnostic {
1623 pub message: String,
1625 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 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 pub fn with_dialect(input: &'a str, dialect: ShellDialect) -> Self {
1820 Self::with_profile(input, ShellProfile::native(dialect))
1821 }
1822
1823 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 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 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 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 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 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 pub fn dialect(&self) -> ShellDialect {
1979 self.dialect
1980 }
1981
1982 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 pub fn current_span(&self) -> Span {
2040 self.current_span
2041 }
2042
2043 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 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 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 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("$(")
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 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 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 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 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 fn pop_depth(&mut self) {
7019 if self.current_depth > 0 {
7020 self.current_depth -= 1;
7021 }
7022 }
7023
7024 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: Vec::new(),
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_vec(),
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_vec(),
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_vec(),
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_vec(),
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_vec(),
7166 span: command.span,
7167 }),
7168 negated: false,
7169 redirects: command.redirects.into_vec(),
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_vec(),
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_vec(),
7197 span: command.span,
7198 }),
7199 negated: false,
7200 redirects: command.redirects.into_vec(),
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_vec(),
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: Vec::new(),
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_vec(),
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 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 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;