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_assignment_word(
2063 source: &str,
2064 word: Word,
2065 explicit_array_kind: Option<ArrayKind>,
2066 subscript_interpretation: SubscriptInterpretation,
2067 ) -> Option<Assignment> {
2068 let mut parser = Parser::new(source);
2069 parser.parse_assignment_from_word(word, explicit_array_kind, subscript_interpretation)
2070 }
2071
2072 pub fn parse_assignment_word_group(
2078 source: &str,
2079 words: &[&Word],
2080 explicit_array_kind: Option<ArrayKind>,
2081 subscript_interpretation: SubscriptInterpretation,
2082 ) -> Option<Assignment> {
2083 let first = words.first()?;
2084 let last = words.last()?;
2085 let span = Span::from_positions(first.span.start, last.span.end);
2086 let raw = span.slice(source);
2087 let mut parser = Parser::new(source);
2088 parser.parse_assignment_from_text(raw, span, explicit_array_kind, subscript_interpretation)
2089 }
2090
2091 pub fn parse_word_string_with_limits(input: &str, max_depth: usize, max_fuel: usize) -> Word {
2094 Self::parse_word_string_with_limits_and_dialect(
2095 input,
2096 max_depth,
2097 max_fuel,
2098 ShellDialect::Bash,
2099 )
2100 }
2101
2102 pub fn parse_word_string_with_limits_and_dialect(
2104 input: &str,
2105 max_depth: usize,
2106 max_fuel: usize,
2107 dialect: ShellDialect,
2108 ) -> Word {
2109 let mut parser = Parser::with_limits_and_profile(
2110 input,
2111 max_depth,
2112 max_fuel,
2113 ShellProfile::native(dialect),
2114 );
2115 let start = Position::new();
2116 parser.parse_word_with_context(
2117 input,
2118 Span::from_positions(start, start.advanced_by(input)),
2119 start,
2120 true,
2121 )
2122 }
2123
2124 pub fn parse_word_fragment(source: &str, text: &str, span: Span) -> Word {
2127 let mut parser = Parser::new(text);
2128 let source_backed = span.end.offset <= source.len() && span.slice(source) == text;
2129 let start = Position::new();
2130 let fragment_span = Span::from_positions(start, start.advanced_by(text));
2131 let mut word = parser.parse_word_with_context(text, fragment_span, start, source_backed);
2132 if !source_backed {
2133 Self::materialize_word_source_backing(&mut word, text);
2134 }
2135 Self::rebase_word(&mut word, span.start);
2136 word.span = span;
2137 word
2138 }
2139
2140 fn maybe_record_comment(&mut self, token: &LexedToken<'_>) {
2141 if token.kind == TokenKind::Comment && !token.flags.is_synthetic() {
2142 self.comments.push(Comment {
2143 range: token.span.to_range(),
2144 });
2145 }
2146 }
2147
2148 fn record_zsh_brace_if_span(&mut self, span: Span) {
2149 if !self.syntax_facts.zsh_brace_if_spans.contains(&span) {
2150 self.syntax_facts.zsh_brace_if_spans.push(span);
2151 }
2152 }
2153
2154 fn record_zsh_always_span(&mut self, span: Span) {
2155 if !self.syntax_facts.zsh_always_spans.contains(&span) {
2156 self.syntax_facts.zsh_always_spans.push(span);
2157 }
2158 }
2159
2160 fn record_zsh_case_group_part(&mut self, pattern_part_index: usize, span: Span) {
2161 if !self
2162 .syntax_facts
2163 .zsh_case_group_parts
2164 .iter()
2165 .any(|fact| fact.pattern_part_index == pattern_part_index && fact.span == span)
2166 {
2167 self.syntax_facts
2168 .zsh_case_group_parts
2169 .push(ZshCaseGroupPart {
2170 pattern_part_index,
2171 span,
2172 });
2173 }
2174 }
2175
2176 fn word_text_needs_parse(text: &str) -> bool {
2177 memchr3(b'$', b'`', b'\0', text.as_bytes()).is_some()
2178 }
2179
2180 fn word_with_parts(&self, parts: Vec<WordPartNode>, span: Span) -> Word {
2181 let brace_syntax = self.brace_syntax_from_parts(&parts, span.start.offset);
2182 Word {
2183 parts,
2184 span,
2185 brace_syntax,
2186 }
2187 }
2188
2189 fn word_with_part_buffer(&self, parts: WordPartBuffer, span: Span) -> Word {
2190 let brace_syntax = self.brace_syntax_from_parts(&parts, span.start.offset);
2191 let parts = if parts.spilled() {
2192 parts.into_vec()
2193 } else {
2194 let mut vec = Vec::with_capacity(parts.len());
2195 vec.extend(parts);
2196 vec
2197 };
2198 Word {
2199 parts,
2200 span,
2201 brace_syntax,
2202 }
2203 }
2204
2205 fn word_with_single_part(&self, part: WordPartNode, span: Span) -> Word {
2206 let mut parts = WordPartBuffer::new();
2207 parts.push(part);
2208 self.word_with_part_buffer(parts, span)
2209 }
2210
2211 fn heredoc_body_with_parts(
2212 &self,
2213 parts: Vec<HeredocBodyPartNode>,
2214 span: Span,
2215 mode: HeredocBodyMode,
2216 source_backed: bool,
2217 ) -> HeredocBody {
2218 HeredocBody {
2219 mode,
2220 source_backed,
2221 parts,
2222 span,
2223 }
2224 }
2225
2226 fn heredoc_body_part_from_word_part_node(part: WordPartNode) -> HeredocBodyPartNode {
2227 let kind = match part.kind {
2228 WordPart::Literal(text) => HeredocBodyPart::Literal(text),
2229 WordPart::Variable(name) => HeredocBodyPart::Variable(name),
2230 WordPart::CommandSubstitution { body, syntax } => {
2231 HeredocBodyPart::CommandSubstitution { body, syntax }
2232 }
2233 WordPart::ArithmeticExpansion {
2234 expression,
2235 expression_ast,
2236 expression_word_ast,
2237 syntax,
2238 } => HeredocBodyPart::ArithmeticExpansion {
2239 expression,
2240 expression_ast,
2241 expression_word_ast,
2242 syntax,
2243 },
2244 WordPart::Parameter(parameter) => HeredocBodyPart::Parameter(Box::new(parameter)),
2245 other => panic!("unsupported heredoc body part: {other:?}"),
2246 };
2247
2248 HeredocBodyPartNode::new(kind, part.span)
2249 }
2250
2251 fn brace_syntax_from_parts(&self, parts: &[WordPartNode], offset: usize) -> Vec<BraceSyntax> {
2252 if !self.brace_syntax_enabled_at(offset) {
2253 return Vec::new();
2254 }
2255 let mut brace_syntax = Vec::new();
2256 self.collect_brace_syntax_from_parts(parts, BraceQuoteContext::Unquoted, &mut brace_syntax);
2257 brace_syntax.sort_by_key(|brace| (brace.span.start.offset, brace.span.end.offset));
2258 brace_syntax.dedup_by_key(|brace| {
2259 (
2260 brace.span.start.offset,
2261 brace.span.end.offset,
2262 brace.quote_context,
2263 brace.kind,
2264 )
2265 });
2266 brace_syntax
2267 }
2268
2269 fn collect_brace_syntax_from_parts(
2270 &self,
2271 parts: &[WordPartNode],
2272 quote_context: BraceQuoteContext,
2273 out: &mut Vec<BraceSyntax>,
2274 ) {
2275 for part in parts {
2276 match &part.kind {
2277 WordPart::Literal(text) => Self::scan_brace_syntax_text(
2278 text.syntax_str(self.input, part.span),
2279 part.span.start,
2280 quote_context,
2281 out,
2282 ),
2283 WordPart::SingleQuoted { .. } => Self::scan_brace_syntax_text(
2284 part.span.slice(self.input),
2285 part.span.start,
2286 BraceQuoteContext::SingleQuoted,
2287 out,
2288 ),
2289 WordPart::DoubleQuoted { parts, .. } => self.collect_brace_syntax_from_parts(
2290 parts,
2291 BraceQuoteContext::DoubleQuoted,
2292 out,
2293 ),
2294 WordPart::ZshQualifiedGlob(glob) => {
2295 self.collect_brace_syntax_from_zsh_qualified_glob(glob, quote_context, out)
2296 }
2297 WordPart::Variable(_)
2298 | WordPart::CommandSubstitution { .. }
2299 | WordPart::ArithmeticExpansion { .. }
2300 | WordPart::Parameter(_)
2301 | WordPart::ParameterExpansion { .. }
2302 | WordPart::Length(_)
2303 | WordPart::ArrayAccess(_)
2304 | WordPart::ArrayLength(_)
2305 | WordPart::ArrayIndices(_)
2306 | WordPart::Substring { .. }
2307 | WordPart::ArraySlice { .. }
2308 | WordPart::IndirectExpansion { .. }
2309 | WordPart::PrefixMatch { .. }
2310 | WordPart::ProcessSubstitution { .. }
2311 | WordPart::Transformation { .. } => {}
2312 }
2313 }
2314
2315 if self.needs_cross_part_brace_scan(parts) {
2316 let mut chars = Vec::new();
2317 self.collect_brace_scan_chars_from_parts(parts, &mut chars);
2318 Self::scan_brace_syntax_chars(&chars, quote_context, out);
2319 }
2320 }
2321
2322 fn needs_cross_part_brace_scan(&self, parts: &[WordPartNode]) -> bool {
2323 if parts.len() < 2 {
2324 return false;
2325 }
2326
2327 let mut cursor = parts[0].span.start.offset;
2328 for part in parts {
2329 if part.span.start.offset > cursor
2330 && self.input[cursor..part.span.start.offset].contains(['{', '}'])
2331 {
2332 return true;
2333 }
2334
2335 let has_brace_text = match &part.kind {
2336 WordPart::Literal(text) => {
2337 text.syntax_str(self.input, part.span).contains(['{', '}'])
2338 }
2339 WordPart::SingleQuoted { .. }
2340 | WordPart::DoubleQuoted { .. }
2341 | WordPart::ZshQualifiedGlob(_) => part.span.slice(self.input).contains(['{', '}']),
2342 WordPart::Variable(_)
2343 | WordPart::CommandSubstitution { .. }
2344 | WordPart::ArithmeticExpansion { .. }
2345 | WordPart::Parameter(_)
2346 | WordPart::ParameterExpansion { .. }
2347 | WordPart::Length(_)
2348 | WordPart::ArrayAccess(_)
2349 | WordPart::ArrayLength(_)
2350 | WordPart::ArrayIndices(_)
2351 | WordPart::Substring { .. }
2352 | WordPart::ArraySlice { .. }
2353 | WordPart::IndirectExpansion { .. }
2354 | WordPart::PrefixMatch { .. }
2355 | WordPart::ProcessSubstitution { .. }
2356 | WordPart::Transformation { .. } => false,
2357 };
2358 if has_brace_text {
2359 return true;
2360 }
2361
2362 cursor = part.span.end.offset;
2363 }
2364
2365 false
2366 }
2367
2368 fn collect_brace_scan_chars_from_parts(
2369 &self,
2370 parts: &[WordPartNode],
2371 out: &mut Vec<(char, Position)>,
2372 ) {
2373 for part in parts {
2374 match &part.kind {
2375 WordPart::Literal(text) => {
2376 Self::push_brace_scan_text(
2377 text.syntax_str(self.input, part.span),
2378 part.span.start,
2379 out,
2380 );
2381 }
2382 WordPart::SingleQuoted { .. } => {
2383 Self::push_brace_scan_text(part.span.slice(self.input), part.span.start, out);
2384 }
2385 WordPart::DoubleQuoted { parts, .. } => {
2386 self.collect_brace_scan_chars_from_double_quoted_part(part.span, parts, out);
2387 }
2388 WordPart::ZshQualifiedGlob(_) => {}
2389 WordPart::Variable(_)
2390 | WordPart::CommandSubstitution { .. }
2391 | WordPart::ArithmeticExpansion { .. }
2392 | WordPart::Parameter(_)
2393 | WordPart::ParameterExpansion { .. }
2394 | WordPart::Length(_)
2395 | WordPart::ArrayAccess(_)
2396 | WordPart::ArrayLength(_)
2397 | WordPart::ArrayIndices(_)
2398 | WordPart::Substring { .. }
2399 | WordPart::ArraySlice { .. }
2400 | WordPart::IndirectExpansion { .. }
2401 | WordPart::PrefixMatch { .. }
2402 | WordPart::ProcessSubstitution { .. }
2403 | WordPart::Transformation { .. } => {
2404 Self::push_brace_scan_boundary(part.span.start, out);
2405 }
2406 }
2407 }
2408 }
2409
2410 fn collect_brace_scan_chars_from_double_quoted_part(
2411 &self,
2412 span: Span,
2413 parts: &[WordPartNode],
2414 out: &mut Vec<(char, Position)>,
2415 ) {
2416 let mut cursor_offset = span.start.offset;
2417 let mut cursor_position = span.start;
2418
2419 for part in parts {
2420 if part.span.start.offset > cursor_offset
2421 && let Some(raw) = self.input.get(cursor_offset..part.span.start.offset)
2422 {
2423 Self::push_brace_scan_text(raw, cursor_position, out);
2424 }
2425
2426 match &part.kind {
2427 WordPart::Literal(text) => {
2428 Self::push_brace_scan_text(
2429 text.syntax_str(self.input, part.span),
2430 part.span.start,
2431 out,
2432 );
2433 }
2434 WordPart::SingleQuoted { .. } => {
2435 Self::push_brace_scan_text(part.span.slice(self.input), part.span.start, out);
2436 }
2437 WordPart::DoubleQuoted { parts, .. } => {
2438 self.collect_brace_scan_chars_from_double_quoted_part(part.span, parts, out);
2439 }
2440 WordPart::ZshQualifiedGlob(_) => {}
2441 WordPart::Variable(_)
2442 | WordPart::CommandSubstitution { .. }
2443 | WordPart::ArithmeticExpansion { .. }
2444 | WordPart::Parameter(_)
2445 | WordPart::ParameterExpansion { .. }
2446 | WordPart::Length(_)
2447 | WordPart::ArrayAccess(_)
2448 | WordPart::ArrayLength(_)
2449 | WordPart::ArrayIndices(_)
2450 | WordPart::Substring { .. }
2451 | WordPart::ArraySlice { .. }
2452 | WordPart::IndirectExpansion { .. }
2453 | WordPart::PrefixMatch { .. }
2454 | WordPart::ProcessSubstitution { .. }
2455 | WordPart::Transformation { .. } => {
2456 Self::push_brace_scan_boundary(part.span.start, out);
2457 }
2458 }
2459
2460 cursor_offset = part.span.end.offset;
2461 cursor_position = part.span.end;
2462 }
2463
2464 if cursor_offset < span.end.offset
2465 && let Some(raw) = self.input.get(cursor_offset..span.end.offset)
2466 {
2467 Self::push_brace_scan_text(raw, cursor_position, out);
2468 }
2469 }
2470
2471 fn push_brace_scan_text(text: &str, start: Position, out: &mut Vec<(char, Position)>) {
2472 let mut position = start;
2473 for ch in text.chars() {
2474 out.push((ch, position));
2475 position.advance(ch);
2476 }
2477 }
2478
2479 fn push_brace_scan_boundary(position: Position, out: &mut Vec<(char, Position)>) {
2480 out.push(('\0', position));
2481 }
2482
2483 fn scan_brace_syntax_chars(
2484 chars: &[(char, Position)],
2485 initial_quote_context: BraceQuoteContext,
2486 out: &mut Vec<BraceSyntax>,
2487 ) {
2488 #[derive(Clone, Copy, PartialEq, Eq)]
2489 enum QuoteState {
2490 Single,
2491 AnsiSingle,
2492 Double,
2493 }
2494
2495 #[derive(Clone, Copy)]
2496 struct Candidate {
2497 start: Position,
2498 quote_context: BraceQuoteContext,
2499 has_comma: bool,
2500 has_dot_dot: bool,
2501 saw_unquoted_whitespace: bool,
2502 saw_quote_boundary: bool,
2503 prev_char: Option<char>,
2504 }
2505
2506 fn quote_state(context: BraceQuoteContext) -> Option<QuoteState> {
2507 match context {
2508 BraceQuoteContext::Unquoted => None,
2509 BraceQuoteContext::SingleQuoted => Some(QuoteState::Single),
2510 BraceQuoteContext::DoubleQuoted => Some(QuoteState::Double),
2511 }
2512 }
2513
2514 fn quote_context(state: Option<QuoteState>) -> BraceQuoteContext {
2515 match state {
2516 None => BraceQuoteContext::Unquoted,
2517 Some(QuoteState::Single | QuoteState::AnsiSingle) => {
2518 BraceQuoteContext::SingleQuoted
2519 }
2520 Some(QuoteState::Double) => BraceQuoteContext::DoubleQuoted,
2521 }
2522 }
2523
2524 fn quote_context_is_active(context: BraceQuoteContext, state: Option<QuoteState>) -> bool {
2525 match context {
2526 BraceQuoteContext::Unquoted => state.is_none(),
2527 BraceQuoteContext::SingleQuoted => {
2528 matches!(state, Some(QuoteState::Single | QuoteState::AnsiSingle))
2529 }
2530 BraceQuoteContext::DoubleQuoted => matches!(state, Some(QuoteState::Double)),
2531 }
2532 }
2533
2534 fn last_active_candidate_index(
2535 stack: &[Candidate],
2536 state: Option<QuoteState>,
2537 ) -> Option<usize> {
2538 stack
2539 .iter()
2540 .rposition(|candidate| quote_context_is_active(candidate.quote_context, state))
2541 }
2542
2543 fn template_placeholder_end(
2544 chars: &[(char, Position)],
2545 start: usize,
2546 quote_context: BraceQuoteContext,
2547 ) -> Option<usize> {
2548 if chars.get(start)?.0 != '{' || chars.get(start + 1)?.0 != '{' {
2549 return None;
2550 }
2551
2552 let mut index = start + 2;
2553 let mut depth = 1usize;
2554
2555 while index < chars.len() {
2556 if matches!(quote_context, BraceQuoteContext::Unquoted) && chars[index].0 == '\\' {
2557 index += 1;
2558 if index < chars.len() {
2559 index += 1;
2560 }
2561 continue;
2562 }
2563
2564 if chars.get(index).is_some_and(|(ch, _)| *ch == '{')
2565 && chars.get(index + 1).is_some_and(|(ch, _)| *ch == '{')
2566 {
2567 depth += 1;
2568 index += 2;
2569 continue;
2570 }
2571
2572 if chars.get(index).is_some_and(|(ch, _)| *ch == '}')
2573 && chars.get(index + 1).is_some_and(|(ch, _)| *ch == '}')
2574 {
2575 depth -= 1;
2576 index += 2;
2577 if depth == 0 {
2578 return Some(index);
2579 }
2580 continue;
2581 }
2582
2583 index += 1;
2584 }
2585
2586 None
2587 }
2588
2589 fn brace_span(start: Position, end: (char, Position)) -> Span {
2590 let mut end_position = end.1;
2591 end_position.advance(end.0);
2592 Span::from_positions(start, end_position)
2593 }
2594
2595 let mut index = 0usize;
2596 let mut quote_state = quote_state(initial_quote_context);
2597 let mut stack = Vec::<Candidate>::new();
2598
2599 while index < chars.len() {
2600 let ch = chars[index].0;
2601
2602 if matches!(
2603 quote_state,
2604 None | Some(QuoteState::AnsiSingle) | Some(QuoteState::Double)
2605 ) && ch == '\\'
2606 {
2607 if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state) {
2608 stack[candidate_index].prev_char = None;
2609 }
2610 index += 1;
2611 if index < chars.len() {
2612 index += 1;
2613 }
2614 continue;
2615 }
2616
2617 let current_quote_context = quote_context(quote_state);
2618 if ch == '{'
2619 && let Some(end_index) =
2620 template_placeholder_end(chars, index, current_quote_context)
2621 {
2622 out.push(BraceSyntax {
2623 kind: BraceSyntaxKind::TemplatePlaceholder,
2624 span: brace_span(chars[index].1, chars[end_index - 1]),
2625 quote_context: current_quote_context,
2626 });
2627 if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state) {
2628 stack[candidate_index].prev_char = None;
2629 }
2630 index = end_index;
2631 continue;
2632 }
2633
2634 match quote_state {
2635 None => match ch {
2636 '\'' => {
2637 quote_state = if index > 0 && chars[index - 1].0 == '$' {
2638 Some(QuoteState::AnsiSingle)
2639 } else {
2640 Some(QuoteState::Single)
2641 };
2642 for candidate in &mut stack {
2643 if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
2644 candidate.saw_quote_boundary = true;
2645 }
2646 }
2647 }
2648 '"' => {
2649 quote_state = Some(QuoteState::Double);
2650 for candidate in &mut stack {
2651 if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
2652 candidate.saw_quote_boundary = true;
2653 }
2654 }
2655 }
2656 _ => {}
2657 },
2658 Some(QuoteState::Single) => {
2659 if ch == '\'' {
2660 quote_state = None;
2661 for candidate in &mut stack {
2662 if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
2663 candidate.saw_quote_boundary = true;
2664 }
2665 }
2666 }
2667 }
2668 Some(QuoteState::AnsiSingle) => {
2669 if ch == '\'' {
2670 quote_state = None;
2671 for candidate in &mut stack {
2672 if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
2673 candidate.saw_quote_boundary = true;
2674 }
2675 }
2676 }
2677 }
2678 Some(QuoteState::Double) => {
2679 if ch == '"' {
2680 quote_state = None;
2681 for candidate in &mut stack {
2682 if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
2683 candidate.saw_quote_boundary = true;
2684 }
2685 }
2686 }
2687 }
2688 }
2689
2690 match ch {
2691 '{' => {
2692 stack.push(Candidate {
2693 start: chars[index].1,
2694 quote_context: current_quote_context,
2695 has_comma: false,
2696 has_dot_dot: false,
2697 saw_unquoted_whitespace: false,
2698 saw_quote_boundary: false,
2699 prev_char: Some(ch),
2700 });
2701 index += 1;
2702 continue;
2703 }
2704 '}' => {
2705 if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state)
2706 {
2707 let candidate = stack.remove(candidate_index);
2708 let kind = if matches!(candidate.quote_context, BraceQuoteContext::Unquoted)
2709 && candidate.saw_unquoted_whitespace
2710 {
2711 BraceSyntaxKind::Literal
2712 } else if candidate.has_comma {
2713 BraceSyntaxKind::Expansion(BraceExpansionKind::CommaList)
2714 } else if candidate.has_dot_dot {
2715 BraceSyntaxKind::Expansion(BraceExpansionKind::Sequence)
2716 } else {
2717 BraceSyntaxKind::Literal
2718 };
2719 if !matches!(kind, BraceSyntaxKind::Literal)
2720 || !candidate.saw_quote_boundary
2721 {
2722 out.push(BraceSyntax {
2723 kind,
2724 span: brace_span(candidate.start, chars[index]),
2725 quote_context: candidate.quote_context,
2726 });
2727 }
2728 }
2729 }
2730 _ => {
2731 if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state)
2732 {
2733 let candidate = &mut stack[candidate_index];
2734 let counts_as_top_level =
2735 quote_context_is_active(candidate.quote_context, quote_state);
2736
2737 if counts_as_top_level {
2738 match ch {
2739 ',' => candidate.has_comma = true,
2740 '.' if candidate.prev_char == Some('.') => {
2741 candidate.has_dot_dot = true;
2742 }
2743 c if matches!(
2744 candidate.quote_context,
2745 BraceQuoteContext::Unquoted
2746 ) && quote_state.is_none()
2747 && c.is_whitespace() =>
2748 {
2749 candidate.saw_unquoted_whitespace = true;
2750 }
2751 _ => {}
2752 }
2753 }
2754 }
2755 }
2756 }
2757
2758 if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state) {
2759 stack[candidate_index].prev_char = Some(ch);
2760 }
2761
2762 index += 1;
2763 }
2764 }
2765
2766 fn collect_brace_syntax_from_pattern(
2767 &self,
2768 pattern: &Pattern,
2769 quote_context: BraceQuoteContext,
2770 out: &mut Vec<BraceSyntax>,
2771 ) {
2772 for (part, span) in pattern.parts_with_spans() {
2773 match part {
2774 PatternPart::Literal(text) => Self::scan_brace_syntax_text(
2775 text.as_str(self.input, span),
2776 span.start,
2777 quote_context,
2778 out,
2779 ),
2780 PatternPart::CharClass(text) => Self::scan_brace_syntax_text(
2781 text.slice(self.input),
2782 text.span().start,
2783 quote_context,
2784 out,
2785 ),
2786 PatternPart::Group { patterns, .. } => {
2787 for pattern in patterns {
2788 self.collect_brace_syntax_from_pattern(pattern, quote_context, out);
2789 }
2790 }
2791 PatternPart::Word(word) => {
2792 self.collect_brace_syntax_from_parts(&word.parts, quote_context, out)
2793 }
2794 PatternPart::AnyString | PatternPart::AnyChar => {}
2795 }
2796 }
2797 }
2798
2799 fn collect_brace_syntax_from_zsh_qualified_glob(
2800 &self,
2801 glob: &ZshQualifiedGlob,
2802 quote_context: BraceQuoteContext,
2803 out: &mut Vec<BraceSyntax>,
2804 ) {
2805 for segment in &glob.segments {
2806 if let ZshGlobSegment::Pattern(pattern) = segment {
2807 self.collect_brace_syntax_from_pattern(pattern, quote_context, out);
2808 }
2809 }
2810 }
2811
2812 fn scan_brace_syntax_text(
2813 text: &str,
2814 base: Position,
2815 quote_context: BraceQuoteContext,
2816 out: &mut Vec<BraceSyntax>,
2817 ) {
2818 if memchr(b'{', text.as_bytes()).is_none() {
2819 return;
2820 }
2821
2822 #[derive(Clone, Copy)]
2823 struct ScanFrame<'a> {
2824 text: &'a str,
2825 index: usize,
2826 position: Position,
2827 }
2828
2829 let mut work = SmallVec::<[ScanFrame<'_>; 2]>::new();
2830 work.push(ScanFrame {
2831 text,
2832 index: 0,
2833 position: base,
2834 });
2835
2836 while let Some(mut frame) = work.pop() {
2837 let bytes = frame.text.as_bytes();
2838
2839 while frame.index < bytes.len() {
2840 let next_special = if matches!(quote_context, BraceQuoteContext::Unquoted) {
2841 memchr2(b'{', b'\\', &bytes[frame.index..])
2842 .map(|relative| frame.index + relative)
2843 } else {
2844 memchr(b'{', &bytes[frame.index..]).map(|relative| frame.index + relative)
2845 };
2846
2847 let Some(next_index) = next_special else {
2848 break;
2849 };
2850
2851 if next_index > frame.index {
2852 frame.position = frame
2853 .position
2854 .advanced_by(&frame.text[frame.index..next_index]);
2855 frame.index = next_index;
2856 }
2857
2858 if matches!(quote_context, BraceQuoteContext::Unquoted)
2859 && bytes[frame.index] == b'\\'
2860 {
2861 let escaped_start = frame.index;
2862 frame.index += 1;
2863 if let Some(next) = frame.text[frame.index..].chars().next() {
2864 frame.index += next.len_utf8();
2865 }
2866 frame.position = frame
2867 .position
2868 .advanced_by(&frame.text[escaped_start..frame.index]);
2869 continue;
2870 }
2871
2872 let brace_start = frame.position;
2873 if let Some(len) =
2874 Self::template_placeholder_len(frame.text, frame.index, quote_context)
2875 {
2876 let brace_end =
2877 brace_start.advanced_by(&frame.text[frame.index..frame.index + len]);
2878 out.push(BraceSyntax {
2879 kind: BraceSyntaxKind::TemplatePlaceholder,
2880 span: Span::from_positions(brace_start, brace_end),
2881 quote_context,
2882 });
2883 frame.position = brace_end;
2884 frame.index += len;
2885 continue;
2886 }
2887
2888 if let Some((len, kind)) =
2889 Self::brace_construct_len(frame.text, frame.index, quote_context)
2890 {
2891 let brace_end =
2892 brace_start.advanced_by(&frame.text[frame.index..frame.index + len]);
2893 out.push(BraceSyntax {
2894 kind,
2895 span: Span::from_positions(brace_start, brace_end),
2896 quote_context,
2897 });
2898
2899 frame.position = brace_end;
2900 frame.index += len;
2901
2902 if len > 2 {
2903 let inner_start = frame.index - len + '{'.len_utf8();
2904 let inner_end = frame.index - '}'.len_utf8();
2905 if inner_start < inner_end {
2906 let inner_base = brace_start.advanced_by("{");
2907 work.push(frame);
2908 work.push(ScanFrame {
2909 text: &frame.text[inner_start..inner_end],
2910 index: 0,
2911 position: inner_base,
2912 });
2913 break;
2914 }
2915 }
2916
2917 continue;
2918 }
2919
2920 frame.position.advance('{');
2921 frame.index += '{'.len_utf8();
2922 }
2923 }
2924 }
2925
2926 fn text_position(base: Position, text: &str, offset: usize) -> Position {
2927 base.advanced_by(&text[..offset])
2928 }
2929
2930 fn template_placeholder_len(
2931 text: &str,
2932 start: usize,
2933 quote_context: BraceQuoteContext,
2934 ) -> Option<usize> {
2935 text.get(start..).filter(|rest| rest.starts_with("{{"))?;
2936
2937 let mut index = start + "{{".len();
2938 let mut depth = 1usize;
2939
2940 while index < text.len() {
2941 if matches!(quote_context, BraceQuoteContext::Unquoted)
2942 && text[index..].starts_with('\\')
2943 {
2944 index += 1;
2945 if let Some(next) = text[index..].chars().next() {
2946 index += next.len_utf8();
2947 }
2948 continue;
2949 }
2950
2951 if text[index..].starts_with("{{") {
2952 depth += 1;
2953 index += "{{".len();
2954 continue;
2955 }
2956
2957 if text[index..].starts_with("}}") {
2958 depth -= 1;
2959 index += "}}".len();
2960 if depth == 0 {
2961 return Some(index - start);
2962 }
2963 continue;
2964 }
2965
2966 index += text[index..].chars().next()?.len_utf8();
2967 }
2968
2969 None
2970 }
2971
2972 fn brace_construct_len(
2973 text: &str,
2974 start: usize,
2975 quote_context: BraceQuoteContext,
2976 ) -> Option<(usize, BraceSyntaxKind)> {
2977 text.get(start..).filter(|rest| rest.starts_with('{'))?;
2978
2979 #[derive(Clone, Copy, PartialEq, Eq)]
2980 enum QuoteState {
2981 Single,
2982 Double,
2983 }
2984
2985 let mut index = start + '{'.len_utf8();
2986 let mut depth = 1usize;
2987 let mut has_comma = false;
2988 let mut has_dot_dot = false;
2989 let mut saw_unquoted_whitespace = false;
2990 let mut prev_char = None;
2991 let mut quote_state = None;
2992
2993 while index < text.len() {
2994 if matches!(quote_context, BraceQuoteContext::Unquoted)
2995 && quote_state.is_none()
2996 && text[index..].starts_with('\\')
2997 {
2998 index += 1;
2999 if let Some(next) = text[index..].chars().next() {
3000 index += next.len_utf8();
3001 }
3002 prev_char = None;
3003 continue;
3004 }
3005
3006 let ch = text[index..].chars().next()?;
3007 index += ch.len_utf8();
3008
3009 if matches!(quote_context, BraceQuoteContext::Unquoted) {
3010 match quote_state {
3011 None => match ch {
3012 '\'' => {
3013 quote_state = Some(QuoteState::Single);
3014 prev_char = None;
3015 continue;
3016 }
3017 '"' => {
3018 quote_state = Some(QuoteState::Double);
3019 prev_char = None;
3020 continue;
3021 }
3022 '{' => depth += 1,
3023 '}' => {
3024 depth -= 1;
3025 if depth == 0 {
3026 let kind = if saw_unquoted_whitespace {
3027 BraceSyntaxKind::Literal
3028 } else if has_comma {
3029 BraceSyntaxKind::Expansion(BraceExpansionKind::CommaList)
3030 } else if has_dot_dot {
3031 BraceSyntaxKind::Expansion(BraceExpansionKind::Sequence)
3032 } else {
3033 BraceSyntaxKind::Literal
3034 };
3035 return Some((index - start, kind));
3036 }
3037 }
3038 ',' if depth == 1 => has_comma = true,
3039 '.' if depth == 1 && prev_char == Some('.') => has_dot_dot = true,
3040 c if c.is_whitespace() => saw_unquoted_whitespace = true,
3041 _ => {}
3042 },
3043 Some(QuoteState::Single) => {
3044 if ch == '\'' {
3045 quote_state = None;
3046 }
3047 }
3048 Some(QuoteState::Double) => match ch {
3049 '\\' => {
3050 if let Some(next) = text[index..].chars().next() {
3051 index += next.len_utf8();
3052 }
3053 prev_char = None;
3054 continue;
3055 }
3056 '"' => quote_state = None,
3057 _ => {}
3058 },
3059 }
3060 } else {
3061 match ch {
3062 '{' => depth += 1,
3063 '}' => {
3064 depth -= 1;
3065 if depth == 0 {
3066 let kind = if has_comma {
3067 BraceSyntaxKind::Expansion(BraceExpansionKind::CommaList)
3068 } else if has_dot_dot {
3069 BraceSyntaxKind::Expansion(BraceExpansionKind::Sequence)
3070 } else {
3071 BraceSyntaxKind::Literal
3072 };
3073 return Some((index - start, kind));
3074 }
3075 }
3076 ',' if depth == 1 => has_comma = true,
3077 '.' if depth == 1 && prev_char == Some('.') => has_dot_dot = true,
3078 _ => {}
3079 }
3080 }
3081
3082 prev_char = Some(ch);
3083 }
3084
3085 None
3086 }
3087
3088 fn maybe_parse_zsh_qualified_glob_word(
3089 &mut self,
3090 text: &str,
3091 span: Span,
3092 source_backed: bool,
3093 ) -> Option<Word> {
3094 if !self.zsh_glob_qualifiers_enabled_at(span.start.offset)
3095 || text.is_empty()
3096 || text.contains('=')
3097 || text.contains(['\x00', '\\', '\'', '"', '$', '`'])
3098 || text.chars().any(char::is_whitespace)
3099 {
3100 return None;
3101 }
3102
3103 let (segments, qualifiers) =
3104 self.parse_zsh_qualified_glob_segments(text, span, source_backed)?;
3105 if !segments.iter().any(|segment| {
3106 matches!(
3107 segment,
3108 ZshGlobSegment::Pattern(pattern) if Self::pattern_has_glob_syntax(pattern)
3109 )
3110 }) {
3111 return None;
3112 }
3113
3114 Some(self.word_with_parts(
3115 vec![WordPartNode::new(
3116 WordPart::ZshQualifiedGlob(ZshQualifiedGlob {
3117 span,
3118 segments,
3119 qualifiers,
3120 }),
3121 span,
3122 )],
3123 span,
3124 ))
3125 }
3126
3127 fn parse_zsh_qualified_glob_segments(
3128 &mut self,
3129 text: &str,
3130 span: Span,
3131 source_backed: bool,
3132 ) -> Option<(Vec<ZshGlobSegment>, Option<ZshGlobQualifierGroup>)> {
3133 let mut segments = Vec::new();
3134 let mut qualifiers = None;
3135 let mut pattern_start = 0usize;
3136 let mut index = 0usize;
3137
3138 while index < text.len() {
3139 if text[index..].starts_with("(#") {
3140 if let Some((len, control)) =
3141 self.parse_zsh_inline_glob_control(text, span.start, index)
3142 {
3143 self.push_zsh_pattern_segment(
3144 &mut segments,
3145 text,
3146 span.start,
3147 pattern_start,
3148 index,
3149 source_backed,
3150 );
3151 segments.push(ZshGlobSegment::InlineControl(control));
3152 index += len;
3153 pattern_start = index;
3154 continue;
3155 }
3156
3157 let suffix_start = Self::text_position(span.start, text, index);
3158 if let Some(group) = self.parse_zsh_terminal_glob_qualifier_group(
3159 &text[index..],
3160 suffix_start,
3161 source_backed,
3162 ) {
3163 self.push_zsh_pattern_segment(
3164 &mut segments,
3165 text,
3166 span.start,
3167 pattern_start,
3168 index,
3169 source_backed,
3170 );
3171 qualifiers = Some(group);
3172 index = text.len();
3173 pattern_start = index;
3174 break;
3175 }
3176
3177 return None;
3178 }
3179
3180 if text[index..].starts_with('(') {
3181 let suffix_start = Self::text_position(span.start, text, index);
3182 if let Some(group) = self.parse_zsh_terminal_glob_qualifier_group(
3183 &text[index..],
3184 suffix_start,
3185 source_backed,
3186 ) && matches!(group.kind, ZshGlobQualifierKind::Classic)
3187 {
3188 self.push_zsh_pattern_segment(
3189 &mut segments,
3190 text,
3191 span.start,
3192 pattern_start,
3193 index,
3194 source_backed,
3195 );
3196 qualifiers = Some(group);
3197 index = text.len();
3198 pattern_start = index;
3199 break;
3200 }
3201 }
3202
3203 index += text[index..].chars().next()?.len_utf8();
3204 }
3205
3206 self.push_zsh_pattern_segment(
3207 &mut segments,
3208 text,
3209 span.start,
3210 pattern_start,
3211 text.len(),
3212 source_backed,
3213 );
3214
3215 segments
3216 .iter()
3217 .any(|segment| matches!(segment, ZshGlobSegment::Pattern(_)))
3218 .then_some((segments, qualifiers))
3219 }
3220
3221 fn push_zsh_pattern_segment(
3222 &mut self,
3223 segments: &mut Vec<ZshGlobSegment>,
3224 text: &str,
3225 base: Position,
3226 start: usize,
3227 end: usize,
3228 source_backed: bool,
3229 ) {
3230 if start >= end {
3231 return;
3232 }
3233
3234 let start_position = Self::text_position(base, text, start);
3235 let end_position = Self::text_position(base, text, end);
3236 let span = Span::from_positions(start_position, end_position);
3237 let pattern_word =
3238 self.decode_word_text(&text[start..end], span, span.start, source_backed);
3239 segments.push(ZshGlobSegment::Pattern(
3240 self.pattern_from_word(&pattern_word),
3241 ));
3242 }
3243
3244 fn parse_zsh_inline_glob_control(
3245 &self,
3246 text: &str,
3247 base: Position,
3248 start: usize,
3249 ) -> Option<(usize, ZshInlineGlobControl)> {
3250 let (len, control) = if text[start..].starts_with("(#i)") {
3251 (
3252 "(#i)".len(),
3253 ZshInlineGlobControl::CaseInsensitive {
3254 span: Span::from_positions(
3255 Self::text_position(base, text, start),
3256 Self::text_position(base, text, start + "(#i)".len()),
3257 ),
3258 },
3259 )
3260 } else if text[start..].starts_with("(#b)") {
3261 (
3262 "(#b)".len(),
3263 ZshInlineGlobControl::Backreferences {
3264 span: Span::from_positions(
3265 Self::text_position(base, text, start),
3266 Self::text_position(base, text, start + "(#b)".len()),
3267 ),
3268 },
3269 )
3270 } else if text[start..].starts_with("(#s)") {
3271 (
3272 "(#s)".len(),
3273 ZshInlineGlobControl::StartAnchor {
3274 span: Span::from_positions(
3275 Self::text_position(base, text, start),
3276 Self::text_position(base, text, start + "(#s)".len()),
3277 ),
3278 },
3279 )
3280 } else if text[start..].starts_with("(#e)") {
3281 (
3282 "(#e)".len(),
3283 ZshInlineGlobControl::EndAnchor {
3284 span: Span::from_positions(
3285 Self::text_position(base, text, start),
3286 Self::text_position(base, text, start + "(#e)".len()),
3287 ),
3288 },
3289 )
3290 } else {
3291 return None;
3292 };
3293
3294 Some((len, control))
3295 }
3296
3297 fn parse_zsh_terminal_glob_qualifier_group(
3298 &self,
3299 text: &str,
3300 base: Position,
3301 source_backed: bool,
3302 ) -> Option<ZshGlobQualifierGroup> {
3303 let (kind, prefix_len, inner) = if let Some(inner) = text
3304 .strip_prefix("(#q")
3305 .and_then(|rest| rest.strip_suffix(')'))
3306 {
3307 (ZshGlobQualifierKind::HashQ, "(#q".len(), inner)
3308 } else {
3309 let inner = text.strip_prefix('(')?.strip_suffix(')')?;
3310 (ZshGlobQualifierKind::Classic, "(".len(), inner)
3311 };
3312
3313 let fragments = self.parse_zsh_glob_qualifier_fragments(
3314 inner,
3315 Self::text_position(base, text, prefix_len),
3316 source_backed,
3317 )?;
3318
3319 Some(ZshGlobQualifierGroup {
3320 span: Span::from_positions(base, Self::text_position(base, text, text.len())),
3321 kind,
3322 fragments,
3323 })
3324 }
3325
3326 fn parse_zsh_glob_qualifier_fragments(
3327 &self,
3328 text: &str,
3329 base: Position,
3330 source_backed: bool,
3331 ) -> Option<Vec<ZshGlobQualifier>> {
3332 let mut fragments = Vec::new();
3333 let mut index = 0;
3334 let mut saw_non_letter_fragment = false;
3335
3336 while index < text.len() {
3337 let start = index;
3338 let ch = text[index..].chars().next()?;
3339
3340 match ch {
3341 '^' => {
3342 index += ch.len_utf8();
3343 fragments.push(ZshGlobQualifier::Negation {
3344 span: Span::from_positions(
3345 Self::text_position(base, text, start),
3346 Self::text_position(base, text, index),
3347 ),
3348 });
3349 saw_non_letter_fragment = true;
3350 }
3351 '.' | '/' | '-' | 'A'..='Z' => {
3352 index += ch.len_utf8();
3353 fragments.push(ZshGlobQualifier::Flag {
3354 name: ch,
3355 span: Span::from_positions(
3356 Self::text_position(base, text, start),
3357 Self::text_position(base, text, index),
3358 ),
3359 });
3360 saw_non_letter_fragment = true;
3361 }
3362 '[' => {
3363 index += ch.len_utf8();
3364 let number_start = index;
3365 while matches!(text[index..].chars().next(), Some('0'..='9')) {
3366 index += 1;
3367 }
3368 if number_start == index {
3369 return None;
3370 }
3371
3372 let start_text = self.zsh_glob_qualifier_source_text(
3373 text,
3374 base,
3375 number_start,
3376 index,
3377 source_backed,
3378 );
3379 let end_text = if text[index..].starts_with(',') {
3380 index += 1;
3381 let range_start = index;
3382 while matches!(text[index..].chars().next(), Some('0'..='9')) {
3383 index += 1;
3384 }
3385 if range_start == index {
3386 return None;
3387 }
3388 Some(self.zsh_glob_qualifier_source_text(
3389 text,
3390 base,
3391 range_start,
3392 index,
3393 source_backed,
3394 ))
3395 } else {
3396 None
3397 };
3398
3399 if !text[index..].starts_with(']') {
3400 return None;
3401 }
3402 index += "]".len();
3403 fragments.push(ZshGlobQualifier::NumericArgument {
3404 span: Span::from_positions(
3405 Self::text_position(base, text, start),
3406 Self::text_position(base, text, index),
3407 ),
3408 start: start_text,
3409 end: end_text,
3410 });
3411 saw_non_letter_fragment = true;
3412 }
3413 'a'..='z' => {
3414 index += ch.len_utf8();
3415 while matches!(text[index..].chars().next(), Some('a'..='z' | 'A'..='Z')) {
3416 index += 1;
3417 }
3418 if index - start <= 1 {
3419 return None;
3420 }
3421 fragments.push(ZshGlobQualifier::LetterSequence {
3422 text: self.zsh_glob_qualifier_source_text(
3423 text,
3424 base,
3425 start,
3426 index,
3427 source_backed,
3428 ),
3429 span: Span::from_positions(
3430 Self::text_position(base, text, start),
3431 Self::text_position(base, text, index),
3432 ),
3433 });
3434 }
3435 _ => return None,
3436 }
3437 }
3438
3439 (!fragments.is_empty() && saw_non_letter_fragment).then_some(fragments)
3440 }
3441
3442 fn zsh_glob_qualifier_source_text(
3443 &self,
3444 text: &str,
3445 base: Position,
3446 start: usize,
3447 end: usize,
3448 source_backed: bool,
3449 ) -> SourceText {
3450 let span = Span::from_positions(
3451 Self::text_position(base, text, start),
3452 Self::text_position(base, text, end),
3453 );
3454 if source_backed {
3455 SourceText::source(span)
3456 } else {
3457 SourceText::cooked(span, text[start..end].to_string())
3458 }
3459 }
3460
3461 fn pattern_has_glob_syntax(pattern: &Pattern) -> bool {
3462 pattern.parts.iter().any(|part| match &part.kind {
3463 PatternPart::AnyString | PatternPart::AnyChar | PatternPart::CharClass(_) => true,
3464 PatternPart::Group { .. } => true,
3465 PatternPart::Word(word) => Self::pattern_has_glob_word(word),
3466 PatternPart::Literal(_) => false,
3467 })
3468 }
3469
3470 fn pattern_has_glob_word(word: &Word) -> bool {
3471 word.parts
3472 .iter()
3473 .any(|part| !matches!(part.kind, WordPart::Literal(_)))
3474 }
3475
3476 fn simple_word_from_token(&mut self, token: &LexedToken<'_>, span: Span) -> Option<Word> {
3477 let word = token.word()?;
3478 let source_backed = !token.flags.is_synthetic();
3479
3480 if self.zsh_glob_qualifiers_enabled_at(span.start.offset)
3481 && let Some(segment) = word.single_segment()
3482 && segment.kind() == LexedWordSegmentKind::Plain
3483 && let Some(word) = self.maybe_parse_zsh_qualified_glob_word(
3484 segment.as_str(),
3485 span,
3486 segment.span().is_some() && source_backed && segment.text_is_source_backed(),
3487 )
3488 {
3489 return Some(word);
3490 }
3491 let mut parts = Self::word_part_buffer_with_capacity(word.segments().size_hint().0);
3492
3493 for segment in word.segments() {
3494 let source_backed = segment.span().is_some() && !token.flags.is_synthetic();
3495 let content_span = Self::segment_content_span(segment, span);
3496 let raw_text = segment.as_str();
3497 let use_source_slice = source_backed
3498 && match segment.kind() {
3499 LexedWordSegmentKind::Plain => {
3500 segment.text_is_source_backed()
3501 || raw_text.contains("${") && raw_text.contains('/')
3502 || !raw_text.contains("$(")
3503 }
3504 _ => segment.text_is_source_backed(),
3505 };
3506 let text = if use_source_slice {
3507 content_span.slice(self.input)
3508 } else {
3509 raw_text
3510 };
3511 match segment.kind() {
3512 LexedWordSegmentKind::Plain
3513 | LexedWordSegmentKind::DoubleQuoted
3514 | LexedWordSegmentKind::DollarDoubleQuoted
3515 if Self::word_text_needs_parse(text) =>
3516 {
3517 return None;
3518 }
3519 LexedWordSegmentKind::Plain
3520 | LexedWordSegmentKind::SingleQuoted
3521 | LexedWordSegmentKind::DollarSingleQuoted
3522 | LexedWordSegmentKind::DoubleQuoted
3523 | LexedWordSegmentKind::DollarDoubleQuoted => {}
3524 LexedWordSegmentKind::Composite => return None,
3525 }
3526
3527 let wrapper_span = Self::segment_wrapper_span(segment, span);
3528 let part = match segment.kind() {
3529 LexedWordSegmentKind::Plain => {
3530 self.literal_part_from_text(text, content_span, source_backed)
3531 }
3532 LexedWordSegmentKind::SingleQuoted => {
3533 self.single_quoted_part_from_text(text, content_span, wrapper_span, false)
3534 }
3535 LexedWordSegmentKind::DollarSingleQuoted => {
3536 self.single_quoted_part_from_text(text, content_span, wrapper_span, true)
3537 }
3538 LexedWordSegmentKind::DoubleQuoted => self.double_quoted_literal_part_from_text(
3539 text,
3540 content_span,
3541 wrapper_span,
3542 source_backed,
3543 false,
3544 ),
3545 LexedWordSegmentKind::DollarDoubleQuoted => self
3546 .double_quoted_literal_part_from_text(
3547 text,
3548 content_span,
3549 wrapper_span,
3550 source_backed,
3551 true,
3552 ),
3553 LexedWordSegmentKind::Composite => unreachable!(),
3554 };
3555 Self::push_word_part_node(&mut parts, part);
3556 }
3557
3558 Some(self.word_with_part_buffer(parts, span))
3559 }
3560
3561 fn segment_content_span(segment: &LexedWordSegment<'_>, fallback: Span) -> Span {
3562 segment
3563 .span()
3564 .or_else(|| segment.wrapper_span())
3565 .unwrap_or(fallback)
3566 }
3567
3568 fn segment_wrapper_span(segment: &LexedWordSegment<'_>, fallback: Span) -> Span {
3569 segment
3570 .wrapper_span()
3571 .or_else(|| segment.span())
3572 .unwrap_or(fallback)
3573 }
3574
3575 fn literal_part_from_text(&self, text: &str, span: Span, source_backed: bool) -> WordPartNode {
3576 WordPartNode::new(
3577 WordPart::Literal(self.literal_text_from_str(
3578 text,
3579 span.start,
3580 span.end,
3581 source_backed,
3582 )),
3583 span,
3584 )
3585 }
3586
3587 fn single_quoted_part_from_text(
3588 &self,
3589 text: &str,
3590 content_span: Span,
3591 wrapper_span: Span,
3592 dollar: bool,
3593 ) -> WordPartNode {
3594 WordPartNode::new(
3595 WordPart::SingleQuoted {
3596 value: self.source_text_from_str(text, content_span.start, content_span.end),
3597 dollar,
3598 },
3599 wrapper_span,
3600 )
3601 }
3602
3603 fn double_quoted_literal_part_from_text(
3604 &self,
3605 text: &str,
3606 content_span: Span,
3607 wrapper_span: Span,
3608 source_backed: bool,
3609 dollar: bool,
3610 ) -> WordPartNode {
3611 WordPartNode::new(
3612 WordPart::DoubleQuoted {
3613 parts: vec![self.literal_part_from_text(text, content_span, source_backed)],
3614 dollar,
3615 },
3616 wrapper_span,
3617 )
3618 }
3619
3620 fn decode_word_from_token(&mut self, token: &LexedToken<'_>, span: Span) -> Option<Word> {
3621 let word = token.word()?;
3622
3623 if word.single_segment().is_none()
3624 && !token.flags.is_synthetic()
3625 && let Some(source_text) = token.source_slice(self.input)
3626 {
3627 return Some(self.parse_word_with_context(source_text, span, span.start, true));
3628 }
3629
3630 if let Some(segment) = word.single_segment() {
3631 let content_span = Self::segment_content_span(segment, span);
3632 let wrapper_span = Self::segment_wrapper_span(segment, span);
3633 let source_backed = segment.span().is_some() && !token.flags.is_synthetic();
3634 let raw_text = segment.as_str();
3635 let use_source_slice = source_backed
3636 && match segment.kind() {
3637 LexedWordSegmentKind::Plain => {
3638 segment.text_is_source_backed()
3639 || raw_text.contains("${") && raw_text.contains('/')
3640 || !raw_text.contains("$(")
3641 }
3642 _ => segment.text_is_source_backed(),
3643 };
3644 let text = if use_source_slice {
3645 content_span.slice(self.input)
3646 } else {
3647 raw_text
3648 };
3649 let decode_text = if source_backed
3650 && !self.source_matches(content_span, text)
3651 && matches!(
3652 segment.kind(),
3653 LexedWordSegmentKind::DoubleQuoted | LexedWordSegmentKind::DollarDoubleQuoted
3654 )
3655 && (!text.contains("$(") || text.contains("$(("))
3656 {
3657 content_span.slice(self.input)
3658 } else {
3659 text
3660 };
3661 let preserve_escaped_expansion_literals =
3662 source_backed && self.source_matches(content_span, decode_text);
3663
3664 return match segment.kind() {
3665 LexedWordSegmentKind::SingleQuoted => Some(self.word_with_single_part(
3666 self.single_quoted_part_from_text(text, content_span, wrapper_span, false),
3667 span,
3668 )),
3669 LexedWordSegmentKind::DollarSingleQuoted => Some(self.word_with_single_part(
3670 self.single_quoted_part_from_text(text, content_span, wrapper_span, true),
3671 span,
3672 )),
3673 LexedWordSegmentKind::Plain if Self::word_text_needs_parse(text) => Some(
3674 self.decode_word_text_preserving_quotes_if_needed_with_escape_mode(
3675 text,
3676 span,
3677 content_span.start,
3678 source_backed,
3679 preserve_escaped_expansion_literals,
3680 ),
3681 ),
3682 LexedWordSegmentKind::DoubleQuoted | LexedWordSegmentKind::DollarDoubleQuoted
3683 if Self::word_text_needs_parse(text) =>
3684 {
3685 let inner = self.decode_quoted_segment_text(
3686 decode_text,
3687 content_span,
3688 content_span.start,
3689 source_backed,
3690 );
3691 Some(self.word_with_single_part(
3692 WordPartNode::new(
3693 WordPart::DoubleQuoted {
3694 parts: inner.parts,
3695 dollar: matches!(
3696 segment.kind(),
3697 LexedWordSegmentKind::DollarDoubleQuoted
3698 ),
3699 },
3700 wrapper_span,
3701 ),
3702 span,
3703 ))
3704 }
3705 LexedWordSegmentKind::Plain => Some(self.word_with_single_part(
3706 self.literal_part_from_text(text, content_span, source_backed),
3707 span,
3708 )),
3709 LexedWordSegmentKind::DoubleQuoted => Some(self.word_with_single_part(
3710 self.double_quoted_literal_part_from_text(
3711 text,
3712 content_span,
3713 wrapper_span,
3714 source_backed,
3715 false,
3716 ),
3717 span,
3718 )),
3719 LexedWordSegmentKind::DollarDoubleQuoted => Some(self.word_with_single_part(
3720 self.double_quoted_literal_part_from_text(
3721 text,
3722 content_span,
3723 wrapper_span,
3724 source_backed,
3725 true,
3726 ),
3727 span,
3728 )),
3729 LexedWordSegmentKind::Composite => None,
3730 };
3731 }
3732
3733 let mut parts = Self::word_part_buffer_with_capacity(word.segments().size_hint().0);
3734 let mut cursor = span.start;
3735
3736 for segment in word.segments() {
3737 let raw_text = segment.as_str();
3738 let content_span = if let Some(segment_span) = segment.span() {
3739 cursor = segment_span.end;
3740 segment_span
3741 } else {
3742 let start = cursor;
3743 let end = start.advanced_by(raw_text);
3744 cursor = end;
3745 Span::from_positions(start, end)
3746 };
3747 let wrapper_span = segment.wrapper_span().unwrap_or(content_span);
3748 let source_backed = segment.span().is_some() && !token.flags.is_synthetic();
3749 let use_source_slice = source_backed
3750 && match segment.kind() {
3751 LexedWordSegmentKind::Plain => {
3752 segment.text_is_source_backed()
3753 || raw_text.contains("${") && raw_text.contains('/')
3754 || !raw_text.contains("$(")
3755 }
3756 _ => segment.text_is_source_backed(),
3757 };
3758 let text = if use_source_slice {
3759 content_span.slice(self.input)
3760 } else {
3761 raw_text
3762 };
3763 let preserve_escaped_expansion_literals = source_backed;
3764
3765 match segment.kind() {
3766 LexedWordSegmentKind::SingleQuoted => Self::push_word_part_node(
3767 &mut parts,
3768 self.single_quoted_part_from_text(text, content_span, wrapper_span, false),
3769 ),
3770 LexedWordSegmentKind::DollarSingleQuoted => Self::push_word_part_node(
3771 &mut parts,
3772 self.single_quoted_part_from_text(text, content_span, wrapper_span, true),
3773 ),
3774 LexedWordSegmentKind::Plain => {
3775 if Self::word_text_needs_parse(text) {
3776 parts.extend(
3777 self.decode_word_text_preserving_quotes_if_needed_with_escape_mode(
3778 text,
3779 content_span,
3780 content_span.start,
3781 source_backed,
3782 preserve_escaped_expansion_literals,
3783 )
3784 .parts,
3785 );
3786 } else {
3787 Self::push_word_part_node(
3788 &mut parts,
3789 self.literal_part_from_text(text, content_span, source_backed),
3790 );
3791 }
3792 }
3793 LexedWordSegmentKind::DoubleQuoted | LexedWordSegmentKind::DollarDoubleQuoted => {
3794 if Self::word_text_needs_parse(text) {
3795 let inner = self.decode_quoted_segment_text(
3796 text,
3797 content_span,
3798 content_span.start,
3799 source_backed,
3800 );
3801 Self::push_word_part_node(
3802 &mut parts,
3803 WordPartNode::new(
3804 WordPart::DoubleQuoted {
3805 parts: inner.parts,
3806 dollar: matches!(
3807 segment.kind(),
3808 LexedWordSegmentKind::DollarDoubleQuoted
3809 ),
3810 },
3811 wrapper_span,
3812 ),
3813 );
3814 } else {
3815 Self::push_word_part_node(
3816 &mut parts,
3817 self.double_quoted_literal_part_from_text(
3818 text,
3819 content_span,
3820 wrapper_span,
3821 source_backed,
3822 matches!(segment.kind(), LexedWordSegmentKind::DollarDoubleQuoted),
3823 ),
3824 );
3825 }
3826 }
3827 LexedWordSegmentKind::Composite => return None,
3828 }
3829 }
3830
3831 Some(self.word_with_part_buffer(parts, span))
3832 }
3833
3834 fn current_word_ref(&mut self) -> Option<&Word> {
3835 if self.current_word_cache.is_none() {
3836 self.current_word_cache = self.current_word();
3837 }
3838
3839 self.current_word_cache.as_ref()
3840 }
3841
3842 fn current_word(&mut self) -> Option<Word> {
3843 if let Some(word) = self.current_word_cache.as_ref() {
3844 return Some(word.clone());
3845 }
3846
3847 if let Some(word) = self.current_zsh_glob_word_from_source() {
3848 self.current_word_cache = Some(word.clone());
3849 return Some(word);
3850 }
3851
3852 let span = self.current_span;
3853 if let Some(token) = self.current_token.clone()
3854 && let Some(word) = self.simple_word_from_token(&token, span)
3855 {
3856 return Some(word);
3857 }
3858
3859 let token = self.current_token.take()?;
3860 let word = self.decode_word_from_token(&token, span);
3861 self.current_token = Some(token);
3862 if let Some(word) = word.as_ref() {
3863 self.current_word_cache = Some(word.clone());
3864 }
3865 word
3866 }
3867
3868 fn take_current_word(&mut self) -> Option<Word> {
3869 if let Some(word) = self.current_word_cache.take() {
3870 return Some(word);
3871 }
3872
3873 if let Some(word) = self.current_zsh_glob_word_from_source() {
3874 return Some(word);
3875 }
3876
3877 let span = self.current_span;
3878 if let Some(token) = self.current_token.clone()
3879 && let Some(word) = self.simple_word_from_token(&token, span)
3880 {
3881 return Some(word);
3882 }
3883
3884 let token = self.current_token.take()?;
3885 let word = self.decode_word_from_token(&token, span);
3886 self.current_token = Some(token);
3887 word
3888 }
3889
3890 fn take_current_word_and_advance(&mut self) -> Option<Word> {
3891 let word = self.take_current_word()?;
3892 self.advance_past_word(&word);
3893 Some(word)
3894 }
3895
3896 fn current_zsh_glob_word_from_source(&mut self) -> Option<Word> {
3897 if !matches!(
3898 self.current_token_kind,
3899 Some(TokenKind::LeftParen | TokenKind::Word)
3900 ) {
3901 return None;
3902 }
3903
3904 let start = self.current_span.start;
3905 if !self.source_word_contains_zsh_glob_control(start) {
3906 return None;
3907 }
3908 let (text, end) = self.scan_source_word(start)?;
3909 if !text.contains("(#") {
3910 return None;
3911 }
3912 let span = Span::from_positions(start, end);
3913 if self.zsh_glob_qualifiers_enabled_at(span.start.offset)
3914 && let Some(word) = self.maybe_parse_zsh_qualified_glob_word(&text, span, true)
3915 {
3916 return Some(word);
3917 }
3918
3919 Some(self.parse_word_with_context(&text, span, start, true))
3920 }
3921
3922 fn source_word_contains_zsh_glob_control(&self, start: Position) -> bool {
3923 if start.offset >= self.input.len() {
3924 return false;
3925 }
3926
3927 let source = &self.input[start.offset..];
3928 let mut chars = source.chars().peekable();
3929 let mut cursor = start;
3930 let mut paren_depth = 0_i32;
3931 let mut brace_depth = 0_i32;
3932 let mut in_single = false;
3933 let mut in_double = false;
3934 let mut in_backtick = false;
3935 let mut escaped = false;
3936 let mut prev_char = None;
3937
3938 while let Some(&ch) = chars.peek() {
3939 if !in_single
3940 && !in_double
3941 && !in_backtick
3942 && paren_depth == 0
3943 && brace_depth == 0
3944 && matches!(ch, ' ' | '\t' | '\n' | ';' | '|' | '&' | '>' | '<' | ')')
3945 {
3946 break;
3947 }
3948
3949 let ch = Self::next_word_char_unwrap(&mut chars, &mut cursor);
3950 if !in_single && !in_double && !in_backtick && prev_char == Some('(') && ch == '#' {
3951 return true;
3952 }
3953
3954 if escaped {
3955 escaped = false;
3956 prev_char = Some(ch);
3957 continue;
3958 }
3959
3960 match ch {
3961 '\\' if !in_single => escaped = true,
3962 '\'' if !in_double => in_single = !in_single,
3963 '"' if !in_single => in_double = !in_double,
3964 '`' if !in_single => in_backtick = !in_backtick,
3965 '(' if !in_single && !in_double => paren_depth += 1,
3966 ')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
3967 '{' if !in_single && !in_double => brace_depth += 1,
3968 '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
3969 _ => {}
3970 }
3971
3972 prev_char = Some(ch);
3973 }
3974
3975 false
3976 }
3977
3978 fn scan_source_word(&self, start: Position) -> Option<(String, Position)> {
3979 if start.offset >= self.input.len() {
3980 return None;
3981 }
3982
3983 let source = &self.input[start.offset..];
3984 let mut chars = source.chars().peekable();
3985 let mut cursor = start;
3986 let mut text = String::new();
3987 let mut paren_depth = 0_i32;
3988 let mut brace_depth = 0_i32;
3989 let mut in_single = false;
3990 let mut in_double = false;
3991 let mut in_backtick = false;
3992 let mut escaped = false;
3993
3994 while let Some(&ch) = chars.peek() {
3995 if !in_single
3996 && !in_double
3997 && !in_backtick
3998 && paren_depth == 0
3999 && brace_depth == 0
4000 && matches!(ch, ' ' | '\t' | '\n' | ';' | '|' | '&' | '>' | '<' | ')')
4001 {
4002 break;
4003 }
4004
4005 let ch = Self::next_word_char_unwrap(&mut chars, &mut cursor);
4006 text.push(ch);
4007
4008 if escaped {
4009 escaped = false;
4010 continue;
4011 }
4012
4013 match ch {
4014 '\\' if !in_single => escaped = true,
4015 '\'' if !in_double => in_single = !in_single,
4016 '"' if !in_single => in_double = !in_double,
4017 '`' if !in_single => in_backtick = !in_backtick,
4018 '(' if !in_single && !in_double => paren_depth += 1,
4019 ')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
4020 '{' if !in_single && !in_double => brace_depth += 1,
4021 '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
4022 _ => {}
4023 }
4024 }
4025
4026 (!text.is_empty()).then_some((text, cursor))
4027 }
4028
4029 fn advance_past_word(&mut self, word: &Word) {
4030 let stop_after_synthetic = self
4031 .current_token
4032 .as_ref()
4033 .is_some_and(|token| token.flags.is_synthetic());
4034 while self.current_token.is_some() && self.current_span.start.offset < word.span.end.offset
4035 {
4036 self.advance();
4037 if stop_after_synthetic
4038 && self
4039 .current_token
4040 .as_ref()
4041 .is_none_or(|token| !token.flags.is_synthetic())
4042 {
4043 break;
4044 }
4045 }
4046 }
4047
4048 fn token_source_like_word_text(&self, token: &LexedToken<'a>) -> Option<Cow<'a, str>> {
4049 token
4050 .source_slice(self.input)
4051 .map(Cow::Borrowed)
4052 .or_else(|| {
4053 (token.span.start.offset <= token.span.end.offset
4054 && token.span.end.offset <= self.input.len())
4055 .then(|| Cow::Borrowed(&self.input[token.span.start.offset..token.span.end.offset]))
4056 })
4057 .or_else(|| token.word_string().map(Cow::Owned))
4058 }
4059
4060 fn current_source_like_word_text(&self) -> Option<Cow<'a, str>> {
4061 self.current_token_kind
4062 .filter(|kind| kind.is_word_like())
4063 .and(self.current_token.as_ref())
4064 .and_then(|token| self.token_source_like_word_text(token))
4065 }
4066
4067 fn current_source_like_word_text_or_error(
4068 &self,
4069 context: &'static str,
4070 ) -> Result<Cow<'a, str>> {
4071 self.current_source_like_word_text().ok_or_else(|| {
4072 self.error(format!(
4073 "internal parser error: missing source text for {context}"
4074 ))
4075 })
4076 }
4077
4078 fn keyword_from_token(token: &LexedToken<'_>) -> Option<Keyword> {
4079 (token.kind == TokenKind::Word)
4080 .then(|| token.word_text())
4081 .flatten()
4082 .and_then(Self::classify_keyword)
4083 }
4084
4085 fn current_conditional_literal_word(&self) -> Option<Word> {
4086 match self.current_token_kind? {
4087 TokenKind::LeftBrace | TokenKind::RightBrace => Some(Word::literal_with_span(
4088 self.input[self.current_span.start.offset..self.current_span.end.offset]
4089 .to_string(),
4090 self.current_span,
4091 )),
4092 _ => None,
4093 }
4094 }
4095
4096 fn current_name_token(&self) -> Option<(Name, Span)> {
4097 self.current_token_kind
4098 .filter(|kind| kind.is_word_like())
4099 .and_then(|_| self.current_word_str())
4100 .map(|word| (Name::from(word), self.current_span))
4101 }
4102
4103 fn current_static_token_text(&self) -> Option<(String, bool)> {
4104 let token = self.current_token.as_ref()?;
4105 let raw_text = token.word_string()?;
4106 let text_had_escape_markers = raw_text.contains('\x00');
4107 let text = if text_had_escape_markers {
4108 raw_text.replace('\x00', "")
4109 } else {
4110 raw_text
4111 };
4112
4113 match token.kind {
4114 TokenKind::LiteralWord => Some((text, true)),
4115 TokenKind::QuotedWord if !Self::word_text_needs_parse(&text) => Some((text, true)),
4116 TokenKind::Word if !Self::word_text_needs_parse(&text) => Some((
4117 text,
4118 token.flags.has_cooked_text() || text_had_escape_markers,
4119 )),
4120 _ => None,
4121 }
4122 }
4123
4124 fn nested_stmt_seq_from_source(&mut self, source: &str, base: Position) -> StmtSeq {
4125 let remaining_depth = self.max_depth.saturating_sub(self.current_depth);
4126 let nested_profile = self
4127 .current_zsh_options()
4128 .cloned()
4129 .map(|options| ShellProfile::with_zsh_options(self.dialect, options))
4130 .unwrap_or_else(|| self.shell_profile.clone());
4131 let inner_parser =
4132 Parser::with_limits_and_profile(source, remaining_depth, self.fuel, nested_profile);
4133 let mut output = inner_parser.parse();
4134 if output.is_ok() {
4135 Self::materialize_stmt_seq_source_backing(&mut output.file.body, source);
4136 Self::rebase_file(&mut output.file, base);
4137 output.file.body
4138 } else {
4139 StmtSeq {
4140 leading_comments: Vec::new(),
4141 stmts: Vec::new(),
4142 trailing_comments: Vec::new(),
4143 span: Span::from_positions(base, base),
4144 }
4145 }
4146 }
4147
4148 fn nested_stmt_seq_from_current_input(&mut self, start: Position, end: Position) -> StmtSeq {
4149 if start.offset > end.offset || end.offset > self.input.len() {
4150 return StmtSeq {
4151 leading_comments: Vec::new(),
4152 stmts: Vec::new(),
4153 trailing_comments: Vec::new(),
4154 span: Span::from_positions(start, start),
4155 };
4156 }
4157 let source = &self.input[start.offset..end.offset];
4158 self.nested_stmt_seq_from_source(source, start)
4159 }
4160
4161 fn merge_optional_span(primary: Span, other: Span) -> Span {
4162 if other == Span::new() {
4163 primary
4164 } else {
4165 primary.merge(other)
4166 }
4167 }
4168
4169 fn redirect_span(operator_span: Span, target: &Word) -> Span {
4170 Self::merge_optional_span(operator_span, target.span)
4171 }
4172
4173 fn optional_span(start: Position, end: Position) -> Option<Span> {
4174 (start.offset < end.offset).then(|| Span::from_positions(start, end))
4175 }
4176
4177 fn split_nested_arithmetic_close(&mut self, context: &'static str) -> Result<Span> {
4178 let right_paren_start = self.current_span.start.advanced_by(")");
4179 self.advance();
4180
4181 if self.at(TokenKind::RightParen) {
4182 let right_paren_span = Span::from_positions(right_paren_start, self.current_span.end);
4183 self.advance();
4184 Ok(right_paren_span)
4185 } else {
4186 Err(Error::parse(format!(
4187 "expected ')' after '))' in {context}"
4188 )))
4189 }
4190 }
4191
4192 fn split_double_semicolon(span: Span) -> (Span, Span) {
4193 let middle = span.start.advanced_by(";");
4194 (
4195 Span::from_positions(span.start, middle),
4196 Span::from_positions(middle, span.end),
4197 )
4198 }
4199
4200 fn split_double_left_paren(span: Span) -> (Span, Span) {
4201 let middle = span.start.advanced_by("(");
4202 (
4203 Span::from_positions(span.start, middle),
4204 Span::from_positions(middle, span.end),
4205 )
4206 }
4207
4208 fn split_double_right_paren(span: Span) -> (Span, Span) {
4209 let middle = span.start.advanced_by(")");
4210 (
4211 Span::from_positions(span.start, middle),
4212 Span::from_positions(middle, span.end),
4213 )
4214 }
4215
4216 fn record_arithmetic_for_separator(
4217 semicolon_span: Span,
4218 segment_start: &mut Position,
4219 init_span: &mut Option<Span>,
4220 first_semicolon_span: &mut Option<Span>,
4221 condition_span: &mut Option<Span>,
4222 second_semicolon_span: &mut Option<Span>,
4223 ) -> Result<()> {
4224 if first_semicolon_span.is_none() {
4225 *init_span = Self::optional_span(*segment_start, semicolon_span.start);
4226 *first_semicolon_span = Some(semicolon_span);
4227 *segment_start = semicolon_span.end;
4228 return Ok(());
4229 }
4230
4231 if second_semicolon_span.is_none() {
4232 *condition_span = Self::optional_span(*segment_start, semicolon_span.start);
4233 *second_semicolon_span = Some(semicolon_span);
4234 *segment_start = semicolon_span.end;
4235 return Ok(());
4236 }
4237
4238 Err(Error::parse(
4239 "unexpected ';' in arithmetic for header".to_string(),
4240 ))
4241 }
4242
4243 fn rebase_file(file: &mut File, base: Position) {
4244 file.span = file.span.rebased(base);
4245 Self::rebase_stmt_seq(&mut file.body, base);
4246 }
4247
4248 fn rebase_comments(comments: &mut [Comment], base: Position) {
4249 let base_offset = TextSize::new(base.offset as u32);
4250 for comment in comments {
4251 comment.range = comment.range.offset_by(base_offset);
4252 }
4253 }
4254
4255 fn rebase_stmt_seq(sequence: &mut StmtSeq, base: Position) {
4256 sequence.span = sequence.span.rebased(base);
4257 Self::rebase_comments(&mut sequence.leading_comments, base);
4258 for stmt in &mut sequence.stmts {
4259 Self::rebase_stmt(stmt, base);
4260 }
4261 Self::rebase_comments(&mut sequence.trailing_comments, base);
4262 }
4263
4264 fn rebase_stmt(stmt: &mut Stmt, base: Position) {
4265 stmt.span = stmt.span.rebased(base);
4266 Self::rebase_comments(&mut stmt.leading_comments, base);
4267 stmt.terminator_span = stmt.terminator_span.map(|span| span.rebased(base));
4268 if let Some(comment) = &mut stmt.inline_comment {
4269 let base_offset = TextSize::new(base.offset as u32);
4270 comment.range = comment.range.offset_by(base_offset);
4271 }
4272 Self::rebase_redirects(&mut stmt.redirects, base);
4273 Self::rebase_ast_command(&mut stmt.command, base);
4274 }
4275
4276 fn rebase_ast_command(command: &mut AstCommand, base: Position) {
4277 match command {
4278 AstCommand::Simple(simple) => {
4279 simple.span = simple.span.rebased(base);
4280 Self::rebase_word(&mut simple.name, base);
4281 Self::rebase_words(&mut simple.args, base);
4282 Self::rebase_assignments(&mut simple.assignments, base);
4283 }
4284 AstCommand::Builtin(builtin) => match builtin {
4285 AstBuiltinCommand::Break(command) => {
4286 command.span = command.span.rebased(base);
4287 if let Some(depth) = &mut command.depth {
4288 Self::rebase_word(depth, base);
4289 }
4290 Self::rebase_words(&mut command.extra_args, base);
4291 Self::rebase_assignments(&mut command.assignments, base);
4292 }
4293 AstBuiltinCommand::Continue(command) => {
4294 command.span = command.span.rebased(base);
4295 if let Some(depth) = &mut command.depth {
4296 Self::rebase_word(depth, base);
4297 }
4298 Self::rebase_words(&mut command.extra_args, base);
4299 Self::rebase_assignments(&mut command.assignments, base);
4300 }
4301 AstBuiltinCommand::Return(command) => {
4302 command.span = command.span.rebased(base);
4303 if let Some(code) = &mut command.code {
4304 Self::rebase_word(code, base);
4305 }
4306 Self::rebase_words(&mut command.extra_args, base);
4307 Self::rebase_assignments(&mut command.assignments, base);
4308 }
4309 AstBuiltinCommand::Exit(command) => {
4310 command.span = command.span.rebased(base);
4311 if let Some(code) = &mut command.code {
4312 Self::rebase_word(code, base);
4313 }
4314 Self::rebase_words(&mut command.extra_args, base);
4315 Self::rebase_assignments(&mut command.assignments, base);
4316 }
4317 },
4318 AstCommand::Decl(decl) => {
4319 decl.span = decl.span.rebased(base);
4320 decl.variant_span = decl.variant_span.rebased(base);
4321 Self::rebase_assignments(&mut decl.assignments, base);
4322 for operand in &mut decl.operands {
4323 match operand {
4324 DeclOperand::Flag(word) | DeclOperand::Dynamic(word) => {
4325 Self::rebase_word(word, base);
4326 }
4327 DeclOperand::Name(name) => Self::rebase_var_ref(name, base),
4328 DeclOperand::Assignment(assignment) => {
4329 Self::rebase_assignments(std::slice::from_mut(assignment), base);
4330 }
4331 }
4332 }
4333 }
4334 AstCommand::Binary(binary) => {
4335 binary.span = binary.span.rebased(base);
4336 binary.op_span = binary.op_span.rebased(base);
4337 Self::rebase_stmt(binary.left.as_mut(), base);
4338 Self::rebase_stmt(binary.right.as_mut(), base);
4339 }
4340 AstCommand::Compound(compound) => Self::rebase_compound(compound, base),
4341 AstCommand::Function(function) => {
4342 function.span = function.span.rebased(base);
4343 if let Some(span) = &mut function.header.function_keyword_span {
4344 *span = span.rebased(base);
4345 }
4346 if let Some(span) = &mut function.header.trailing_parens_span {
4347 *span = span.rebased(base);
4348 }
4349 for entry in &mut function.header.entries {
4350 Self::rebase_word(&mut entry.word, base);
4351 }
4352 Self::rebase_stmt(function.body.as_mut(), base);
4353 }
4354 AstCommand::AnonymousFunction(function) => {
4355 function.span = function.span.rebased(base);
4356 function.surface = match function.surface {
4357 AnonymousFunctionSurface::FunctionKeyword {
4358 function_keyword_span,
4359 } => AnonymousFunctionSurface::FunctionKeyword {
4360 function_keyword_span: function_keyword_span.rebased(base),
4361 },
4362 AnonymousFunctionSurface::Parens { parens_span } => {
4363 AnonymousFunctionSurface::Parens {
4364 parens_span: parens_span.rebased(base),
4365 }
4366 }
4367 };
4368 Self::rebase_stmt(function.body.as_mut(), base);
4369 Self::rebase_words(&mut function.args, base);
4370 }
4371 }
4372 }
4373
4374 fn rebase_subscript(subscript: &mut Subscript, base: Position) {
4375 subscript.text.rebased(base);
4376 if let Some(raw) = &mut subscript.raw {
4377 raw.rebased(base);
4378 }
4379 if let Some(word) = &mut subscript.word_ast {
4380 Self::rebase_word(word, base);
4381 }
4382 if let Some(expr) = &mut subscript.arithmetic_ast {
4383 Self::rebase_arithmetic_expr(expr, base);
4384 }
4385 }
4386
4387 fn rebase_var_ref(reference: &mut VarRef, base: Position) {
4388 reference.span = reference.span.rebased(base);
4389 reference.name_span = reference.name_span.rebased(base);
4390 if let Some(subscript) = &mut reference.subscript {
4391 Self::rebase_subscript(subscript, base);
4392 }
4393 }
4394
4395 fn rebase_array_expr(array: &mut ArrayExpr, base: Position) {
4396 array.span = array.span.rebased(base);
4397 for element in &mut array.elements {
4398 match element {
4399 ArrayElem::Sequential(word) => Self::rebase_word(word, base),
4400 ArrayElem::Keyed { key, value } | ArrayElem::KeyedAppend { key, value } => {
4401 Self::rebase_subscript(key, base);
4402 Self::rebase_word(value, base);
4403 }
4404 }
4405 }
4406 }
4407
4408 fn rebase_compound(compound: &mut CompoundCommand, base: Position) {
4409 match compound {
4410 CompoundCommand::If(command) => {
4411 command.span = command.span.rebased(base);
4412 command.syntax = match command.syntax {
4413 IfSyntax::ThenFi { then_span, fi_span } => IfSyntax::ThenFi {
4414 then_span: then_span.rebased(base),
4415 fi_span: fi_span.rebased(base),
4416 },
4417 IfSyntax::Brace {
4418 left_brace_span,
4419 right_brace_span,
4420 } => IfSyntax::Brace {
4421 left_brace_span: left_brace_span.rebased(base),
4422 right_brace_span: right_brace_span.rebased(base),
4423 },
4424 };
4425 Self::rebase_stmt_seq(&mut command.condition, base);
4426 Self::rebase_stmt_seq(&mut command.then_branch, base);
4427 for (condition, body) in &mut command.elif_branches {
4428 Self::rebase_stmt_seq(condition, base);
4429 Self::rebase_stmt_seq(body, base);
4430 }
4431 if let Some(else_branch) = &mut command.else_branch {
4432 Self::rebase_stmt_seq(else_branch, base);
4433 }
4434 }
4435 CompoundCommand::For(command) => {
4436 command.span = command.span.rebased(base);
4437 for target in &mut command.targets {
4438 target.span = target.span.rebased(base);
4439 }
4440 if let Some(words) = &mut command.words {
4441 Self::rebase_words(words, base);
4442 }
4443 command.syntax = match command.syntax {
4444 ForSyntax::InDoDone {
4445 in_span,
4446 do_span,
4447 done_span,
4448 } => ForSyntax::InDoDone {
4449 in_span: in_span.map(|span| span.rebased(base)),
4450 do_span: do_span.rebased(base),
4451 done_span: done_span.rebased(base),
4452 },
4453 ForSyntax::InDirect { in_span } => ForSyntax::InDirect {
4454 in_span: in_span.map(|span| span.rebased(base)),
4455 },
4456 ForSyntax::InBrace {
4457 in_span,
4458 left_brace_span,
4459 right_brace_span,
4460 } => ForSyntax::InBrace {
4461 in_span: in_span.map(|span| span.rebased(base)),
4462 left_brace_span: left_brace_span.rebased(base),
4463 right_brace_span: right_brace_span.rebased(base),
4464 },
4465 ForSyntax::ParenDoDone {
4466 left_paren_span,
4467 right_paren_span,
4468 do_span,
4469 done_span,
4470 } => ForSyntax::ParenDoDone {
4471 left_paren_span: left_paren_span.rebased(base),
4472 right_paren_span: right_paren_span.rebased(base),
4473 do_span: do_span.rebased(base),
4474 done_span: done_span.rebased(base),
4475 },
4476 ForSyntax::ParenDirect {
4477 left_paren_span,
4478 right_paren_span,
4479 } => ForSyntax::ParenDirect {
4480 left_paren_span: left_paren_span.rebased(base),
4481 right_paren_span: right_paren_span.rebased(base),
4482 },
4483 ForSyntax::ParenBrace {
4484 left_paren_span,
4485 right_paren_span,
4486 left_brace_span,
4487 right_brace_span,
4488 } => ForSyntax::ParenBrace {
4489 left_paren_span: left_paren_span.rebased(base),
4490 right_paren_span: right_paren_span.rebased(base),
4491 left_brace_span: left_brace_span.rebased(base),
4492 right_brace_span: right_brace_span.rebased(base),
4493 },
4494 };
4495 Self::rebase_stmt_seq(&mut command.body, base);
4496 }
4497 CompoundCommand::Repeat(command) => {
4498 command.span = command.span.rebased(base);
4499 Self::rebase_word(&mut command.count, base);
4500 command.syntax = match command.syntax {
4501 RepeatSyntax::DoDone { do_span, done_span } => RepeatSyntax::DoDone {
4502 do_span: do_span.rebased(base),
4503 done_span: done_span.rebased(base),
4504 },
4505 RepeatSyntax::Direct => RepeatSyntax::Direct,
4506 RepeatSyntax::Brace {
4507 left_brace_span,
4508 right_brace_span,
4509 } => RepeatSyntax::Brace {
4510 left_brace_span: left_brace_span.rebased(base),
4511 right_brace_span: right_brace_span.rebased(base),
4512 },
4513 };
4514 Self::rebase_stmt_seq(&mut command.body, base);
4515 }
4516 CompoundCommand::Foreach(command) => {
4517 command.span = command.span.rebased(base);
4518 command.variable_span = command.variable_span.rebased(base);
4519 Self::rebase_words(&mut command.words, base);
4520 command.syntax = match command.syntax {
4521 ForeachSyntax::ParenBrace {
4522 left_paren_span,
4523 right_paren_span,
4524 left_brace_span,
4525 right_brace_span,
4526 } => ForeachSyntax::ParenBrace {
4527 left_paren_span: left_paren_span.rebased(base),
4528 right_paren_span: right_paren_span.rebased(base),
4529 left_brace_span: left_brace_span.rebased(base),
4530 right_brace_span: right_brace_span.rebased(base),
4531 },
4532 ForeachSyntax::InDoDone {
4533 in_span,
4534 do_span,
4535 done_span,
4536 } => ForeachSyntax::InDoDone {
4537 in_span: in_span.rebased(base),
4538 do_span: do_span.rebased(base),
4539 done_span: done_span.rebased(base),
4540 },
4541 };
4542 Self::rebase_stmt_seq(&mut command.body, base);
4543 }
4544 CompoundCommand::ArithmeticFor(command) => {
4545 command.span = command.span.rebased(base);
4546 command.left_paren_span = command.left_paren_span.rebased(base);
4547 command.init_span = command.init_span.map(|span| span.rebased(base));
4548 if let Some(expr) = &mut command.init_ast {
4549 Self::rebase_arithmetic_expr(expr, base);
4550 }
4551 command.first_semicolon_span = command.first_semicolon_span.rebased(base);
4552 command.condition_span = command.condition_span.map(|span| span.rebased(base));
4553 if let Some(expr) = &mut command.condition_ast {
4554 Self::rebase_arithmetic_expr(expr, base);
4555 }
4556 command.second_semicolon_span = command.second_semicolon_span.rebased(base);
4557 command.step_span = command.step_span.map(|span| span.rebased(base));
4558 if let Some(expr) = &mut command.step_ast {
4559 Self::rebase_arithmetic_expr(expr, base);
4560 }
4561 command.right_paren_span = command.right_paren_span.rebased(base);
4562 Self::rebase_stmt_seq(&mut command.body, base);
4563 }
4564 CompoundCommand::While(command) => {
4565 command.span = command.span.rebased(base);
4566 Self::rebase_stmt_seq(&mut command.condition, base);
4567 Self::rebase_stmt_seq(&mut command.body, base);
4568 }
4569 CompoundCommand::Until(command) => {
4570 command.span = command.span.rebased(base);
4571 Self::rebase_stmt_seq(&mut command.condition, base);
4572 Self::rebase_stmt_seq(&mut command.body, base);
4573 }
4574 CompoundCommand::Case(command) => {
4575 command.span = command.span.rebased(base);
4576 Self::rebase_word(&mut command.word, base);
4577 for case in &mut command.cases {
4578 Self::rebase_patterns(&mut case.patterns, base);
4579 Self::rebase_stmt_seq(&mut case.body, base);
4580 }
4581 }
4582 CompoundCommand::Select(command) => {
4583 command.span = command.span.rebased(base);
4584 command.variable_span = command.variable_span.rebased(base);
4585 Self::rebase_words(&mut command.words, base);
4586 Self::rebase_stmt_seq(&mut command.body, base);
4587 }
4588 CompoundCommand::Subshell(commands) | CompoundCommand::BraceGroup(commands) => {
4589 Self::rebase_stmt_seq(commands, base);
4590 }
4591 CompoundCommand::Arithmetic(command) => {
4592 command.span = command.span.rebased(base);
4593 command.left_paren_span = command.left_paren_span.rebased(base);
4594 command.expr_span = command.expr_span.map(|span| span.rebased(base));
4595 if let Some(expr) = &mut command.expr_ast {
4596 Self::rebase_arithmetic_expr(expr, base);
4597 }
4598 command.right_paren_span = command.right_paren_span.rebased(base);
4599 }
4600 CompoundCommand::Time(command) => {
4601 command.span = command.span.rebased(base);
4602 if let Some(inner) = &mut command.command {
4603 Self::rebase_stmt(inner.as_mut(), base);
4604 }
4605 }
4606 CompoundCommand::Conditional(command) => {
4607 command.span = command.span.rebased(base);
4608 command.left_bracket_span = command.left_bracket_span.rebased(base);
4609 command.right_bracket_span = command.right_bracket_span.rebased(base);
4610 Self::rebase_conditional_expr(&mut command.expression, base);
4611 }
4612 CompoundCommand::Coproc(command) => {
4613 command.span = command.span.rebased(base);
4614 command.name_span = command.name_span.map(|span| span.rebased(base));
4615 Self::rebase_stmt(command.body.as_mut(), base);
4616 }
4617 CompoundCommand::Always(command) => {
4618 command.span = command.span.rebased(base);
4619 Self::rebase_stmt_seq(&mut command.body, base);
4620 Self::rebase_stmt_seq(&mut command.always_body, base);
4621 }
4622 }
4623 }
4624
4625 fn materialize_stmt_seq_source_backing(sequence: &mut StmtSeq, source: &str) {
4626 for stmt in &mut sequence.stmts {
4627 Self::materialize_stmt_source_backing(stmt, source);
4628 }
4629 }
4630
4631 fn materialize_stmt_source_backing(stmt: &mut Stmt, source: &str) {
4632 Self::materialize_ast_command_source_backing(&mut stmt.command, source);
4633 }
4634
4635 fn materialize_ast_command_source_backing(command: &mut AstCommand, source: &str) {
4636 match command {
4637 AstCommand::Simple(simple) => {
4638 Self::materialize_word_source_backing(&mut simple.name, source);
4639 }
4640 AstCommand::Builtin(_) | AstCommand::Decl(_) => {}
4641 AstCommand::Binary(binary) => {
4642 Self::materialize_stmt_source_backing(binary.left.as_mut(), source);
4643 Self::materialize_stmt_source_backing(binary.right.as_mut(), source);
4644 }
4645 AstCommand::Compound(compound) => {
4646 Self::materialize_compound_source_backing(compound, source);
4647 }
4648 AstCommand::Function(function) => {
4649 Self::materialize_stmt_source_backing(function.body.as_mut(), source);
4650 }
4651 AstCommand::AnonymousFunction(function) => {
4652 Self::materialize_stmt_source_backing(function.body.as_mut(), source);
4653 }
4654 }
4655 }
4656
4657 fn materialize_compound_source_backing(compound: &mut CompoundCommand, source: &str) {
4658 match compound {
4659 CompoundCommand::If(command) => {
4660 Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
4661 Self::materialize_stmt_seq_source_backing(&mut command.then_branch, source);
4662 for (condition, body) in &mut command.elif_branches {
4663 Self::materialize_stmt_seq_source_backing(condition, source);
4664 Self::materialize_stmt_seq_source_backing(body, source);
4665 }
4666 if let Some(else_branch) = &mut command.else_branch {
4667 Self::materialize_stmt_seq_source_backing(else_branch, source);
4668 }
4669 }
4670 CompoundCommand::For(command) => {
4671 Self::materialize_stmt_seq_source_backing(&mut command.body, source)
4672 }
4673 CompoundCommand::Repeat(command) => {
4674 Self::materialize_stmt_seq_source_backing(&mut command.body, source)
4675 }
4676 CompoundCommand::Foreach(command) => {
4677 Self::materialize_stmt_seq_source_backing(&mut command.body, source)
4678 }
4679 CompoundCommand::ArithmeticFor(command) => {
4680 Self::materialize_stmt_seq_source_backing(&mut command.body, source)
4681 }
4682 CompoundCommand::While(command) => {
4683 Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
4684 Self::materialize_stmt_seq_source_backing(&mut command.body, source);
4685 }
4686 CompoundCommand::Until(command) => {
4687 Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
4688 Self::materialize_stmt_seq_source_backing(&mut command.body, source);
4689 }
4690 CompoundCommand::Case(command) => {
4691 for case in &mut command.cases {
4692 Self::materialize_stmt_seq_source_backing(&mut case.body, source);
4693 }
4694 }
4695 CompoundCommand::Select(command) => {
4696 Self::materialize_stmt_seq_source_backing(&mut command.body, source)
4697 }
4698 CompoundCommand::Subshell(commands) | CompoundCommand::BraceGroup(commands) => {
4699 Self::materialize_stmt_seq_source_backing(commands, source);
4700 }
4701 CompoundCommand::Arithmetic(_) => {}
4702 CompoundCommand::Time(command) => {
4703 if let Some(inner) = &mut command.command {
4704 Self::materialize_stmt_source_backing(inner.as_mut(), source);
4705 }
4706 }
4707 CompoundCommand::Conditional(_) => {}
4708 CompoundCommand::Coproc(command) => {
4709 Self::materialize_stmt_source_backing(command.body.as_mut(), source);
4710 }
4711 CompoundCommand::Always(command) => {
4712 Self::materialize_stmt_seq_source_backing(&mut command.body, source);
4713 Self::materialize_stmt_seq_source_backing(&mut command.always_body, source);
4714 }
4715 }
4716 }
4717
4718 fn rebase_words(words: &mut [Word], base: Position) {
4719 for word in words {
4720 Self::rebase_word(word, base);
4721 }
4722 }
4723
4724 fn rebase_patterns(patterns: &mut [Pattern], base: Position) {
4725 for pattern in patterns {
4726 Self::rebase_pattern(pattern, base);
4727 }
4728 }
4729
4730 fn materialize_literal_text_source_backing(text: &mut LiteralText, span: Span, source: &str) {
4731 match text {
4732 LiteralText::Source => {
4733 *text = LiteralText::owned(span.slice(source).to_string());
4734 }
4735 LiteralText::CookedSource(cooked) => {
4736 *text = LiteralText::owned(cooked.to_string());
4737 }
4738 LiteralText::Owned(_) => {}
4739 }
4740 }
4741
4742 fn materialize_source_text_source_backing(text: &mut SourceText, source: &str) {
4743 if text.is_source_backed() {
4744 let span = text.span();
4745 let cooked = text.slice(source).to_string();
4746 *text = SourceText::cooked(span, cooked);
4747 }
4748 }
4749
4750 fn materialize_word_source_backing(word: &mut Word, source: &str) {
4751 for part in &mut word.parts {
4752 Self::materialize_word_part_source_backing(part, source);
4753 }
4754 }
4755
4756 fn materialize_pattern_source_backing(pattern: &mut Pattern, source: &str) {
4757 for part in &mut pattern.parts {
4758 Self::materialize_pattern_part_source_backing(part, source);
4759 }
4760 }
4761
4762 fn materialize_pattern_part_source_backing(part: &mut PatternPartNode, source: &str) {
4763 match &mut part.kind {
4764 PatternPart::Literal(text) => {
4765 Self::materialize_literal_text_source_backing(text, part.span, source);
4766 }
4767 PatternPart::CharClass(text) => {
4768 Self::materialize_source_text_source_backing(text, source);
4769 }
4770 PatternPart::Group { patterns, .. } => {
4771 for pattern in patterns {
4772 Self::materialize_pattern_source_backing(pattern, source);
4773 }
4774 }
4775 PatternPart::Word(word) => Self::materialize_word_source_backing(word, source),
4776 PatternPart::AnyString | PatternPart::AnyChar => {}
4777 }
4778 }
4779
4780 fn materialize_word_part_source_backing(part: &mut WordPartNode, source: &str) {
4781 match &mut part.kind {
4782 WordPart::Literal(text) => {
4783 Self::materialize_literal_text_source_backing(text, part.span, source);
4784 }
4785 WordPart::ZshQualifiedGlob(glob) => {
4786 Self::materialize_zsh_qualified_glob_source_backing(glob, source);
4787 }
4788 WordPart::SingleQuoted { value, .. } => {
4789 Self::materialize_source_text_source_backing(value, source);
4790 }
4791 WordPart::DoubleQuoted { parts, .. } => {
4792 for part in parts {
4793 Self::materialize_word_part_source_backing(part, source);
4794 }
4795 }
4796 WordPart::Parameter(parameter) => {
4797 Self::materialize_source_text_source_backing(&mut parameter.raw_body, source);
4798 Self::materialize_parameter_expansion_syntax_source_backing(
4799 &mut parameter.syntax,
4800 source,
4801 );
4802 }
4803 WordPart::ParameterExpansion {
4804 reference,
4805 operator,
4806 operand,
4807 operand_word_ast,
4808 ..
4809 } => {
4810 Self::materialize_var_ref_source_backing(reference, source);
4811 Self::materialize_parameter_operator_source_backing(operator, source);
4812 if let Some(operand) = operand {
4813 Self::materialize_source_text_source_backing(operand, source);
4814 }
4815 if let Some(word_ast) = operand_word_ast {
4816 Self::materialize_word_source_backing(word_ast, source);
4817 }
4818 }
4819 WordPart::ArrayAccess(reference)
4820 | WordPart::Length(reference)
4821 | WordPart::ArrayLength(reference)
4822 | WordPart::ArrayIndices(reference)
4823 | WordPart::Transformation { reference, .. } => {
4824 Self::materialize_var_ref_source_backing(reference, source);
4825 }
4826 WordPart::Substring {
4827 reference,
4828 offset,
4829 offset_ast,
4830 offset_word_ast,
4831 length,
4832 length_ast,
4833 length_word_ast,
4834 ..
4835 }
4836 | WordPart::ArraySlice {
4837 reference,
4838 offset,
4839 offset_ast,
4840 offset_word_ast,
4841 length,
4842 length_ast,
4843 length_word_ast,
4844 ..
4845 } => {
4846 Self::materialize_var_ref_source_backing(reference, source);
4847 Self::materialize_source_text_source_backing(offset, source);
4848 Self::materialize_word_source_backing(offset_word_ast, source);
4849 if let Some(expr) = offset_ast {
4850 Self::materialize_arithmetic_expr_source_backing(expr, source);
4851 }
4852 if let Some(length) = length {
4853 Self::materialize_source_text_source_backing(length, source);
4854 }
4855 if let Some(word_ast) = length_word_ast {
4856 Self::materialize_word_source_backing(word_ast, source);
4857 }
4858 if let Some(expr) = length_ast {
4859 Self::materialize_arithmetic_expr_source_backing(expr, source);
4860 }
4861 }
4862 WordPart::IndirectExpansion {
4863 reference,
4864 operator,
4865 operand,
4866 operand_word_ast,
4867 ..
4868 } => {
4869 Self::materialize_var_ref_source_backing(reference, source);
4870 if let Some(operator) = operator {
4871 Self::materialize_parameter_operator_source_backing(operator, source);
4872 }
4873 if let Some(operand) = operand {
4874 Self::materialize_source_text_source_backing(operand, source);
4875 }
4876 if let Some(word_ast) = operand_word_ast {
4877 Self::materialize_word_source_backing(word_ast, source);
4878 }
4879 }
4880 WordPart::ArithmeticExpansion {
4881 expression,
4882 expression_ast,
4883 expression_word_ast,
4884 ..
4885 } => {
4886 Self::materialize_source_text_source_backing(expression, source);
4887 Self::materialize_word_source_backing(expression_word_ast, source);
4888 if let Some(expr) = expression_ast {
4889 Self::materialize_arithmetic_expr_source_backing(expr, source);
4890 }
4891 }
4892 WordPart::CommandSubstitution { .. }
4893 | WordPart::ProcessSubstitution { .. }
4894 | WordPart::Variable(_)
4895 | WordPart::PrefixMatch { .. } => {}
4896 }
4897 }
4898
4899 fn materialize_var_ref_source_backing(reference: &mut VarRef, source: &str) {
4900 if let Some(subscript) = &mut reference.subscript {
4901 Self::materialize_subscript_source_backing(subscript, source);
4902 }
4903 }
4904
4905 fn materialize_subscript_source_backing(subscript: &mut Subscript, source: &str) {
4906 Self::materialize_source_text_source_backing(&mut subscript.text, source);
4907 if let Some(raw) = &mut subscript.raw {
4908 Self::materialize_source_text_source_backing(raw, source);
4909 }
4910 if let Some(word_ast) = &mut subscript.word_ast {
4911 Self::materialize_word_source_backing(word_ast, source);
4912 }
4913 if let Some(expr) = &mut subscript.arithmetic_ast {
4914 Self::materialize_arithmetic_expr_source_backing(expr, source);
4915 }
4916 }
4917
4918 fn materialize_zsh_qualified_glob_source_backing(glob: &mut ZshQualifiedGlob, source: &str) {
4919 for segment in &mut glob.segments {
4920 match segment {
4921 ZshGlobSegment::Pattern(pattern) => {
4922 Self::materialize_pattern_source_backing(pattern, source);
4923 }
4924 ZshGlobSegment::InlineControl(_) => {}
4925 }
4926 }
4927 if let Some(qualifiers) = &mut glob.qualifiers {
4928 for fragment in &mut qualifiers.fragments {
4929 match fragment {
4930 ZshGlobQualifier::LetterSequence { text, .. } => {
4931 Self::materialize_source_text_source_backing(text, source);
4932 }
4933 ZshGlobQualifier::NumericArgument { start, end, .. } => {
4934 Self::materialize_source_text_source_backing(start, source);
4935 if let Some(end) = end {
4936 Self::materialize_source_text_source_backing(end, source);
4937 }
4938 }
4939 ZshGlobQualifier::Negation { .. } | ZshGlobQualifier::Flag { .. } => {}
4940 }
4941 }
4942 }
4943 }
4944
4945 fn materialize_parameter_expansion_syntax_source_backing(
4946 syntax: &mut ParameterExpansionSyntax,
4947 source: &str,
4948 ) {
4949 match syntax {
4950 ParameterExpansionSyntax::Bourne(syntax) => match syntax {
4951 BourneParameterExpansion::Access { reference }
4952 | BourneParameterExpansion::Length { reference }
4953 | BourneParameterExpansion::Indices { reference }
4954 | BourneParameterExpansion::Transformation { reference, .. } => {
4955 Self::materialize_var_ref_source_backing(reference, source);
4956 }
4957 BourneParameterExpansion::Indirect {
4958 reference,
4959 operator,
4960 operand,
4961 operand_word_ast,
4962 ..
4963 } => {
4964 Self::materialize_var_ref_source_backing(reference, source);
4965 if let Some(operator) = operator {
4966 Self::materialize_parameter_operator_source_backing(operator, source);
4967 }
4968 if let Some(operand) = operand {
4969 Self::materialize_source_text_source_backing(operand, source);
4970 }
4971 if let Some(word_ast) = operand_word_ast {
4972 Self::materialize_word_source_backing(word_ast, source);
4973 }
4974 }
4975 BourneParameterExpansion::PrefixMatch { .. } => {}
4976 BourneParameterExpansion::Slice {
4977 reference,
4978 offset,
4979 offset_ast,
4980 offset_word_ast,
4981 length,
4982 length_ast,
4983 length_word_ast,
4984 } => {
4985 Self::materialize_var_ref_source_backing(reference, source);
4986 Self::materialize_source_text_source_backing(offset, source);
4987 Self::materialize_word_source_backing(offset_word_ast, source);
4988 if let Some(expr) = offset_ast {
4989 Self::materialize_arithmetic_expr_source_backing(expr, source);
4990 }
4991 if let Some(length) = length {
4992 Self::materialize_source_text_source_backing(length, source);
4993 }
4994 if let Some(word_ast) = length_word_ast {
4995 Self::materialize_word_source_backing(word_ast, source);
4996 }
4997 if let Some(expr) = length_ast {
4998 Self::materialize_arithmetic_expr_source_backing(expr, source);
4999 }
5000 }
5001 BourneParameterExpansion::Operation {
5002 reference,
5003 operator,
5004 operand,
5005 operand_word_ast,
5006 ..
5007 } => {
5008 Self::materialize_var_ref_source_backing(reference, source);
5009 Self::materialize_parameter_operator_source_backing(operator, source);
5010 if let Some(operand) = operand {
5011 Self::materialize_source_text_source_backing(operand, source);
5012 }
5013 if let Some(word_ast) = operand_word_ast {
5014 Self::materialize_word_source_backing(word_ast, source);
5015 }
5016 }
5017 },
5018 ParameterExpansionSyntax::Zsh(syntax) => {
5019 match &mut syntax.target {
5020 ZshExpansionTarget::Reference(reference) => {
5021 Self::materialize_var_ref_source_backing(reference, source);
5022 }
5023 ZshExpansionTarget::Word(word) => {
5024 Self::materialize_word_source_backing(word, source);
5025 }
5026 ZshExpansionTarget::Nested(parameter) => {
5027 Self::materialize_source_text_source_backing(
5028 &mut parameter.raw_body,
5029 source,
5030 );
5031 Self::materialize_parameter_expansion_syntax_source_backing(
5032 &mut parameter.syntax,
5033 source,
5034 );
5035 }
5036 ZshExpansionTarget::Empty => {}
5037 }
5038 for modifier in &mut syntax.modifiers {
5039 if let Some(argument) = &mut modifier.argument {
5040 Self::materialize_source_text_source_backing(argument, source);
5041 }
5042 if let Some(argument_word_ast) = &mut modifier.argument_word_ast {
5043 Self::materialize_word_source_backing(argument_word_ast, source);
5044 }
5045 }
5046 if let Some(operation) = &mut syntax.operation {
5047 match operation {
5048 ZshExpansionOperation::PatternOperation {
5049 operand,
5050 operand_word_ast,
5051 ..
5052 }
5053 | ZshExpansionOperation::Defaulting {
5054 operand,
5055 operand_word_ast,
5056 ..
5057 }
5058 | ZshExpansionOperation::TrimOperation {
5059 operand,
5060 operand_word_ast,
5061 ..
5062 } => {
5063 Self::materialize_source_text_source_backing(operand, source);
5064 Self::materialize_word_source_backing(operand_word_ast, source);
5065 }
5066 ZshExpansionOperation::Unknown { text, word_ast } => {
5067 Self::materialize_source_text_source_backing(text, source);
5068 Self::materialize_word_source_backing(word_ast, source);
5069 }
5070 ZshExpansionOperation::ReplacementOperation {
5071 pattern,
5072 pattern_word_ast,
5073 replacement,
5074 replacement_word_ast,
5075 ..
5076 } => {
5077 Self::materialize_source_text_source_backing(pattern, source);
5078 Self::materialize_word_source_backing(pattern_word_ast, source);
5079 if let Some(replacement) = replacement {
5080 Self::materialize_source_text_source_backing(replacement, source);
5081 }
5082 if let Some(replacement_word_ast) = replacement_word_ast {
5083 Self::materialize_word_source_backing(replacement_word_ast, source);
5084 }
5085 }
5086 ZshExpansionOperation::Slice {
5087 offset,
5088 offset_word_ast,
5089 length,
5090 length_word_ast,
5091 } => {
5092 Self::materialize_source_text_source_backing(offset, source);
5093 Self::materialize_word_source_backing(offset_word_ast, source);
5094 if let Some(length) = length {
5095 Self::materialize_source_text_source_backing(length, source);
5096 }
5097 if let Some(length_word_ast) = length_word_ast {
5098 Self::materialize_word_source_backing(length_word_ast, source);
5099 }
5100 }
5101 }
5102 }
5103 }
5104 }
5105 }
5106
5107 fn materialize_parameter_operator_source_backing(operator: &mut ParameterOp, source: &str) {
5108 match operator {
5109 ParameterOp::RemovePrefixShort { pattern }
5110 | ParameterOp::RemovePrefixLong { pattern }
5111 | ParameterOp::RemoveSuffixShort { pattern }
5112 | ParameterOp::RemoveSuffixLong { pattern } => {
5113 Self::materialize_pattern_source_backing(pattern, source);
5114 }
5115 ParameterOp::ReplaceFirst {
5116 pattern,
5117 replacement,
5118 replacement_word_ast,
5119 }
5120 | ParameterOp::ReplaceAll {
5121 pattern,
5122 replacement,
5123 replacement_word_ast,
5124 } => {
5125 Self::materialize_pattern_source_backing(pattern, source);
5126 Self::materialize_source_text_source_backing(replacement, source);
5127 Self::materialize_word_source_backing(replacement_word_ast, source);
5128 }
5129 ParameterOp::UseDefault
5130 | ParameterOp::AssignDefault
5131 | ParameterOp::UseReplacement
5132 | ParameterOp::Error
5133 | ParameterOp::UpperFirst
5134 | ParameterOp::UpperAll
5135 | ParameterOp::LowerFirst
5136 | ParameterOp::LowerAll => {}
5137 }
5138 }
5139
5140 fn materialize_arithmetic_expr_source_backing(expr: &mut ArithmeticExprNode, source: &str) {
5141 match &mut expr.kind {
5142 ArithmeticExpr::Number(text) => {
5143 Self::materialize_source_text_source_backing(text, source);
5144 }
5145 ArithmeticExpr::Variable(_) => {}
5146 ArithmeticExpr::Indexed { index, .. } => {
5147 Self::materialize_arithmetic_expr_source_backing(index, source);
5148 }
5149 ArithmeticExpr::ShellWord(word) => {
5150 Self::materialize_word_source_backing(word, source);
5151 }
5152 ArithmeticExpr::Parenthesized { expression } => {
5153 Self::materialize_arithmetic_expr_source_backing(expression, source);
5154 }
5155 ArithmeticExpr::Unary { expr, .. } | ArithmeticExpr::Postfix { expr, .. } => {
5156 Self::materialize_arithmetic_expr_source_backing(expr, source);
5157 }
5158 ArithmeticExpr::Binary { left, right, .. } => {
5159 Self::materialize_arithmetic_expr_source_backing(left, source);
5160 Self::materialize_arithmetic_expr_source_backing(right, source);
5161 }
5162 ArithmeticExpr::Conditional {
5163 condition,
5164 then_expr,
5165 else_expr,
5166 } => {
5167 Self::materialize_arithmetic_expr_source_backing(condition, source);
5168 Self::materialize_arithmetic_expr_source_backing(then_expr, source);
5169 Self::materialize_arithmetic_expr_source_backing(else_expr, source);
5170 }
5171 ArithmeticExpr::Assignment { target, value, .. } => {
5172 Self::materialize_arithmetic_lvalue_source_backing(target, source);
5173 Self::materialize_arithmetic_expr_source_backing(value, source);
5174 }
5175 }
5176 }
5177
5178 fn materialize_arithmetic_lvalue_source_backing(target: &mut ArithmeticLvalue, source: &str) {
5179 match target {
5180 ArithmeticLvalue::Variable(_) => {}
5181 ArithmeticLvalue::Indexed { index, .. } => {
5182 Self::materialize_arithmetic_expr_source_backing(index, source);
5183 }
5184 }
5185 }
5186
5187 fn rebase_word(word: &mut Word, base: Position) {
5188 word.span = word.span.rebased(base);
5189 for brace in &mut word.brace_syntax {
5190 brace.span = brace.span.rebased(base);
5191 }
5192 Self::rebase_word_parts(&mut word.parts, base);
5193 }
5194
5195 fn rebase_heredoc_body(body: &mut HeredocBody, base: Position) {
5196 body.span = body.span.rebased(base);
5197 for part in &mut body.parts {
5198 Self::rebase_heredoc_body_part(part, base);
5199 }
5200 }
5201
5202 fn rebase_pattern(pattern: &mut Pattern, base: Position) {
5203 pattern.span = pattern.span.rebased(base);
5204 Self::rebase_pattern_parts(&mut pattern.parts, base);
5205 }
5206
5207 fn rebase_word_parts(parts: &mut [WordPartNode], base: Position) {
5208 for part in parts {
5209 Self::rebase_word_part(part, base);
5210 }
5211 }
5212
5213 fn rebase_pattern_parts(parts: &mut [PatternPartNode], base: Position) {
5214 for part in parts {
5215 part.span = part.span.rebased(base);
5216 match &mut part.kind {
5217 PatternPart::CharClass(text) => text.rebased(base),
5218 PatternPart::Group { patterns, .. } => Self::rebase_patterns(patterns, base),
5219 PatternPart::Word(word) => Self::rebase_word(word, base),
5220 PatternPart::Literal(_) | PatternPart::AnyString | PatternPart::AnyChar => {}
5221 }
5222 }
5223 }
5224
5225 fn rebase_heredoc_body_part(part: &mut HeredocBodyPartNode, base: Position) {
5226 part.span = part.span.rebased(base);
5227 match &mut part.kind {
5228 HeredocBodyPart::Literal(_) | HeredocBodyPart::Variable(_) => {}
5229 HeredocBodyPart::CommandSubstitution { body, .. } => Self::rebase_stmt_seq(body, base),
5230 HeredocBodyPart::ArithmeticExpansion {
5231 expression,
5232 expression_ast,
5233 expression_word_ast,
5234 ..
5235 } => {
5236 expression.rebased(base);
5237 Self::rebase_word(expression_word_ast, base);
5238 if let Some(expr) = expression_ast {
5239 Self::rebase_arithmetic_expr(expr, base);
5240 }
5241 }
5242 HeredocBodyPart::Parameter(parameter) => {
5243 parameter.span = parameter.span.rebased(base);
5244 parameter.raw_body.rebased(base);
5245 Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
5246 }
5247 }
5248 }
5249
5250 fn rebase_word_part(part: &mut WordPartNode, base: Position) {
5251 part.span = part.span.rebased(base);
5252 match &mut part.kind {
5253 WordPart::ZshQualifiedGlob(glob) => Self::rebase_zsh_qualified_glob(glob, base),
5254 WordPart::SingleQuoted { value, .. } => value.rebased(base),
5255 WordPart::DoubleQuoted { parts, .. } => Self::rebase_word_parts(parts, base),
5256 WordPart::Parameter(parameter) => {
5257 parameter.span = parameter.span.rebased(base);
5258 parameter.raw_body.rebased(base);
5259 Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
5260 }
5261 WordPart::ParameterExpansion {
5262 reference,
5263 operator,
5264 operand,
5265 operand_word_ast,
5266 ..
5267 } => {
5268 Self::rebase_var_ref(reference, base);
5269 match operator {
5270 ParameterOp::RemovePrefixShort { pattern }
5271 | ParameterOp::RemovePrefixLong { pattern }
5272 | ParameterOp::RemoveSuffixShort { pattern }
5273 | ParameterOp::RemoveSuffixLong { pattern } => {
5274 Self::rebase_pattern(pattern, base);
5275 }
5276 ParameterOp::ReplaceFirst {
5277 pattern,
5278 replacement,
5279 ..
5280 }
5281 | ParameterOp::ReplaceAll {
5282 pattern,
5283 replacement,
5284 ..
5285 } => {
5286 Self::rebase_pattern(pattern, base);
5287 replacement.rebased(base);
5288 }
5289 ParameterOp::UseDefault
5290 | ParameterOp::AssignDefault
5291 | ParameterOp::UseReplacement
5292 | ParameterOp::Error
5293 | ParameterOp::UpperFirst
5294 | ParameterOp::UpperAll
5295 | ParameterOp::LowerFirst
5296 | ParameterOp::LowerAll => {}
5297 }
5298 if let Some(operand) = operand {
5299 operand.rebased(base);
5300 }
5301 if let Some(word_ast) = operand_word_ast {
5302 Self::rebase_word(word_ast, base);
5303 }
5304 }
5305 WordPart::ArrayAccess(reference)
5306 | WordPart::Length(reference)
5307 | WordPart::ArrayLength(reference)
5308 | WordPart::ArrayIndices(reference)
5309 | WordPart::Transformation { reference, .. } => Self::rebase_var_ref(reference, base),
5310 WordPart::Substring {
5311 reference,
5312 offset,
5313 offset_ast,
5314 offset_word_ast,
5315 length,
5316 length_ast,
5317 length_word_ast,
5318 ..
5319 }
5320 | WordPart::ArraySlice {
5321 reference,
5322 offset,
5323 offset_ast,
5324 offset_word_ast,
5325 length,
5326 length_ast,
5327 length_word_ast,
5328 ..
5329 } => {
5330 Self::rebase_var_ref(reference, base);
5331 offset.rebased(base);
5332 Self::rebase_word(offset_word_ast, base);
5333 if let Some(expr) = offset_ast {
5334 Self::rebase_arithmetic_expr(expr, base);
5335 }
5336 if let Some(length) = length {
5337 length.rebased(base);
5338 }
5339 if let Some(word_ast) = length_word_ast {
5340 Self::rebase_word(word_ast, base);
5341 }
5342 if let Some(expr) = length_ast {
5343 Self::rebase_arithmetic_expr(expr, base);
5344 }
5345 }
5346 WordPart::IndirectExpansion {
5347 reference,
5348 operator,
5349 operand,
5350 operand_word_ast,
5351 ..
5352 } => {
5353 Self::rebase_var_ref(reference, base);
5354 if let Some(operator) = operator {
5355 Self::rebase_parameter_operator(operator, base);
5356 }
5357 if let Some(operand) = operand {
5358 operand.rebased(base);
5359 }
5360 if let Some(word_ast) = operand_word_ast {
5361 Self::rebase_word(word_ast, base);
5362 }
5363 }
5364 WordPart::ArithmeticExpansion {
5365 expression,
5366 expression_ast,
5367 expression_word_ast,
5368 ..
5369 } => {
5370 expression.rebased(base);
5371 Self::rebase_word(expression_word_ast, base);
5372 if let Some(expr) = expression_ast {
5373 Self::rebase_arithmetic_expr(expr, base);
5374 }
5375 }
5376 WordPart::CommandSubstitution { body, .. }
5377 | WordPart::ProcessSubstitution { body, .. } => Self::rebase_stmt_seq(body, base),
5378 WordPart::Literal(_) | WordPart::Variable(_) | WordPart::PrefixMatch { .. } => {}
5379 }
5380 }
5381
5382 fn rebase_zsh_qualified_glob(glob: &mut ZshQualifiedGlob, base: Position) {
5383 glob.span = glob.span.rebased(base);
5384 for segment in &mut glob.segments {
5385 Self::rebase_zsh_glob_segment(segment, base);
5386 }
5387 if let Some(qualifiers) = &mut glob.qualifiers {
5388 Self::rebase_zsh_glob_qualifier_group(qualifiers, base);
5389 }
5390 }
5391
5392 fn rebase_zsh_glob_segment(segment: &mut ZshGlobSegment, base: Position) {
5393 match segment {
5394 ZshGlobSegment::Pattern(pattern) => Self::rebase_pattern(pattern, base),
5395 ZshGlobSegment::InlineControl(control) => {
5396 Self::rebase_zsh_inline_glob_control(control, base)
5397 }
5398 }
5399 }
5400
5401 fn rebase_zsh_inline_glob_control(control: &mut ZshInlineGlobControl, base: Position) {
5402 match control {
5403 ZshInlineGlobControl::CaseInsensitive { span }
5404 | ZshInlineGlobControl::Backreferences { span }
5405 | ZshInlineGlobControl::StartAnchor { span }
5406 | ZshInlineGlobControl::EndAnchor { span } => {
5407 *span = span.rebased(base);
5408 }
5409 }
5410 }
5411
5412 fn rebase_zsh_glob_qualifier_group(group: &mut ZshGlobQualifierGroup, base: Position) {
5413 group.span = group.span.rebased(base);
5414 for fragment in &mut group.fragments {
5415 match fragment {
5416 ZshGlobQualifier::Negation { span } | ZshGlobQualifier::Flag { span, .. } => {
5417 *span = span.rebased(base);
5418 }
5419 ZshGlobQualifier::LetterSequence { text, span } => {
5420 *span = span.rebased(base);
5421 text.rebased(base);
5422 }
5423 ZshGlobQualifier::NumericArgument { span, start, end } => {
5424 *span = span.rebased(base);
5425 start.rebased(base);
5426 if let Some(end) = end {
5427 end.rebased(base);
5428 }
5429 }
5430 }
5431 }
5432 }
5433
5434 fn rebase_parameter_expansion_syntax(syntax: &mut ParameterExpansionSyntax, base: Position) {
5435 match syntax {
5436 ParameterExpansionSyntax::Bourne(syntax) => match syntax {
5437 BourneParameterExpansion::Access { reference }
5438 | BourneParameterExpansion::Length { reference }
5439 | BourneParameterExpansion::Indices { reference }
5440 | BourneParameterExpansion::Transformation { reference, .. } => {
5441 Self::rebase_var_ref(reference, base);
5442 }
5443 BourneParameterExpansion::Indirect {
5444 reference,
5445 operand,
5446 operator,
5447 operand_word_ast,
5448 ..
5449 } => {
5450 Self::rebase_var_ref(reference, base);
5451 if let Some(operator) = operator {
5452 Self::rebase_parameter_operator(operator, base);
5453 }
5454 if let Some(operand) = operand {
5455 operand.rebased(base);
5456 }
5457 if let Some(word_ast) = operand_word_ast {
5458 Self::rebase_word(word_ast, base);
5459 }
5460 }
5461 BourneParameterExpansion::PrefixMatch { .. } => {}
5462 BourneParameterExpansion::Slice {
5463 reference,
5464 offset,
5465 offset_ast,
5466 offset_word_ast,
5467 length,
5468 length_ast,
5469 length_word_ast,
5470 } => {
5471 Self::rebase_var_ref(reference, base);
5472 offset.rebased(base);
5473 Self::rebase_word(offset_word_ast, base);
5474 if let Some(expr) = offset_ast {
5475 Self::rebase_arithmetic_expr(expr, base);
5476 }
5477 if let Some(length) = length {
5478 length.rebased(base);
5479 }
5480 if let Some(word_ast) = length_word_ast {
5481 Self::rebase_word(word_ast, base);
5482 }
5483 if let Some(expr) = length_ast {
5484 Self::rebase_arithmetic_expr(expr, base);
5485 }
5486 }
5487 BourneParameterExpansion::Operation {
5488 reference,
5489 operator,
5490 operand,
5491 operand_word_ast,
5492 ..
5493 } => {
5494 Self::rebase_var_ref(reference, base);
5495 Self::rebase_parameter_operator(operator, base);
5496 if let Some(operand) = operand {
5497 operand.rebased(base);
5498 }
5499 if let Some(word_ast) = operand_word_ast {
5500 Self::rebase_word(word_ast, base);
5501 }
5502 }
5503 },
5504 ParameterExpansionSyntax::Zsh(syntax) => {
5505 match &mut syntax.target {
5506 ZshExpansionTarget::Reference(reference) => {
5507 Self::rebase_var_ref(reference, base)
5508 }
5509 ZshExpansionTarget::Word(word) => Self::rebase_word(word, base),
5510 ZshExpansionTarget::Nested(parameter) => {
5511 parameter.span = parameter.span.rebased(base);
5512 parameter.raw_body.rebased(base);
5513 Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
5514 }
5515 ZshExpansionTarget::Empty => {}
5516 }
5517 for modifier in &mut syntax.modifiers {
5518 modifier.span = modifier.span.rebased(base);
5519 if let Some(argument) = &mut modifier.argument {
5520 argument.rebased(base);
5521 }
5522 if let Some(argument_word_ast) = &mut modifier.argument_word_ast {
5523 Self::rebase_word(argument_word_ast, base);
5524 }
5525 }
5526 if let Some(length_prefix) = &mut syntax.length_prefix {
5527 *length_prefix = length_prefix.rebased(base);
5528 }
5529 if let Some(operation) = &mut syntax.operation {
5530 match operation {
5531 ZshExpansionOperation::PatternOperation {
5532 operand,
5533 operand_word_ast,
5534 ..
5535 }
5536 | ZshExpansionOperation::Defaulting {
5537 operand,
5538 operand_word_ast,
5539 ..
5540 }
5541 | ZshExpansionOperation::TrimOperation {
5542 operand,
5543 operand_word_ast,
5544 ..
5545 } => {
5546 operand.rebased(base);
5547 Self::rebase_word(operand_word_ast, base);
5548 }
5549 ZshExpansionOperation::Unknown { text, word_ast } => {
5550 text.rebased(base);
5551 Self::rebase_word(word_ast, base);
5552 }
5553 ZshExpansionOperation::ReplacementOperation {
5554 pattern,
5555 pattern_word_ast,
5556 replacement,
5557 replacement_word_ast,
5558 ..
5559 } => {
5560 pattern.rebased(base);
5561 Self::rebase_word(pattern_word_ast, base);
5562 if let Some(replacement) = replacement {
5563 replacement.rebased(base);
5564 }
5565 if let Some(replacement_word_ast) = replacement_word_ast {
5566 Self::rebase_word(replacement_word_ast, base);
5567 }
5568 }
5569 ZshExpansionOperation::Slice {
5570 offset,
5571 offset_word_ast,
5572 length,
5573 length_word_ast,
5574 } => {
5575 offset.rebased(base);
5576 Self::rebase_word(offset_word_ast, base);
5577 if let Some(length) = length {
5578 length.rebased(base);
5579 }
5580 if let Some(length_word_ast) = length_word_ast {
5581 Self::rebase_word(length_word_ast, base);
5582 }
5583 }
5584 }
5585 }
5586 }
5587 }
5588 }
5589
5590 fn rebase_parameter_operator(operator: &mut ParameterOp, base: Position) {
5591 match operator {
5592 ParameterOp::RemovePrefixShort { pattern }
5593 | ParameterOp::RemovePrefixLong { pattern }
5594 | ParameterOp::RemoveSuffixShort { pattern }
5595 | ParameterOp::RemoveSuffixLong { pattern } => {
5596 Self::rebase_pattern(pattern, base);
5597 }
5598 ParameterOp::ReplaceFirst {
5599 pattern,
5600 replacement,
5601 replacement_word_ast,
5602 }
5603 | ParameterOp::ReplaceAll {
5604 pattern,
5605 replacement,
5606 replacement_word_ast,
5607 } => {
5608 Self::rebase_pattern(pattern, base);
5609 replacement.rebased(base);
5610 Self::rebase_word(replacement_word_ast, base);
5611 }
5612 ParameterOp::UseDefault
5613 | ParameterOp::AssignDefault
5614 | ParameterOp::UseReplacement
5615 | ParameterOp::Error
5616 | ParameterOp::UpperFirst
5617 | ParameterOp::UpperAll
5618 | ParameterOp::LowerFirst
5619 | ParameterOp::LowerAll => {}
5620 }
5621 }
5622
5623 fn rebase_conditional_expr(expr: &mut ConditionalExpr, base: Position) {
5624 match expr {
5625 ConditionalExpr::Binary(binary) => {
5626 binary.op_span = binary.op_span.rebased(base);
5627 Self::rebase_conditional_expr(&mut binary.left, base);
5628 Self::rebase_conditional_expr(&mut binary.right, base);
5629 }
5630 ConditionalExpr::Unary(unary) => {
5631 unary.op_span = unary.op_span.rebased(base);
5632 Self::rebase_conditional_expr(&mut unary.expr, base);
5633 }
5634 ConditionalExpr::Parenthesized(paren) => {
5635 paren.left_paren_span = paren.left_paren_span.rebased(base);
5636 paren.right_paren_span = paren.right_paren_span.rebased(base);
5637 Self::rebase_conditional_expr(&mut paren.expr, base);
5638 }
5639 ConditionalExpr::Word(word) | ConditionalExpr::Regex(word) => {
5640 Self::rebase_word(word, base);
5641 }
5642 ConditionalExpr::Pattern(pattern) => Self::rebase_pattern(pattern, base),
5643 ConditionalExpr::VarRef(var_ref) => Self::rebase_var_ref(var_ref, base),
5644 }
5645 }
5646
5647 fn rebase_arithmetic_expr(expr: &mut ArithmeticExprNode, base: Position) {
5648 expr.span = expr.span.rebased(base);
5649 match &mut expr.kind {
5650 ArithmeticExpr::Number(text) => text.rebased(base),
5651 ArithmeticExpr::Variable(_) => {}
5652 ArithmeticExpr::Indexed { index, .. } => Self::rebase_arithmetic_expr(index, base),
5653 ArithmeticExpr::ShellWord(word) => Self::rebase_word(word, base),
5654 ArithmeticExpr::Parenthesized { expression } => {
5655 Self::rebase_arithmetic_expr(expression, base)
5656 }
5657 ArithmeticExpr::Unary { expr, .. } | ArithmeticExpr::Postfix { expr, .. } => {
5658 Self::rebase_arithmetic_expr(expr, base)
5659 }
5660 ArithmeticExpr::Binary { left, right, .. } => {
5661 Self::rebase_arithmetic_expr(left, base);
5662 Self::rebase_arithmetic_expr(right, base);
5663 }
5664 ArithmeticExpr::Conditional {
5665 condition,
5666 then_expr,
5667 else_expr,
5668 } => {
5669 Self::rebase_arithmetic_expr(condition, base);
5670 Self::rebase_arithmetic_expr(then_expr, base);
5671 Self::rebase_arithmetic_expr(else_expr, base);
5672 }
5673 ArithmeticExpr::Assignment { target, value, .. } => {
5674 Self::rebase_arithmetic_lvalue(target, base);
5675 Self::rebase_arithmetic_expr(value, base);
5676 }
5677 }
5678 }
5679
5680 fn rebase_arithmetic_lvalue(target: &mut ArithmeticLvalue, base: Position) {
5681 match target {
5682 ArithmeticLvalue::Variable(_) => {}
5683 ArithmeticLvalue::Indexed { index, .. } => Self::rebase_arithmetic_expr(index, base),
5684 }
5685 }
5686
5687 fn push_word_part(parts: &mut WordPartBuffer, part: WordPart, start: Position, end: Position) {
5688 Self::push_word_part_node(
5689 parts,
5690 WordPartNode::new(part, Span::from_positions(start, end)),
5691 );
5692 }
5693
5694 fn push_word_part_node(parts: &mut WordPartBuffer, part: WordPartNode) {
5695 parts.push(part);
5696 }
5697
5698 fn flush_literal_part(
5699 &self,
5700 parts: &mut WordPartBuffer,
5701 current: &mut String,
5702 current_start: Position,
5703 end: Position,
5704 source_backed: bool,
5705 ) {
5706 if !current.is_empty() {
5707 Self::push_word_part(
5708 parts,
5709 WordPart::Literal(self.literal_text(
5710 std::mem::take(current),
5711 current_start,
5712 end,
5713 source_backed,
5714 )),
5715 current_start,
5716 end,
5717 );
5718 }
5719 }
5720
5721 fn word_part_buffer_with_capacity(capacity: usize) -> WordPartBuffer {
5722 if capacity <= 2 {
5723 WordPartBuffer::new()
5724 } else {
5725 WordPartBuffer::with_capacity(capacity)
5726 }
5727 }
5728
5729 fn literal_text(
5730 &self,
5731 text: String,
5732 start: Position,
5733 end: Position,
5734 source_backed: bool,
5735 ) -> LiteralText {
5736 let span = Span::from_positions(start, end);
5737 if self.source_matches(span, &text) {
5738 LiteralText::source()
5739 } else if source_backed {
5740 LiteralText::cooked_source(text)
5741 } else {
5742 LiteralText::owned(text)
5743 }
5744 }
5745
5746 fn literal_text_from_str(
5747 &self,
5748 text: &str,
5749 start: Position,
5750 end: Position,
5751 source_backed: bool,
5752 ) -> LiteralText {
5753 self.literal_text_impl(text, None, start, end, source_backed)
5754 }
5755
5756 fn literal_text_impl(
5757 &self,
5758 text: &str,
5759 owned: Option<String>,
5760 start: Position,
5761 end: Position,
5762 source_backed: bool,
5763 ) -> LiteralText {
5764 let span = Span::from_positions(start, end);
5765 if self.source_matches(span, text) {
5766 LiteralText::source()
5767 } else if source_backed {
5768 LiteralText::cooked_source(owned.unwrap_or_else(|| text.to_owned()))
5769 } else {
5770 LiteralText::owned(owned.unwrap_or_else(|| text.to_owned()))
5771 }
5772 }
5773
5774 fn source_text(&self, text: String, start: Position, end: Position) -> SourceText {
5775 let span = Span::from_positions(start, end);
5776 if self.source_matches(span, &text) {
5777 SourceText::source(span)
5778 } else {
5779 SourceText::cooked(span, text)
5780 }
5781 }
5782
5783 fn source_text_from_str(&self, text: &str, start: Position, end: Position) -> SourceText {
5784 self.source_text_impl(text, None, start, end)
5785 }
5786
5787 fn source_text_impl(
5788 &self,
5789 text: &str,
5790 owned: Option<String>,
5791 start: Position,
5792 end: Position,
5793 ) -> SourceText {
5794 let span = Span::from_positions(start, end);
5795 if self.source_matches(span, text) {
5796 SourceText::source(span)
5797 } else {
5798 SourceText::cooked(span, owned.unwrap_or_else(|| text.to_owned()))
5799 }
5800 }
5801
5802 fn empty_source_text(&self, pos: Position) -> SourceText {
5803 SourceText::source(Span::from_positions(pos, pos))
5804 }
5805
5806 fn input_prefix_ends_with(&self, end_offset: usize, ch: char) -> bool {
5807 self.input
5808 .get(..end_offset)
5809 .is_some_and(|prefix| prefix.ends_with(ch))
5810 }
5811
5812 fn input_span_ends_with(&self, start: Position, end: Position, ch: char) -> bool {
5813 self.input
5814 .get(start.offset..end.offset)
5815 .is_some_and(|slice| slice.ends_with(ch))
5816 }
5817
5818 fn input_suffix_starts_with(&self, start_offset: usize, ch: char) -> bool {
5819 self.input
5820 .get(start_offset..)
5821 .is_some_and(|suffix| suffix.starts_with(ch))
5822 }
5823
5824 fn subscript_source_text(&self, raw: &str, span: Span) -> (SourceText, Option<SourceText>) {
5825 if raw.len() >= 2
5826 && ((raw.starts_with('"') && raw.ends_with('"'))
5827 || (raw.starts_with('\'') && raw.ends_with('\'')))
5828 {
5829 let raw_text = raw.to_string();
5830 let raw = if self.source_matches(span, raw) {
5831 SourceText::source(span)
5832 } else {
5833 SourceText::cooked(span, raw_text.clone())
5834 };
5835 let cooked = raw_text[1..raw_text.len() - 1].to_string();
5836 return (self.source_text(cooked, span.start, span.end), Some(raw));
5837 }
5838
5839 let text = if self.source_matches(span, raw) {
5840 SourceText::source(span)
5841 } else {
5842 SourceText::cooked(span, raw.to_string())
5843 };
5844 (text, None)
5845 }
5846
5847 fn subscript_from_source_text(
5848 &self,
5849 text: SourceText,
5850 raw: Option<SourceText>,
5851 interpretation: SubscriptInterpretation,
5852 ) -> Subscript {
5853 let kind = match text.slice(self.input).trim() {
5854 "@" => SubscriptKind::Selector(SubscriptSelector::At),
5855 "*" => SubscriptKind::Selector(SubscriptSelector::Star),
5856 _ => SubscriptKind::Ordinary,
5857 };
5858 let word_ast = if matches!(kind, SubscriptKind::Ordinary) {
5859 Some(self.parse_source_text_as_word(raw.as_ref().unwrap_or(&text)))
5860 } else {
5861 None
5862 };
5863 let arithmetic_ast = if matches!(kind, SubscriptKind::Ordinary) {
5864 self.simple_subscript_arithmetic_ast(&text)
5865 .or_else(|| self.maybe_parse_source_text_as_arithmetic(&text))
5866 } else {
5867 None
5868 };
5869 Subscript {
5870 text,
5871 raw,
5872 kind,
5873 interpretation,
5874 word_ast,
5875 arithmetic_ast,
5876 }
5877 }
5878
5879 fn simple_subscript_arithmetic_ast(&self, text: &SourceText) -> Option<ArithmeticExprNode> {
5880 if !text.is_source_backed() {
5881 return None;
5882 }
5883
5884 let raw = text.slice(self.input);
5885 if raw.is_empty() || raw.trim() != raw {
5886 return None;
5887 }
5888
5889 let span = text.span();
5890 if raw.bytes().all(|byte| byte.is_ascii_digit()) {
5891 return Some(ArithmeticExprNode::new(
5892 ArithmeticExpr::Number(SourceText::source(span)),
5893 span,
5894 ));
5895 }
5896
5897 if Self::is_valid_identifier(raw) {
5898 return Some(ArithmeticExprNode::new(
5899 ArithmeticExpr::Variable(Name::from(raw)),
5900 span,
5901 ));
5902 }
5903
5904 None
5905 }
5906
5907 fn subscript_from_text(
5908 &self,
5909 raw: &str,
5910 span: Span,
5911 interpretation: SubscriptInterpretation,
5912 ) -> Subscript {
5913 let (text, raw) = self.subscript_source_text(raw, span);
5914 self.subscript_from_source_text(text, raw, interpretation)
5915 }
5916
5917 fn var_ref(
5918 &self,
5919 name: impl Into<Name>,
5920 name_span: Span,
5921 subscript: Option<Subscript>,
5922 span: Span,
5923 ) -> VarRef {
5924 VarRef {
5925 name: name.into(),
5926 name_span,
5927 subscript: subscript.map(Box::new),
5928 span,
5929 }
5930 }
5931
5932 fn parameter_var_ref(
5933 &self,
5934 part_start: Position,
5935 prefix: &str,
5936 name: &str,
5937 subscript: Option<Subscript>,
5938 part_end: Position,
5939 ) -> VarRef {
5940 let name_start = part_start.advanced_by(prefix);
5941 let name_span = Span::from_positions(name_start, name_start.advanced_by(name));
5942 self.var_ref(
5943 Name::from(name),
5944 name_span,
5945 subscript,
5946 Span::from_positions(part_start, part_end),
5947 )
5948 }
5949
5950 fn parameter_word_part_from_legacy(
5951 &self,
5952 part: WordPart,
5953 part_start: Position,
5954 part_end: Position,
5955 source_backed: bool,
5956 ) -> WordPart {
5957 let span = Span::from_positions(part_start, part_end);
5958 let raw_body = self.parameter_raw_body_from_legacy(&part, span, source_backed);
5959 let raw_body_text = raw_body.slice(self.input).to_string();
5960
5961 let syntax = match part {
5962 WordPart::ParameterExpansion {
5963 reference,
5964 operator,
5965 operand,
5966 operand_word_ast,
5967 colon_variant,
5968 } => Some(BourneParameterExpansion::Operation {
5969 reference,
5970 operator: self.enrich_parameter_operator(operator),
5971 operand,
5972 operand_word_ast,
5973 colon_variant,
5974 }),
5975 WordPart::Length(reference) | WordPart::ArrayLength(reference) => {
5976 Some(BourneParameterExpansion::Length { reference })
5977 }
5978 WordPart::ArrayAccess(reference) => {
5979 Some(BourneParameterExpansion::Access { reference })
5980 }
5981 WordPart::ArrayIndices(reference) => {
5982 Some(BourneParameterExpansion::Indices { reference })
5983 }
5984 WordPart::Substring {
5985 reference,
5986 offset,
5987 offset_ast,
5988 offset_word_ast,
5989 length,
5990 length_ast,
5991 length_word_ast,
5992 }
5993 | WordPart::ArraySlice {
5994 reference,
5995 offset,
5996 offset_ast,
5997 offset_word_ast,
5998 length,
5999 length_ast,
6000 length_word_ast,
6001 } => Some(BourneParameterExpansion::Slice {
6002 reference,
6003 offset,
6004 offset_ast,
6005 offset_word_ast,
6006 length,
6007 length_ast,
6008 length_word_ast,
6009 }),
6010 WordPart::IndirectExpansion {
6011 reference,
6012 operator,
6013 operand,
6014 operand_word_ast,
6015 colon_variant,
6016 } => Some(BourneParameterExpansion::Indirect {
6017 reference,
6018 operator: operator.map(|operator| self.enrich_parameter_operator(operator)),
6019 operand,
6020 operand_word_ast,
6021 colon_variant,
6022 }),
6023 WordPart::PrefixMatch { prefix, kind } => {
6024 Some(BourneParameterExpansion::PrefixMatch { prefix, kind })
6025 }
6026 WordPart::Transformation {
6027 reference,
6028 operator,
6029 } => Some(BourneParameterExpansion::Transformation {
6030 reference,
6031 operator,
6032 }),
6033 WordPart::Variable(name) if raw_body_text == name.as_str() => {
6034 Some(BourneParameterExpansion::Access {
6035 reference: self.parameter_var_ref(
6036 part_start,
6037 "${",
6038 name.as_str(),
6039 None,
6040 part_end,
6041 ),
6042 })
6043 }
6044 other => return other,
6045 };
6046
6047 let Some(syntax) = syntax else {
6048 unreachable!("matched Some above");
6049 };
6050 WordPart::Parameter(ParameterExpansion {
6051 syntax: ParameterExpansionSyntax::Bourne(syntax),
6052 span,
6053 raw_body,
6054 })
6055 }
6056
6057 fn enrich_parameter_operator(&self, operator: ParameterOp) -> ParameterOp {
6058 match operator {
6059 ParameterOp::ReplaceFirst {
6060 pattern,
6061 replacement,
6062 ..
6063 } => ParameterOp::ReplaceFirst {
6064 pattern,
6065 replacement_word_ast: self.parse_source_text_as_word(&replacement),
6066 replacement,
6067 },
6068 ParameterOp::ReplaceAll {
6069 pattern,
6070 replacement,
6071 ..
6072 } => ParameterOp::ReplaceAll {
6073 pattern,
6074 replacement_word_ast: self.parse_source_text_as_word(&replacement),
6075 replacement,
6076 },
6077 ParameterOp::UseDefault
6078 | ParameterOp::AssignDefault
6079 | ParameterOp::UseReplacement
6080 | ParameterOp::Error
6081 | ParameterOp::RemovePrefixShort { .. }
6082 | ParameterOp::RemovePrefixLong { .. }
6083 | ParameterOp::RemoveSuffixShort { .. }
6084 | ParameterOp::RemoveSuffixLong { .. }
6085 | ParameterOp::UpperFirst
6086 | ParameterOp::UpperAll
6087 | ParameterOp::LowerFirst
6088 | ParameterOp::LowerAll => operator,
6089 }
6090 }
6091
6092 fn parameter_raw_body_from_legacy(
6093 &self,
6094 part: &WordPart,
6095 span: Span,
6096 source_backed: bool,
6097 ) -> SourceText {
6098 if source_backed && span.end.offset <= self.input.len() {
6099 let syntax = span.slice(self.input);
6100 if let Some(body) = syntax
6101 .strip_prefix("${")
6102 .and_then(|syntax| syntax.strip_suffix('}'))
6103 {
6104 let start = span.start.advanced_by("${");
6105 let end = start.advanced_by(body);
6106 return SourceText::source(Span::from_positions(start, end));
6107 }
6108 }
6109
6110 let mut syntax = String::new();
6111 self.push_word_part_syntax(&mut syntax, part, span);
6112 let body = syntax
6113 .strip_prefix("${")
6114 .and_then(|syntax| syntax.strip_suffix('}'))
6115 .unwrap_or(syntax.as_str())
6116 .to_string();
6117 SourceText::from(body)
6118 }
6119
6120 fn zsh_parameter_word_part(
6121 &mut self,
6122 raw_body: SourceText,
6123 part_start: Position,
6124 part_end: Position,
6125 ) -> WordPart {
6126 let syntax = self.parse_zsh_parameter_syntax(&raw_body, raw_body.span().start);
6127 WordPart::Parameter(ParameterExpansion {
6128 syntax: ParameterExpansionSyntax::Zsh(syntax),
6129 span: Span::from_positions(part_start, part_end),
6130 raw_body,
6131 })
6132 }
6133
6134 fn parse_zsh_modifier_group(
6135 &self,
6136 text: &str,
6137 base: Position,
6138 start: usize,
6139 ) -> Option<(usize, Vec<ZshModifier>)> {
6140 let rest = text.get(start..)?;
6141 if !rest.starts_with('(') {
6142 return None;
6143 }
6144
6145 let close_rel = rest[1..].find(')')?;
6146 let close = start + 1 + close_rel;
6147 let group_text = &text[start..=close];
6148 let inner = &text[start + 1..close];
6149 let group_start = base.advanced_by(&text[..start]);
6150 let group_span = Span::from_positions(group_start, group_start.advanced_by(group_text));
6151 let mut modifiers = Vec::new();
6152 let mut index = 0usize;
6153
6154 while index < inner.len() {
6155 let name = inner[index..].chars().next()?;
6156 index += name.len_utf8();
6157
6158 let mut argument_delimiter = None;
6159 let mut argument = None;
6160 if matches!(name, 's' | 'j')
6161 && let Some(delimiter) = inner[index..].chars().next()
6162 {
6163 index += delimiter.len_utf8();
6164 let argument_start = index;
6165 while index < inner.len() {
6166 let ch = inner[index..].chars().next()?;
6167 if ch == delimiter {
6168 let argument_text = &inner[argument_start..index];
6169 let argument_base =
6170 group_start.advanced_by(&group_text[..1 + argument_start]);
6171 let argument_end = argument_base.advanced_by(argument_text);
6172 argument_delimiter = Some(delimiter);
6173 argument = Some(self.source_text(
6174 argument_text.to_string(),
6175 argument_base,
6176 argument_end,
6177 ));
6178 index += delimiter.len_utf8();
6179 break;
6180 }
6181 index += ch.len_utf8();
6182 }
6183 }
6184
6185 let argument_word_ast = argument
6186 .as_ref()
6187 .map(|argument| self.parse_source_text_as_word(argument));
6188
6189 modifiers.push(ZshModifier {
6190 name,
6191 argument,
6192 argument_word_ast,
6193 argument_delimiter,
6194 span: group_span,
6195 });
6196 }
6197
6198 Some((close + 1, modifiers))
6199 }
6200
6201 fn parse_zsh_parameter_syntax(
6202 &mut self,
6203 raw_body: &SourceText,
6204 base: Position,
6205 ) -> ZshParameterExpansion {
6206 let text = raw_body.slice(self.input);
6207 let mut index = 0;
6208 let mut modifiers = Vec::new();
6209 let mut length_prefix = None;
6210 let source_backed = raw_body.is_source_backed();
6211
6212 while text[index..].starts_with('(')
6213 && let Some((next_index, group_modifiers)) =
6214 self.parse_zsh_modifier_group(text, base, index)
6215 {
6216 modifiers.extend(group_modifiers);
6217 index = next_index;
6218 }
6219
6220 while index < text.len() {
6221 let Some(flag) = text[index..].chars().next() else {
6222 break;
6223 };
6224 match flag {
6225 '=' | '~' | '^' => {
6226 let modifier_start = base.advanced_by(&text[..index]);
6227 let modifier_end =
6228 modifier_start.advanced_by(&text[index..index + flag.len_utf8()]);
6229 modifiers.push(ZshModifier {
6230 name: flag,
6231 argument: None,
6232 argument_word_ast: None,
6233 argument_delimiter: None,
6234 span: Span::from_positions(modifier_start, modifier_end),
6235 });
6236 index += flag.len_utf8();
6237 }
6238 '#' if length_prefix.is_none() => {
6239 let prefix_start = base.advanced_by(&text[..index]);
6240 let prefix_end = prefix_start.advanced_by("#");
6241 length_prefix = Some(Span::from_positions(prefix_start, prefix_end));
6242 index += '#'.len_utf8();
6243 }
6244 _ => break,
6245 }
6246 }
6247
6248 let (target, operation_index) = if text[index..].starts_with("${") {
6249 let end = self
6250 .find_matching_parameter_end(&text[index..])
6251 .unwrap_or(text.len() - index);
6252 let nested_text = &text[index..index + end];
6253 let target =
6254 self.parse_nested_parameter_target(nested_text, base.advanced_by(&text[..index]));
6255 (target, index + end)
6256 } else if text[index..].starts_with(':') || text[index..].is_empty() {
6257 (ZshExpansionTarget::Empty, index)
6258 } else {
6259 let end = self
6260 .find_zsh_operation_start(&text[index..])
6261 .map(|offset| index + offset)
6262 .unwrap_or(text.len());
6263 let raw_target = &text[index..end];
6264 let trimmed = raw_target.trim();
6265 let target = if trimmed.is_empty() {
6266 ZshExpansionTarget::Empty
6267 } else {
6268 let leading = raw_target
6269 .len()
6270 .saturating_sub(raw_target.trim_start().len());
6271 let target_base = base.advanced_by(&text[..index + leading]);
6272 self.parse_zsh_target_from_text(
6273 trimmed,
6274 target_base,
6275 source_backed && leading == 0 && trimmed.len() == raw_target.len(),
6276 )
6277 };
6278 (target, end)
6279 };
6280
6281 let operation = (operation_index < text.len()).then(|| {
6282 self.parse_zsh_parameter_operation(
6283 &text[operation_index..],
6284 base.advanced_by(&text[..operation_index]),
6285 )
6286 });
6287
6288 ZshParameterExpansion {
6289 target,
6290 modifiers,
6291 length_prefix,
6292 operation,
6293 }
6294 }
6295
6296 fn parse_zsh_target_from_text(
6297 &mut self,
6298 text: &str,
6299 base: Position,
6300 source_backed: bool,
6301 ) -> ZshExpansionTarget {
6302 let trimmed = text.trim();
6303 if trimmed.is_empty() {
6304 return ZshExpansionTarget::Empty;
6305 }
6306
6307 if trimmed.starts_with("${") && trimmed.ends_with('}') {
6308 return self.parse_nested_parameter_target(trimmed, base);
6309 }
6310
6311 if let Some(reference) = self.maybe_parse_loose_var_ref_target(trimmed) {
6312 return ZshExpansionTarget::Reference(reference);
6313 }
6314
6315 let span = Span::from_positions(base, base.advanced_by(trimmed));
6316 let word = self.parse_word_with_context(trimmed, span, base, source_backed);
6317 if let Some(reference) =
6318 self.parse_var_ref_from_word(&word, SubscriptInterpretation::Contextual)
6319 {
6320 ZshExpansionTarget::Reference(reference)
6321 } else {
6322 ZshExpansionTarget::Word(word)
6323 }
6324 }
6325
6326 fn maybe_parse_loose_var_ref_target(&self, text: &str) -> Option<VarRef> {
6327 let trimmed = text.trim();
6328 Self::looks_like_plain_parameter_access(trimmed).then(|| self.parse_loose_var_ref(trimmed))
6329 }
6330
6331 fn is_plain_special_parameter_name(name: &str) -> bool {
6332 matches!(name, "#" | "$" | "!" | "*" | "@" | "?" | "-") || name == "0"
6333 }
6334
6335 fn looks_like_plain_parameter_access(text: &str) -> bool {
6336 let trimmed = text.trim();
6337 if trimmed.is_empty() {
6338 return false;
6339 }
6340
6341 let name = if let Some(open) = trimmed.find('[') {
6342 if !trimmed.ends_with(']') {
6343 return false;
6344 }
6345 &trimmed[..open]
6346 } else {
6347 trimmed
6348 };
6349
6350 Self::is_valid_identifier(name)
6351 || name.bytes().all(|byte| byte.is_ascii_digit())
6352 || Self::is_plain_special_parameter_name(name)
6353 }
6354
6355 fn parse_nested_parameter_target(&mut self, text: &str, base: Position) -> ZshExpansionTarget {
6356 if !(text.starts_with("${") && text.ends_with('}')) {
6357 return self.parse_zsh_target_from_text(text, base, false);
6358 }
6359
6360 let raw_body_start = base.advanced_by("${");
6361 let raw_body = self.source_text(
6362 text[2..text.len() - 1].to_string(),
6363 raw_body_start,
6364 base.advanced_by(&text[..text.len() - 1]),
6365 );
6366 let raw_body_text = raw_body.slice(self.input);
6367 let has_operation = self.find_zsh_operation_start(raw_body_text).is_some();
6368 let syntax = if Self::looks_like_plain_parameter_access(raw_body_text) && !has_operation {
6369 ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access {
6370 reference: self.parse_loose_var_ref(raw_body_text),
6371 })
6372 } else if raw_body_text.starts_with('(')
6373 || raw_body_text.starts_with(':')
6374 || raw_body_text.starts_with('=')
6375 || raw_body_text.starts_with('^')
6376 || raw_body_text.starts_with('~')
6377 || raw_body_text.starts_with('.')
6378 || raw_body_text.starts_with('#')
6379 || raw_body_text.starts_with('"')
6380 || raw_body_text.starts_with('\'')
6381 || raw_body_text.starts_with('$')
6382 || has_operation
6383 {
6384 ParameterExpansionSyntax::Zsh(
6385 self.parse_zsh_parameter_syntax(&raw_body, raw_body_start),
6386 )
6387 } else {
6388 ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access {
6389 reference: self.parse_loose_var_ref(raw_body_text),
6390 })
6391 };
6392
6393 ZshExpansionTarget::Nested(Box::new(ParameterExpansion {
6394 syntax,
6395 span: Span::from_positions(base, base.advanced_by(text)),
6396 raw_body,
6397 }))
6398 }
6399
6400 fn parse_loose_var_ref(&self, text: &str) -> VarRef {
6401 let trimmed = text.trim();
6402 if let Some(open) = trimmed.find('[')
6403 && trimmed.ends_with(']')
6404 {
6405 let name = &trimmed[..open];
6406 let subscript_text = &trimmed[open + 1..trimmed.len() - 1];
6407 let subscript = self.subscript_from_source_text(
6408 SourceText::from(subscript_text.to_string()),
6409 None,
6410 SubscriptInterpretation::Contextual,
6411 );
6412 return VarRef {
6413 name: Name::from(name),
6414 name_span: Span::new(),
6415 subscript: Some(Box::new(subscript)),
6416 span: Span::new(),
6417 };
6418 }
6419
6420 VarRef {
6421 name: Name::from(trimmed),
6422 name_span: Span::new(),
6423 subscript: None,
6424 span: Span::new(),
6425 }
6426 }
6427
6428 fn find_matching_parameter_end(&self, text: &str) -> Option<usize> {
6429 let mut depth = 0_i32;
6430 let mut chars = text.char_indices().peekable();
6431
6432 while let Some((index, ch)) = chars.next() {
6433 match ch {
6434 '$' if chars.peek().is_some_and(|(_, next)| *next == '{') => {
6435 depth += 1;
6436 }
6437 '}' => {
6438 depth -= 1;
6439 if depth == 0 {
6440 return Some(index + ch.len_utf8());
6441 }
6442 }
6443 _ => {}
6444 }
6445 }
6446
6447 None
6448 }
6449
6450 fn find_zsh_operation_start(&self, text: &str) -> Option<usize> {
6451 let mut bracket_depth = 0_usize;
6452 let mut in_single = false;
6453 let mut in_double = false;
6454 let mut escaped = false;
6455
6456 for (index, ch) in text.char_indices() {
6457 if escaped {
6458 escaped = false;
6459 continue;
6460 }
6461
6462 match ch {
6463 '\\' if !in_single => escaped = true,
6464 '\'' if !in_double => in_single = !in_single,
6465 '"' if !in_single => in_double = !in_double,
6466 '[' if !in_single && !in_double => bracket_depth += 1,
6467 ']' if !in_single && !in_double && bracket_depth > 0 => bracket_depth -= 1,
6468 ':' if !in_single && !in_double && bracket_depth == 0 => return Some(index),
6469 '#' | '%' | '/' | '^' | ',' | '~'
6470 if !in_single && !in_double && bracket_depth == 0 && index > 0 =>
6471 {
6472 return Some(index);
6473 }
6474 _ => {}
6475 }
6476 }
6477
6478 None
6479 }
6480
6481 fn zsh_operation_source_text(
6482 &self,
6483 text: &str,
6484 base: Position,
6485 start: usize,
6486 end: usize,
6487 ) -> SourceText {
6488 self.source_text(
6489 text[start..end].to_string(),
6490 base.advanced_by(&text[..start]),
6491 base.advanced_by(&text[..end]),
6492 )
6493 }
6494
6495 fn find_zsh_top_level_delimiter(&self, text: &str, delimiter: char) -> Option<usize> {
6496 let mut chars = text.char_indices().peekable();
6497 let mut in_single = false;
6498 let mut in_double = false;
6499 let mut escaped = false;
6500 let mut brace_depth = 0_usize;
6501 let mut paren_depth = 0_usize;
6502
6503 while let Some((index, ch)) = chars.next() {
6504 if escaped {
6505 escaped = false;
6506 continue;
6507 }
6508
6509 match ch {
6510 '\\' if !in_single => escaped = true,
6511 '\'' if !in_double => in_single = !in_single,
6512 '"' if !in_single => in_double = !in_double,
6513 '$' if !in_single => {
6514 if let Some((_, next)) = chars.peek() {
6515 if *next == '{' {
6516 brace_depth += 1;
6517 chars.next();
6518 } else if *next == '(' {
6519 paren_depth += 1;
6520 chars.next();
6521 if let Some((_, after)) = chars.peek()
6522 && *after == '('
6523 {
6524 paren_depth += 1;
6525 chars.next();
6526 }
6527 }
6528 }
6529 }
6530 '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
6531 ')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
6532 _ if ch == delimiter
6533 && !in_single
6534 && !in_double
6535 && brace_depth == 0
6536 && paren_depth == 0 =>
6537 {
6538 return Some(index);
6539 }
6540 _ => {}
6541 }
6542 }
6543
6544 None
6545 }
6546
6547 fn zsh_simple_modifier_suffix_segment(segment: &str) -> bool {
6548 let mut chars = segment.chars();
6549 let Some(first) = chars.next() else {
6550 return false;
6551 };
6552
6553 match first {
6554 'a' | 'A' | 'c' | 'e' | 'l' | 'P' | 'q' | 'Q' | 'r' | 'u' => chars.next().is_none(),
6555 'h' | 't' => chars.all(|ch| ch.is_ascii_digit()),
6556 _ => false,
6557 }
6558 }
6559
6560 fn zsh_modifier_suffix_candidate(rest: &str) -> bool {
6561 if rest.is_empty() {
6562 return false;
6563 }
6564
6565 let Some(first) = rest.chars().next() else {
6566 return false;
6567 };
6568 if first.is_ascii_digit()
6569 || first.is_ascii_whitespace()
6570 || matches!(first, '$' | '\'' | '"' | '(' | '{')
6571 {
6572 return false;
6573 }
6574
6575 rest.split(':')
6576 .all(Self::zsh_simple_modifier_suffix_segment)
6577 }
6578
6579 fn zsh_slice_candidate(rest: &str) -> bool {
6580 let Some(first) = rest.chars().next() else {
6581 return false;
6582 };
6583
6584 !Self::zsh_modifier_suffix_candidate(rest)
6585 && (first.is_ascii_alphanumeric()
6586 || first == '_'
6587 || first.is_ascii_whitespace()
6588 || matches!(first, '$' | '\'' | '"' | '(' | '{'))
6589 }
6590
6591 fn parse_zsh_parameter_operation(&self, text: &str, base: Position) -> ZshExpansionOperation {
6592 if let Some(operand) = text.strip_prefix(":#") {
6593 let operand = self.source_text(
6594 operand.to_string(),
6595 base.advanced_by(":#"),
6596 base.advanced_by(text),
6597 );
6598 return ZshExpansionOperation::PatternOperation {
6599 kind: ZshPatternOp::Filter,
6600 operand_word_ast: self.parse_source_text_as_word(&operand),
6601 operand,
6602 };
6603 }
6604
6605 if let Some((kind, operand)) = text
6606 .strip_prefix(":-")
6607 .map(|operand| (ZshDefaultingOp::UseDefault, operand))
6608 .or_else(|| {
6609 text.strip_prefix(":=")
6610 .map(|operand| (ZshDefaultingOp::AssignDefault, operand))
6611 })
6612 .or_else(|| {
6613 text.strip_prefix(":+")
6614 .map(|operand| (ZshDefaultingOp::UseReplacement, operand))
6615 })
6616 .or_else(|| {
6617 text.strip_prefix(":?")
6618 .map(|operand| (ZshDefaultingOp::Error, operand))
6619 })
6620 {
6621 let operand = self.source_text(
6622 operand.to_string(),
6623 base.advanced_by(&text[..2]),
6624 base.advanced_by(text),
6625 );
6626 return ZshExpansionOperation::Defaulting {
6627 kind,
6628 operand_word_ast: self.parse_source_text_as_word(&operand),
6629 operand,
6630 colon_variant: true,
6631 };
6632 }
6633
6634 if let Some((kind, prefix_len)) = [
6635 ("##", ZshTrimOp::RemovePrefixLong),
6636 ("#", ZshTrimOp::RemovePrefixShort),
6637 ("%%", ZshTrimOp::RemoveSuffixLong),
6638 ("%", ZshTrimOp::RemoveSuffixShort),
6639 ]
6640 .into_iter()
6641 .find_map(|(prefix, kind)| text.starts_with(prefix).then_some((kind, prefix.len())))
6642 {
6643 let operand = self.zsh_operation_source_text(text, base, prefix_len, text.len());
6644 return ZshExpansionOperation::TrimOperation {
6645 kind,
6646 operand_word_ast: self.parse_source_text_as_word(&operand),
6647 operand,
6648 };
6649 }
6650
6651 if let Some((kind, prefix_len)) = [
6652 ("//", ZshReplacementOp::ReplaceAll),
6653 ("/#", ZshReplacementOp::ReplacePrefix),
6654 ("/%", ZshReplacementOp::ReplaceSuffix),
6655 ("/", ZshReplacementOp::ReplaceFirst),
6656 ]
6657 .into_iter()
6658 .find_map(|(prefix, kind)| text.starts_with(prefix).then_some((kind, prefix.len())))
6659 {
6660 let rest = &text[prefix_len..];
6661 let separator = self.find_zsh_top_level_delimiter(rest, '/');
6662 let pattern_end = separator.unwrap_or(rest.len());
6663 let pattern =
6664 self.zsh_operation_source_text(text, base, prefix_len, prefix_len + pattern_end);
6665 let replacement = separator.map(|separator| {
6666 self.zsh_operation_source_text(text, base, prefix_len + separator + 1, text.len())
6667 });
6668 return ZshExpansionOperation::ReplacementOperation {
6669 kind,
6670 pattern_word_ast: self.parse_source_text_as_word(&pattern),
6671 replacement_word_ast: self.parse_optional_source_text_as_word(replacement.as_ref()),
6672 pattern,
6673 replacement,
6674 };
6675 }
6676
6677 if let Some(rest) = text.strip_prefix(':') {
6678 if Self::zsh_modifier_suffix_candidate(rest) {
6679 let text = self.source_text(text.to_string(), base, base.advanced_by(text));
6680 return ZshExpansionOperation::Unknown {
6681 word_ast: self.parse_source_text_as_word(&text),
6682 text,
6683 };
6684 }
6685
6686 if Self::zsh_slice_candidate(rest) {
6687 let separator = self.find_zsh_top_level_delimiter(rest, ':');
6688 let offset_end = separator.unwrap_or(rest.len());
6689 let offset = self.zsh_operation_source_text(text, base, 1, 1 + offset_end);
6690 let length = separator.map(|separator| {
6691 self.zsh_operation_source_text(text, base, 1 + separator + 1, text.len())
6692 });
6693 return ZshExpansionOperation::Slice {
6694 offset_word_ast: self.parse_source_text_as_word(&offset),
6695 length_word_ast: self.parse_optional_source_text_as_word(length.as_ref()),
6696 offset,
6697 length,
6698 };
6699 }
6700 }
6701
6702 let text = self.source_text(text.to_string(), base, base.advanced_by(text));
6703 ZshExpansionOperation::Unknown {
6704 word_ast: self.parse_source_text_as_word(&text),
6705 text,
6706 }
6707 }
6708
6709 fn parse_explicit_arithmetic_span(
6710 &self,
6711 span: Option<Span>,
6712 context: &'static str,
6713 ) -> Result<Option<ArithmeticExprNode>> {
6714 let Some(span) = span else {
6715 return Ok(None);
6716 };
6717 if span.slice(self.input).trim().is_empty() {
6718 return Ok(None);
6719 }
6720 arithmetic::parse_expression(
6721 span.slice(self.input),
6722 span,
6723 self.dialect,
6724 self.max_depth.saturating_sub(self.current_depth),
6725 self.fuel,
6726 )
6727 .map(Some)
6728 .map_err(|error| match error {
6729 Error::Parse { message, .. } => self.error(format!("{context}: {message}")),
6730 })
6731 }
6732
6733 fn parse_source_text_as_arithmetic(&self, text: &SourceText) -> Result<ArithmeticExprNode> {
6734 arithmetic::parse_expression(
6735 text.slice(self.input),
6736 text.span(),
6737 self.dialect,
6738 self.max_depth.saturating_sub(self.current_depth),
6739 self.fuel,
6740 )
6741 }
6742
6743 fn maybe_parse_source_text_as_arithmetic(
6744 &self,
6745 text: &SourceText,
6746 ) -> Option<ArithmeticExprNode> {
6747 if !text.is_source_backed() {
6748 return None;
6749 }
6750 self.parse_source_text_as_arithmetic(text).ok()
6751 }
6752
6753 fn parse_source_text_as_word(&self, text: &SourceText) -> Word {
6754 if let Some(word) = self.simple_source_text_as_word(text) {
6755 return word;
6756 }
6757
6758 let span = text.span();
6759 if !text.is_source_backed()
6760 && span.start.offset <= span.end.offset
6761 && span.end.offset <= self.input.len()
6762 {
6763 let raw = span.slice(self.input);
6764 if raw.contains("\\\"") {
6765 return Self::parse_word_fragment(self.input, raw, span);
6766 }
6767 }
6768
6769 Self::parse_word_fragment(self.input, text.slice(self.input), text.span())
6770 }
6771
6772 fn simple_source_text_as_word(&self, text: &SourceText) -> Option<Word> {
6773 if !text.is_source_backed() {
6774 return None;
6775 }
6776
6777 let span = text.span();
6778 let raw = text.slice(self.input);
6779 if raw.is_empty() {
6780 return Some(Word::literal_with_span("", span));
6781 }
6782
6783 if let Some(word) = self.simple_quoted_source_text_as_word(raw, span) {
6784 return Some(word);
6785 }
6786
6787 if Self::word_text_needs_parse(raw)
6788 || raw.contains(['\'', '"', '\\'])
6789 || self.zsh_glob_qualifiers_enabled_at(span.start.offset)
6790 {
6791 return None;
6792 }
6793
6794 Some(self.word_with_single_part(self.literal_part_from_text(raw, span, true), span))
6795 }
6796
6797 fn simple_quoted_source_text_as_word(&self, raw: &str, span: Span) -> Option<Word> {
6798 if raw.len() < 2 {
6799 return None;
6800 }
6801
6802 let quote = raw.as_bytes()[0];
6803 if quote != b'\'' && quote != b'"' || raw.as_bytes().last().copied() != Some(quote) {
6804 return None;
6805 }
6806
6807 let inner = &raw[1..raw.len() - 1];
6808 if quote == b'\'' && inner.contains('\'') {
6809 return None;
6810 }
6811
6812 let inner_start = span.start.advanced_by(&raw[..1]);
6813 let inner_end = inner_start.advanced_by(inner);
6814 let inner_span = Span::from_positions(inner_start, inner_end);
6815 let part = match quote {
6816 b'\'' => self.single_quoted_part_from_text(inner, inner_span, span, false),
6817 b'"' => {
6818 if Self::word_text_needs_parse(inner) || inner.contains(['\\', '"']) {
6819 return None;
6820 }
6821 self.double_quoted_literal_part_from_text(inner, inner_span, span, true, false)
6822 }
6823 _ => unreachable!("quote is checked above"),
6824 };
6825 Some(self.word_with_single_part(part, span))
6826 }
6827
6828 fn parse_optional_source_text_as_word(&self, text: Option<&SourceText>) -> Option<Word> {
6829 text.map(|text| self.parse_source_text_as_word(text))
6830 }
6831
6832 fn source_matches(&self, span: Span, text: &str) -> bool {
6833 span.start.offset <= span.end.offset
6834 && self
6835 .input
6836 .get(span.start.offset..span.end.offset)
6837 .is_some_and(|slice| slice == text)
6838 }
6839
6840 fn checkpoint(&self) -> ParserCheckpoint<'a> {
6841 ParserCheckpoint {
6842 lexer: self.lexer.clone(),
6843 synthetic_tokens: self.synthetic_tokens.clone(),
6844 alias_replays: self.alias_replays.clone(),
6845 current_token: self.current_token.clone(),
6846 current_token_kind: self.current_token_kind,
6847 current_keyword: self.current_keyword,
6848 current_span: self.current_span,
6849 peeked_token: self.peeked_token.clone(),
6850 current_depth: self.current_depth,
6851 fuel: self.fuel,
6852 comments: self.comments.clone(),
6853 expand_next_word: self.expand_next_word,
6854 brace_group_depth: self.brace_group_depth,
6855 brace_body_stack: self.brace_body_stack.clone(),
6856 syntax_facts: self.syntax_facts.clone(),
6857 #[cfg(feature = "benchmarking")]
6858 benchmark_counters: self.benchmark_counters,
6859 }
6860 }
6861
6862 fn restore(&mut self, checkpoint: ParserCheckpoint<'a>) {
6863 self.lexer = checkpoint.lexer;
6864 self.synthetic_tokens = checkpoint.synthetic_tokens;
6865 self.alias_replays = checkpoint.alias_replays;
6866 self.current_token = checkpoint.current_token;
6867 self.current_word_cache = None;
6869 self.current_token_kind = checkpoint.current_token_kind;
6870 self.current_keyword = checkpoint.current_keyword;
6871 self.current_span = checkpoint.current_span;
6872 self.peeked_token = checkpoint.peeked_token;
6873 self.current_depth = checkpoint.current_depth;
6874 self.fuel = checkpoint.fuel;
6875 self.comments = checkpoint.comments;
6876 self.expand_next_word = checkpoint.expand_next_word;
6877 self.brace_group_depth = checkpoint.brace_group_depth;
6878 self.brace_body_stack = checkpoint.brace_body_stack;
6879 self.syntax_facts = checkpoint.syntax_facts;
6880 #[cfg(feature = "benchmarking")]
6881 {
6882 self.benchmark_counters = checkpoint.benchmark_counters;
6883 }
6884 }
6885
6886 fn set_current_spanned(&mut self, token: LexedToken<'a>) {
6887 #[cfg(feature = "benchmarking")]
6888 self.maybe_record_set_current_spanned_call();
6889 let span = token.span;
6890 self.current_token_kind = Some(token.kind);
6891 self.current_keyword = Self::keyword_from_token(&token);
6892 self.current_token = Some(token);
6893 self.current_word_cache = None;
6894 self.current_span = span;
6895 }
6896
6897 fn set_current_kind(&mut self, kind: TokenKind, span: Span) {
6898 self.current_token_kind = Some(kind);
6899 self.current_keyword = None;
6900 self.current_token = Some(LexedToken::punctuation(kind).with_span(span));
6901 self.current_word_cache = None;
6902 self.current_span = span;
6903 }
6904
6905 fn clear_current_token(&mut self) {
6906 self.current_token = None;
6907 self.current_word_cache = None;
6908 self.current_token_kind = None;
6909 self.current_keyword = None;
6910 }
6911
6912 fn next_pending_token(&mut self) -> Option<LexedToken<'a>> {
6913 if let Some(token) = self.synthetic_tokens.pop_front() {
6914 return Some(token.materialize());
6915 }
6916
6917 loop {
6918 let replay = self.alias_replays.last_mut()?;
6919 if let Some(token) = replay.next_token() {
6920 return Some(token);
6921 }
6922 self.alias_replays.pop();
6923 }
6924 }
6925
6926 fn next_spanned_token_with_comments(&mut self) -> Option<LexedToken<'a>> {
6927 self.next_pending_token()
6928 .or_else(|| self.lexer.next_lexed_token_with_comments())
6929 }
6930
6931 fn compile_alias_definition(&self, value: &str) -> AliasDefinition {
6932 let source = Arc::<str>::from(value.to_string());
6933 let mut lexer = Lexer::with_max_subst_depth(source.as_ref(), self.max_depth);
6934 let mut tokens = Vec::new();
6935
6936 while let Some(token) = lexer.next_lexed_token_with_comments() {
6937 tokens.push(token.into_shared(&source));
6938 }
6939
6940 AliasDefinition {
6941 tokens: tokens.into(),
6942 expands_next_word: value.chars().last().is_some_and(char::is_whitespace),
6943 }
6944 }
6945
6946 fn maybe_expand_current_alias_chain(&mut self) {
6947 if !self.expand_aliases {
6948 self.expand_next_word = false;
6949 return;
6950 }
6951
6952 let mut seen = HashSet::new();
6953 let mut expands_next_word = false;
6954
6955 loop {
6956 if self.current_token_kind != Some(TokenKind::Word) {
6957 break;
6958 }
6959 let Some(name) = self.current_token.as_ref().and_then(LexedToken::word_text) else {
6960 break;
6961 };
6962 if self.current_source_word_starts_posix_function_header(name) {
6963 break;
6964 }
6965 let Some(alias) = self.aliases.get(name).cloned() else {
6966 break;
6967 };
6968 if !seen.insert(name.to_string()) {
6969 break;
6970 }
6971
6972 expands_next_word = alias.expands_next_word;
6973 self.peeked_token = None;
6974 self.alias_replays
6975 .push(AliasReplay::new(&alias, self.current_span.start));
6976 self.advance_raw();
6977 }
6978
6979 self.expand_next_word = expands_next_word;
6980 }
6981
6982 fn current_source_word_starts_posix_function_header(&self, name: &str) -> bool {
6983 if name.contains('=') || name.contains('[') {
6984 return false;
6985 }
6986
6987 if self
6988 .current_token
6989 .as_ref()
6990 .is_some_and(|token| token.flags.is_synthetic())
6991 {
6992 return false;
6993 }
6994
6995 let Some(tail) = self.input.get(self.current_span.end.offset..) else {
6996 return false;
6997 };
6998 let tail = tail.trim_start_matches([' ', '\t']);
6999 let Some(after_left) = tail.strip_prefix('(') else {
7000 return false;
7001 };
7002 let after_left = after_left.trim_start_matches([' ', '\t']);
7003 after_left.starts_with(')')
7004 }
7005
7006 fn next_word_char(
7007 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
7008 cursor: &mut Position,
7009 ) -> Option<char> {
7010 let ch = chars.next()?;
7011 cursor.advance(ch);
7012 Some(ch)
7013 }
7014
7015 fn next_word_char_unwrap(
7016 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
7017 cursor: &mut Position,
7018 ) -> char {
7019 let Some(ch) = Self::next_word_char(chars, cursor) else {
7020 unreachable!("word parser should only consume characters that were already peeked");
7021 };
7022 ch
7023 }
7024
7025 fn consume_word_char_if(
7026 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
7027 cursor: &mut Position,
7028 expected: char,
7029 ) -> bool {
7030 if chars.peek() == Some(&expected) {
7031 Self::next_word_char_unwrap(chars, cursor);
7032 true
7033 } else {
7034 false
7035 }
7036 }
7037
7038 fn read_word_while<F>(
7039 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
7040 cursor: &mut Position,
7041 mut predicate: F,
7042 ) -> String
7043 where
7044 F: FnMut(char) -> bool,
7045 {
7046 let mut text = String::new();
7047 while let Some(&ch) = chars.peek() {
7048 if !predicate(ch) {
7049 break;
7050 }
7051 text.push(Self::next_word_char_unwrap(chars, cursor));
7052 }
7053 text
7054 }
7055
7056 fn rebase_redirects(redirects: &mut [Redirect], base: Position) {
7057 for redirect in redirects {
7058 redirect.span = redirect.span.rebased(base);
7059 redirect.fd_var_span = redirect.fd_var_span.map(|span| span.rebased(base));
7060 match &mut redirect.target {
7061 RedirectTarget::Word(word) => Self::rebase_word(word, base),
7062 RedirectTarget::Heredoc(heredoc) => {
7063 heredoc.delimiter.span = heredoc.delimiter.span.rebased(base);
7064 Self::rebase_word(&mut heredoc.delimiter.raw, base);
7065 Self::rebase_heredoc_body(&mut heredoc.body, base);
7066 }
7067 }
7068 }
7069 }
7070
7071 fn rebase_assignments(assignments: &mut [Assignment], base: Position) {
7072 for assignment in assignments {
7073 assignment.span = assignment.span.rebased(base);
7074 Self::rebase_var_ref(&mut assignment.target, base);
7075 match &mut assignment.value {
7076 AssignmentValue::Scalar(word) => Self::rebase_word(word, base),
7077 AssignmentValue::Compound(array) => Self::rebase_array_expr(array, base),
7078 }
7079 }
7080 }
7081
7082 fn error(&self, message: impl Into<String>) -> Error {
7084 Error::parse_at(
7085 message,
7086 self.current_span.start.line,
7087 self.current_span.start.column,
7088 )
7089 }
7090
7091 fn ensure_feature(
7092 &self,
7093 enabled: bool,
7094 feature: &str,
7095 unsupported_message: &str,
7096 ) -> Result<()> {
7097 if enabled {
7098 Ok(())
7099 } else {
7100 Err(self.error(format!("{feature} {unsupported_message}")))
7101 }
7102 }
7103
7104 fn ensure_double_bracket(&self) -> Result<()> {
7105 self.ensure_feature(
7106 self.dialect.features().double_bracket,
7107 "[[ ]] conditionals",
7108 "are not available in this shell mode",
7109 )
7110 }
7111
7112 fn ensure_arithmetic_for(&self) -> Result<()> {
7113 self.ensure_feature(
7114 self.dialect.features().arithmetic_for,
7115 "c-style for loops",
7116 "are not available in this shell mode",
7117 )
7118 }
7119
7120 fn ensure_coproc(&self) -> Result<()> {
7121 self.ensure_feature(
7122 self.dialect.features().coproc_keyword,
7123 "coprocess commands",
7124 "are not available in this shell mode",
7125 )
7126 }
7127
7128 fn ensure_arithmetic_command(&self) -> Result<()> {
7129 self.ensure_feature(
7130 self.dialect.features().arithmetic_command,
7131 "arithmetic commands",
7132 "are not available in this shell mode",
7133 )
7134 }
7135
7136 fn ensure_select_loop(&self) -> Result<()> {
7137 self.ensure_feature(
7138 self.dialect.features().select_loop,
7139 "select loops",
7140 "are not available in this shell mode",
7141 )
7142 }
7143
7144 fn ensure_repeat_loop(&self) -> Result<()> {
7145 self.ensure_feature(
7146 self.zsh_short_repeat_enabled(),
7147 "repeat loops",
7148 "are not available in this shell mode",
7149 )
7150 }
7151
7152 fn ensure_foreach_loop(&self) -> Result<()> {
7153 self.ensure_feature(
7154 self.zsh_short_loops_enabled(),
7155 "foreach loops",
7156 "are not available in this shell mode",
7157 )
7158 }
7159
7160 fn ensure_function_keyword(&self) -> Result<()> {
7161 self.ensure_feature(
7162 self.dialect.features().function_keyword,
7163 "function keyword definitions",
7164 "are not available in this shell mode",
7165 )
7166 }
7167
7168 fn tick(&mut self) -> Result<()> {
7170 if self.fuel == 0 {
7171 let used = self.max_fuel - self.fuel;
7172 return Err(Error::parse(format!(
7173 "parser fuel exhausted ({} operations, max {})",
7174 used, self.max_fuel
7175 )));
7176 }
7177 self.fuel -= 1;
7178 Ok(())
7179 }
7180
7181 fn push_depth(&mut self) -> Result<()> {
7183 self.current_depth += 1;
7184 if self.current_depth > self.max_depth {
7185 return Err(Error::parse(format!(
7186 "AST nesting too deep ({} levels, max {})",
7187 self.current_depth, self.max_depth
7188 )));
7189 }
7190 Ok(())
7191 }
7192
7193 fn pop_depth(&mut self) {
7195 if self.current_depth > 0 {
7196 self.current_depth -= 1;
7197 }
7198 }
7199
7200 fn check_error_token(&self) -> Result<()> {
7202 if self.current_token_kind == Some(TokenKind::Error) {
7203 let msg = self
7204 .current_token
7205 .as_ref()
7206 .and_then(LexedToken::error_kind)
7207 .map(|kind| kind.message())
7208 .unwrap_or("unknown lexer error");
7209 return Err(self.error(format!("syntax error: {}", msg)));
7210 }
7211 Ok(())
7212 }
7213
7214 fn parse_diagnostic_from_error(&self, error: Error) -> ParseDiagnostic {
7215 let Error::Parse { message, .. } = error;
7216 ParseDiagnostic {
7217 message,
7218 span: self.current_span,
7219 }
7220 }
7221
7222 fn compound_span(compound: &CompoundCommand) -> Span {
7223 match compound {
7224 CompoundCommand::If(command) => command.span,
7225 CompoundCommand::For(command) => command.span,
7226 CompoundCommand::Repeat(command) => command.span,
7227 CompoundCommand::Foreach(command) => command.span,
7228 CompoundCommand::ArithmeticFor(command) => command.span,
7229 CompoundCommand::While(command) => command.span,
7230 CompoundCommand::Until(command) => command.span,
7231 CompoundCommand::Case(command) => command.span,
7232 CompoundCommand::Select(command) => command.span,
7233 CompoundCommand::Subshell(body) | CompoundCommand::BraceGroup(body) => body.span,
7234 CompoundCommand::Arithmetic(command) => command.span,
7235 CompoundCommand::Time(command) => command.span,
7236 CompoundCommand::Conditional(command) => command.span,
7237 CompoundCommand::Coproc(command) => command.span,
7238 CompoundCommand::Always(command) => command.span,
7239 }
7240 }
7241
7242 fn stmt_seq_with_span(span: Span, stmts: Vec<Stmt>) -> StmtSeq {
7243 StmtSeq {
7244 leading_comments: Vec::new(),
7245 stmts,
7246 trailing_comments: Vec::new(),
7247 span,
7248 }
7249 }
7250
7251 fn binary_stmt(left: Stmt, op: BinaryOp, op_span: Span, right: Stmt) -> Stmt {
7252 let span = left.span.merge(right.span);
7253 Stmt {
7254 leading_comments: Vec::new(),
7255 command: AstCommand::Binary(BinaryCommand {
7256 left: Box::new(left),
7257 op,
7258 op_span,
7259 right: Box::new(right),
7260 span,
7261 }),
7262 negated: false,
7263 redirects: Box::default(),
7264 terminator: None,
7265 terminator_span: None,
7266 inline_comment: None,
7267 span,
7268 }
7269 }
7270
7271 fn lower_builtin_command(
7272 builtin: BuiltinCommand,
7273 ) -> (AstBuiltinCommand, SmallVec<[Redirect; 1]>, Span) {
7274 match builtin {
7275 BuiltinCommand::Break(command) => {
7276 let span = command.span;
7277 let redirects = command.redirects;
7278 (
7279 AstBuiltinCommand::Break(AstBreakCommand {
7280 depth: command.depth,
7281 extra_args: command.extra_args.into_vec(),
7282 assignments: command.assignments.into_boxed_slice(),
7283 span,
7284 }),
7285 redirects,
7286 span,
7287 )
7288 }
7289 BuiltinCommand::Continue(command) => {
7290 let span = command.span;
7291 let redirects = command.redirects;
7292 (
7293 AstBuiltinCommand::Continue(AstContinueCommand {
7294 depth: command.depth,
7295 extra_args: command.extra_args.into_vec(),
7296 assignments: command.assignments.into_boxed_slice(),
7297 span,
7298 }),
7299 redirects,
7300 span,
7301 )
7302 }
7303 BuiltinCommand::Return(command) => {
7304 let span = command.span;
7305 let redirects = command.redirects;
7306 (
7307 AstBuiltinCommand::Return(AstReturnCommand {
7308 code: command.code,
7309 extra_args: command.extra_args.into_vec(),
7310 assignments: command.assignments.into_boxed_slice(),
7311 span,
7312 }),
7313 redirects,
7314 span,
7315 )
7316 }
7317 BuiltinCommand::Exit(command) => {
7318 let span = command.span;
7319 let redirects = command.redirects;
7320 (
7321 AstBuiltinCommand::Exit(AstExitCommand {
7322 code: command.code,
7323 extra_args: command.extra_args.into_vec(),
7324 assignments: command.assignments.into_boxed_slice(),
7325 span,
7326 }),
7327 redirects,
7328 span,
7329 )
7330 }
7331 }
7332 }
7333
7334 fn lower_non_sequence_command_to_stmt(command: Command) -> Stmt {
7335 match command {
7336 Command::Simple(command) => Stmt {
7337 leading_comments: Vec::new(),
7338 command: AstCommand::Simple(AstSimpleCommand {
7339 name: command.name,
7340 args: command.args.into_vec(),
7341 assignments: command.assignments.into_boxed_slice(),
7342 span: command.span,
7343 }),
7344 negated: false,
7345 redirects: command.redirects.into_boxed_slice(),
7346 terminator: None,
7347 terminator_span: None,
7348 inline_comment: None,
7349 span: command.span,
7350 },
7351 Command::Builtin(command) => {
7352 let (command, redirects, span) = Self::lower_builtin_command(command);
7353 Stmt {
7354 leading_comments: Vec::new(),
7355 command: AstCommand::Builtin(command),
7356 negated: false,
7357 redirects: redirects.into_boxed_slice(),
7358 terminator: None,
7359 terminator_span: None,
7360 inline_comment: None,
7361 span,
7362 }
7363 }
7364 Command::Decl(command) => {
7365 let command = *command;
7366 Stmt {
7367 leading_comments: Vec::new(),
7368 command: AstCommand::Decl(AstDeclClause {
7369 variant: command.variant,
7370 variant_span: command.variant_span,
7371 operands: command.operands.into_vec(),
7372 assignments: command.assignments.into_boxed_slice(),
7373 span: command.span,
7374 }),
7375 negated: false,
7376 redirects: command.redirects.into_boxed_slice(),
7377 terminator: None,
7378 terminator_span: None,
7379 inline_comment: None,
7380 span: command.span,
7381 }
7382 }
7383 Command::Compound(compound, redirects) => {
7384 let span = Self::compound_span(&compound);
7385 Stmt {
7386 leading_comments: Vec::new(),
7387 command: AstCommand::Compound(*compound),
7388 negated: false,
7389 redirects: redirects.into_boxed_slice(),
7390 terminator: None,
7391 terminator_span: None,
7392 inline_comment: None,
7393 span,
7394 }
7395 }
7396 Command::Function(function) => Stmt {
7397 leading_comments: Vec::new(),
7398 span: function.span,
7399 command: AstCommand::Function(function),
7400 negated: false,
7401 redirects: Box::default(),
7402 terminator: None,
7403 terminator_span: None,
7404 inline_comment: None,
7405 },
7406 Command::AnonymousFunction(function, redirects) => Stmt {
7407 leading_comments: Vec::new(),
7408 span: function.span,
7409 command: AstCommand::AnonymousFunction(function),
7410 negated: false,
7411 redirects: redirects.into_boxed_slice(),
7412 terminator: None,
7413 terminator_span: None,
7414 inline_comment: None,
7415 },
7416 }
7417 }
7418
7419 fn comment_start(comment: Comment) -> usize {
7420 usize::from(comment.range.start())
7421 }
7422
7423 fn is_inline_comment(source: &str, stmt: &Stmt, comment: Comment) -> bool {
7424 let comment_start = Self::comment_start(comment);
7425 if comment_start < stmt.span.end.offset {
7426 return false;
7427 }
7428 source
7429 .get(stmt.span.end.offset..comment_start)
7430 .is_some_and(|gap| !gap.contains('\n'))
7431 }
7432
7433 fn take_comments_before(
7434 comments: &mut VecDeque<Comment>,
7435 end_offset: usize,
7436 ) -> VecDeque<Comment> {
7437 let mut taken = VecDeque::new();
7438 while comments
7439 .front()
7440 .is_some_and(|comment| Self::comment_start(*comment) < end_offset)
7441 {
7442 let Some(comment) = comments.pop_front() else {
7443 unreachable!("front comment should exist while draining");
7444 };
7445 taken.push_back(comment);
7446 }
7447 taken
7448 }
7449
7450 fn attach_comments_to_file(&self, file: &mut File) {
7451 let mut comments = self.comments.iter().copied().collect::<VecDeque<_>>();
7452 Self::attach_comments_to_stmt_seq_with_source(self.input, &mut file.body, &mut comments);
7453 file.body.trailing_comments.extend(comments);
7454 }
7455
7456 fn attach_comments_to_stmt_seq_with_source(
7457 source: &str,
7458 sequence: &mut StmtSeq,
7459 comments: &mut VecDeque<Comment>,
7460 ) {
7461 if sequence.stmts.is_empty() {
7462 sequence
7463 .trailing_comments
7464 .extend(Self::take_comments_before(
7465 comments,
7466 sequence.span.end.offset,
7467 ));
7468 return;
7469 }
7470
7471 for (index, stmt) in sequence.stmts.iter_mut().enumerate() {
7472 let leading = Self::take_comments_before(comments, stmt.span.start.offset);
7473 if index == 0 {
7474 sequence.leading_comments.extend(leading);
7475 } else {
7476 stmt.leading_comments.extend(leading);
7477 }
7478
7479 let mut nested = Self::take_comments_before(comments, stmt.span.end.offset);
7480 Self::attach_comments_to_stmt_with_source(source, stmt, &mut nested);
7481 if !nested.is_empty() {
7482 stmt.leading_comments.extend(nested);
7483 }
7484
7485 if stmt.inline_comment.is_none()
7486 && comments
7487 .front()
7488 .is_some_and(|comment| Self::is_inline_comment(source, stmt, *comment))
7489 {
7490 stmt.inline_comment = comments.pop_front();
7491 }
7492 }
7493
7494 sequence
7495 .trailing_comments
7496 .extend(Self::take_comments_before(
7497 comments,
7498 sequence.span.end.offset,
7499 ));
7500 }
7501
7502 fn attach_comments_to_stmt_with_source(
7503 source: &str,
7504 stmt: &mut Stmt,
7505 comments: &mut VecDeque<Comment>,
7506 ) {
7507 match &mut stmt.command {
7508 AstCommand::Binary(binary) => {
7509 let mut left_comments =
7510 Self::take_comments_before(comments, binary.left.span.end.offset);
7511 Self::attach_comments_to_stmt_with_source(
7512 source,
7513 binary.left.as_mut(),
7514 &mut left_comments,
7515 );
7516 if !left_comments.is_empty() {
7517 binary.left.leading_comments.extend(left_comments);
7518 }
7519
7520 let mut right_comments = std::mem::take(comments);
7521 Self::attach_comments_to_stmt_with_source(
7522 source,
7523 binary.right.as_mut(),
7524 &mut right_comments,
7525 );
7526 if !right_comments.is_empty() {
7527 binary.right.leading_comments.extend(right_comments);
7528 }
7529 }
7530 AstCommand::Compound(compound) => {
7531 Self::attach_comments_to_compound_with_source(source, compound, comments);
7532 }
7533 AstCommand::Function(function) => {
7534 let mut body_comments = std::mem::take(comments);
7535 Self::attach_comments_to_stmt_with_source(
7536 source,
7537 function.body.as_mut(),
7538 &mut body_comments,
7539 );
7540 if !body_comments.is_empty() {
7541 function.body.leading_comments.extend(body_comments);
7542 }
7543 }
7544 AstCommand::AnonymousFunction(function) => {
7545 let mut body_comments = std::mem::take(comments);
7546 Self::attach_comments_to_stmt_with_source(
7547 source,
7548 function.body.as_mut(),
7549 &mut body_comments,
7550 );
7551 if !body_comments.is_empty() {
7552 function.body.leading_comments.extend(body_comments);
7553 }
7554 }
7555 AstCommand::Simple(_) | AstCommand::Builtin(_) | AstCommand::Decl(_) => {}
7556 }
7557 }
7558
7559 fn attach_comments_to_compound_with_source(
7560 source: &str,
7561 command: &mut CompoundCommand,
7562 comments: &mut VecDeque<Comment>,
7563 ) {
7564 match command {
7565 CompoundCommand::If(command) => {
7566 let mut condition =
7567 Self::take_comments_before(comments, command.condition.span.end.offset);
7568 Self::attach_comments_to_stmt_seq_with_source(
7569 source,
7570 &mut command.condition,
7571 &mut condition,
7572 );
7573 command.condition.trailing_comments.extend(condition);
7574
7575 let mut then_branch =
7576 Self::take_comments_before(comments, command.then_branch.span.end.offset);
7577 Self::attach_comments_to_stmt_seq_with_source(
7578 source,
7579 &mut command.then_branch,
7580 &mut then_branch,
7581 );
7582 command.then_branch.trailing_comments.extend(then_branch);
7583
7584 for (condition_seq, body_seq) in &mut command.elif_branches {
7585 let mut elif_condition =
7586 Self::take_comments_before(comments, condition_seq.span.end.offset);
7587 Self::attach_comments_to_stmt_seq_with_source(
7588 source,
7589 condition_seq,
7590 &mut elif_condition,
7591 );
7592 condition_seq.trailing_comments.extend(elif_condition);
7593
7594 let mut elif_body =
7595 Self::take_comments_before(comments, body_seq.span.end.offset);
7596 Self::attach_comments_to_stmt_seq_with_source(source, body_seq, &mut elif_body);
7597 body_seq.trailing_comments.extend(elif_body);
7598 }
7599
7600 if let Some(else_branch) = &mut command.else_branch {
7601 let mut else_comments = std::mem::take(comments);
7602 Self::attach_comments_to_stmt_seq_with_source(
7603 source,
7604 else_branch,
7605 &mut else_comments,
7606 );
7607 else_branch.trailing_comments.extend(else_comments);
7608 }
7609 }
7610 CompoundCommand::For(command) => {
7611 let mut body_comments = std::mem::take(comments);
7612 Self::attach_comments_to_stmt_seq_with_source(
7613 source,
7614 &mut command.body,
7615 &mut body_comments,
7616 );
7617 command.body.trailing_comments.extend(body_comments);
7618 }
7619 CompoundCommand::Repeat(command) => {
7620 let mut body_comments = std::mem::take(comments);
7621 Self::attach_comments_to_stmt_seq_with_source(
7622 source,
7623 &mut command.body,
7624 &mut body_comments,
7625 );
7626 command.body.trailing_comments.extend(body_comments);
7627 }
7628 CompoundCommand::Foreach(command) => {
7629 let mut body_comments = std::mem::take(comments);
7630 Self::attach_comments_to_stmt_seq_with_source(
7631 source,
7632 &mut command.body,
7633 &mut body_comments,
7634 );
7635 command.body.trailing_comments.extend(body_comments);
7636 }
7637 CompoundCommand::ArithmeticFor(command) => {
7638 let mut body_comments = std::mem::take(comments);
7639 Self::attach_comments_to_stmt_seq_with_source(
7640 source,
7641 &mut command.body,
7642 &mut body_comments,
7643 );
7644 command.body.trailing_comments.extend(body_comments);
7645 }
7646 CompoundCommand::While(command) => {
7647 let mut condition =
7648 Self::take_comments_before(comments, command.condition.span.end.offset);
7649 Self::attach_comments_to_stmt_seq_with_source(
7650 source,
7651 &mut command.condition,
7652 &mut condition,
7653 );
7654 command.condition.trailing_comments.extend(condition);
7655
7656 let mut body_comments = std::mem::take(comments);
7657 Self::attach_comments_to_stmt_seq_with_source(
7658 source,
7659 &mut command.body,
7660 &mut body_comments,
7661 );
7662 command.body.trailing_comments.extend(body_comments);
7663 }
7664 CompoundCommand::Until(command) => {
7665 let mut condition =
7666 Self::take_comments_before(comments, command.condition.span.end.offset);
7667 Self::attach_comments_to_stmt_seq_with_source(
7668 source,
7669 &mut command.condition,
7670 &mut condition,
7671 );
7672 command.condition.trailing_comments.extend(condition);
7673
7674 let mut body_comments = std::mem::take(comments);
7675 Self::attach_comments_to_stmt_seq_with_source(
7676 source,
7677 &mut command.body,
7678 &mut body_comments,
7679 );
7680 command.body.trailing_comments.extend(body_comments);
7681 }
7682 CompoundCommand::Case(command) => {
7683 for case in &mut command.cases {
7684 let mut body_comments =
7685 Self::take_comments_before(comments, case.body.span.end.offset);
7686 Self::attach_comments_to_stmt_seq_with_source(
7687 source,
7688 &mut case.body,
7689 &mut body_comments,
7690 );
7691 case.body.trailing_comments.extend(body_comments);
7692 }
7693 }
7694 CompoundCommand::Select(command) => {
7695 let mut body_comments = std::mem::take(comments);
7696 Self::attach_comments_to_stmt_seq_with_source(
7697 source,
7698 &mut command.body,
7699 &mut body_comments,
7700 );
7701 command.body.trailing_comments.extend(body_comments);
7702 }
7703 CompoundCommand::Subshell(body) | CompoundCommand::BraceGroup(body) => {
7704 let mut body_comments = std::mem::take(comments);
7705 Self::attach_comments_to_stmt_seq_with_source(source, body, &mut body_comments);
7706 body.trailing_comments.extend(body_comments);
7707 }
7708 CompoundCommand::Always(command) => {
7709 let mut body_comments =
7710 Self::take_comments_before(comments, command.body.span.end.offset);
7711 Self::attach_comments_to_stmt_seq_with_source(
7712 source,
7713 &mut command.body,
7714 &mut body_comments,
7715 );
7716 command.body.trailing_comments.extend(body_comments);
7717
7718 let mut always_comments = std::mem::take(comments);
7719 Self::attach_comments_to_stmt_seq_with_source(
7720 source,
7721 &mut command.always_body,
7722 &mut always_comments,
7723 );
7724 command
7725 .always_body
7726 .trailing_comments
7727 .extend(always_comments);
7728 }
7729 CompoundCommand::Time(command) => {
7730 if let Some(inner) = &mut command.command {
7731 let mut inner_comments = std::mem::take(comments);
7732 Self::attach_comments_to_stmt_with_source(
7733 source,
7734 inner.as_mut(),
7735 &mut inner_comments,
7736 );
7737 if !inner_comments.is_empty() {
7738 inner.leading_comments.extend(inner_comments);
7739 }
7740 }
7741 }
7742 CompoundCommand::Coproc(command) => {
7743 let mut body_comments = std::mem::take(comments);
7744 Self::attach_comments_to_stmt_with_source(
7745 source,
7746 command.body.as_mut(),
7747 &mut body_comments,
7748 );
7749 if !body_comments.is_empty() {
7750 command.body.leading_comments.extend(body_comments);
7751 }
7752 }
7753 CompoundCommand::Arithmetic(_) | CompoundCommand::Conditional(_) => {}
7754 }
7755 }
7756
7757 fn advance_raw(&mut self) {
7758 #[cfg(feature = "benchmarking")]
7759 self.maybe_record_advance_raw_call();
7760 if let Some(peeked) = self.peeked_token.take() {
7761 self.set_current_spanned(peeked);
7762 } else {
7763 loop {
7764 match self.next_spanned_token_with_comments() {
7765 Some(st) if st.kind == TokenKind::Comment => {
7766 self.maybe_record_comment(&st);
7767 }
7768 Some(st) => {
7769 self.set_current_spanned(st);
7770 break;
7771 }
7772 None => {
7773 self.clear_current_token();
7774 break;
7776 }
7777 }
7778 }
7779 }
7780 }
7781
7782 #[cfg(feature = "benchmarking")]
7783 fn maybe_record_set_current_spanned_call(&mut self) {
7784 if let Some(counters) = &mut self.benchmark_counters {
7785 counters.parser_set_current_spanned_calls += 1;
7786 }
7787 }
7788
7789 #[cfg(feature = "benchmarking")]
7790 fn maybe_record_advance_raw_call(&mut self) {
7791 if let Some(counters) = &mut self.benchmark_counters {
7792 counters.parser_advance_raw_calls += 1;
7793 }
7794 }
7795
7796 #[cfg(feature = "benchmarking")]
7797 fn finish_benchmark_counters(&self) -> ParserBenchmarkCounters {
7798 let mut counters = self.benchmark_counters.unwrap_or_default();
7799 counters.lexer_current_position_calls =
7800 self.lexer.benchmark_counters().current_position_calls;
7801 counters
7802 }
7803
7804 fn advance(&mut self) {
7805 let should_expand = std::mem::take(&mut self.expand_next_word);
7806 self.advance_raw();
7807 if should_expand {
7808 if self
7809 .current_token
7810 .as_ref()
7811 .is_some_and(|token| token.flags.is_synthetic())
7812 {
7813 self.expand_next_word = true;
7814 } else {
7815 self.maybe_expand_current_alias_chain();
7816 }
7817 }
7818 }
7819
7820 fn peek_next(&mut self) -> Option<&LexedToken<'a>> {
7822 if self.peeked_token.is_none() {
7823 loop {
7824 match self.next_spanned_token_with_comments() {
7825 Some(st) if st.kind == TokenKind::Comment => {
7826 self.maybe_record_comment(&st);
7827 }
7828 other => {
7829 self.peeked_token = other;
7830 break;
7831 }
7832 }
7833 }
7834 }
7835 self.peeked_token.as_ref()
7836 }
7837
7838 fn peek_next_kind(&mut self) -> Option<TokenKind> {
7839 self.peek_next()?;
7840 self.peeked_token.as_ref().map(|st| st.kind)
7841 }
7842
7843 fn peek_next_is(&mut self, kind: TokenKind) -> bool {
7844 self.peek_next_kind() == Some(kind)
7845 }
7846
7847 fn at(&self, kind: TokenKind) -> bool {
7848 self.current_token_kind == Some(kind)
7849 }
7850
7851 fn current_token_has_leading_whitespace(&self) -> bool {
7852 self.current_span.start.offset > 0
7853 && self.input[..self.current_span.start.offset]
7854 .chars()
7855 .next_back()
7856 .is_some_and(|ch| matches!(ch, ' ' | '\t' | '\n'))
7857 }
7858
7859 fn current_token_is_tight_to_next_token(&mut self) -> bool {
7860 let current_end = self.current_span.end.offset;
7861 self.peek_next()
7862 .is_some_and(|token| token.span.start.offset == current_end)
7863 }
7864
7865 fn at_in_set(&self, set: TokenSet) -> bool {
7866 self.current_token_kind
7867 .is_some_and(|kind| set.contains(kind))
7868 }
7869
7870 fn at_word_like(&self) -> bool {
7871 self.current_token_kind.is_some_and(TokenKind::is_word_like)
7872 }
7873
7874 fn current_word_str(&self) -> Option<&str> {
7875 self.current_token_kind
7876 .filter(|kind| kind.is_word_like())
7877 .and(self.current_token.as_ref())
7878 .and_then(LexedToken::word_text)
7879 }
7880
7881 fn classify_keyword(word: &str) -> Option<Keyword> {
7882 match word.as_bytes() {
7883 b"if" => Some(Keyword::If),
7884 b"for" => Some(Keyword::For),
7885 b"repeat" => Some(Keyword::Repeat),
7886 b"foreach" => Some(Keyword::Foreach),
7887 b"while" => Some(Keyword::While),
7888 b"until" => Some(Keyword::Until),
7889 b"case" => Some(Keyword::Case),
7890 b"select" => Some(Keyword::Select),
7891 b"time" => Some(Keyword::Time),
7892 b"coproc" => Some(Keyword::Coproc),
7893 b"function" => Some(Keyword::Function),
7894 b"always" => Some(Keyword::Always),
7895 b"then" => Some(Keyword::Then),
7896 b"else" => Some(Keyword::Else),
7897 b"elif" => Some(Keyword::Elif),
7898 b"fi" => Some(Keyword::Fi),
7899 b"do" => Some(Keyword::Do),
7900 b"done" => Some(Keyword::Done),
7901 b"esac" => Some(Keyword::Esac),
7902 b"in" => Some(Keyword::In),
7903 _ => None,
7904 }
7905 }
7906
7907 fn current_keyword(&self) -> Option<Keyword> {
7908 self.current_keyword
7909 }
7910
7911 fn looks_like_disabled_repeat_loop(&mut self) -> Result<bool> {
7912 if self.current_keyword() != Some(Keyword::Repeat) {
7913 return Ok(false);
7914 }
7915
7916 let checkpoint = self.checkpoint();
7917 self.advance();
7918 if !self.at_word_like() {
7919 self.restore(checkpoint);
7920 return Ok(false);
7921 }
7922 self.advance();
7923
7924 let result = match self.current_token_kind {
7925 Some(TokenKind::LeftBrace) => Ok(true),
7926 Some(TokenKind::Semicolon) => {
7927 self.advance();
7928 if let Err(error) = self.skip_newlines() {
7929 self.restore(checkpoint);
7930 return Err(error);
7931 }
7932 Ok(self.current_keyword() == Some(Keyword::Do))
7933 }
7934 Some(TokenKind::Newline) => {
7935 if let Err(error) = self.skip_newlines() {
7936 self.restore(checkpoint);
7937 return Err(error);
7938 }
7939 Ok(self.current_keyword() == Some(Keyword::Do))
7940 }
7941 _ => Ok(false),
7942 };
7943 self.restore(checkpoint);
7944 result
7945 }
7946
7947 fn looks_like_disabled_foreach_loop(&mut self) -> Result<bool> {
7948 if self.current_keyword() != Some(Keyword::Foreach) {
7949 return Ok(false);
7950 }
7951
7952 let checkpoint = self.checkpoint();
7953 self.advance();
7954 if self.current_name_token().is_none() {
7955 self.restore(checkpoint);
7956 return Ok(false);
7957 }
7958 self.advance();
7959
7960 let result = if self.at(TokenKind::LeftParen) {
7961 self.advance();
7962 let mut saw_word = false;
7963 while !self.at(TokenKind::RightParen) {
7964 if !self.at_word_like() {
7965 self.restore(checkpoint);
7966 return Ok(false);
7967 }
7968 saw_word = true;
7969 self.advance();
7970 }
7971 if !saw_word {
7972 self.restore(checkpoint);
7973 return Ok(false);
7974 }
7975 self.advance();
7976 Ok(self.at(TokenKind::LeftBrace))
7977 } else {
7978 if self.current_keyword() != Some(Keyword::In) {
7979 self.restore(checkpoint);
7980 return Ok(false);
7981 }
7982 self.advance();
7983
7984 let mut saw_word = false;
7985 let saw_separator = loop {
7986 if self.current_keyword() == Some(Keyword::Do) {
7987 break false;
7988 }
7989
7990 match self.current_token_kind {
7991 Some(kind) if kind.is_word_like() => {
7992 saw_word = true;
7993 self.advance();
7994 }
7995 Some(TokenKind::Semicolon) => {
7996 self.advance();
7997 break true;
7998 }
7999 Some(TokenKind::Newline) => {
8000 if let Err(error) = self.skip_newlines() {
8001 self.restore(checkpoint);
8002 return Err(error);
8003 }
8004 break true;
8005 }
8006 _ => break false,
8007 }
8008 };
8009
8010 Ok(saw_word && saw_separator && self.current_keyword() == Some(Keyword::Do))
8011 };
8012 self.restore(checkpoint);
8013 result
8014 }
8015
8016 fn skip_newlines_with_flag(&mut self) -> Result<bool> {
8017 let mut skipped = false;
8018 while self.at(TokenKind::Newline) {
8019 self.tick()?;
8020 self.advance();
8021 skipped = true;
8022 }
8023 Ok(skipped)
8024 }
8025
8026 fn skip_newlines(&mut self) -> Result<()> {
8027 self.skip_newlines_with_flag().map(|_| ())
8028 }
8029}
8030#[cfg(test)]
8031mod tests;