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