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