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 let kind = self.current_token_kind?;
2327 if !matches!(kind, TokenKind::LeftParen | TokenKind::Word) {
2328 return None;
2329 }
2330 if kind == TokenKind::Word && !self.dialect.features().zsh_glob_qualifiers {
2335 return None;
2336 }
2337
2338 let start = self.current_span.start;
2339 if !self.source_word_contains_zsh_glob_control(start) {
2340 return None;
2341 }
2342 let (text, end) = self.scan_source_word(start)?;
2343 if !text.contains("(#") {
2344 return None;
2345 }
2346 let span = Span::from_positions(start, end);
2347 if self.zsh_glob_qualifiers_enabled_at(span.start.offset)
2348 && let Some(word) = self.maybe_parse_zsh_qualified_glob_word(&text, span, true)
2349 {
2350 return Some(word);
2351 }
2352
2353 Some(self.parse_word_with_context(&text, span, start, true))
2354 }
2355
2356 fn source_word_contains_zsh_glob_control(&self, start: Position) -> bool {
2357 if start.offset >= self.input.len() {
2358 return false;
2359 }
2360
2361 let source = &self.input[start.offset..];
2362 let mut chars = source.chars().peekable();
2363 let mut cursor = start;
2364 let mut paren_depth = 0_i32;
2365 let mut brace_depth = 0_i32;
2366 let mut in_single = false;
2367 let mut in_double = false;
2368 let mut in_backtick = false;
2369 let mut escaped = false;
2370 let mut prev_char = None;
2371
2372 while let Some(&ch) = chars.peek() {
2373 if !in_single
2374 && !in_double
2375 && !in_backtick
2376 && paren_depth == 0
2377 && brace_depth == 0
2378 && matches!(ch, ' ' | '\t' | '\n' | ';' | '|' | '&' | '>' | '<' | ')')
2379 {
2380 break;
2381 }
2382
2383 let ch = Self::next_word_char_unwrap(&mut chars, &mut cursor);
2384 if !in_single && !in_double && !in_backtick && prev_char == Some('(') && ch == '#' {
2385 return true;
2386 }
2387
2388 if escaped {
2389 escaped = false;
2390 prev_char = Some(ch);
2391 continue;
2392 }
2393
2394 match ch {
2395 '\\' if !in_single => escaped = true,
2396 '\'' if !in_double => in_single = !in_single,
2397 '"' if !in_single => in_double = !in_double,
2398 '`' if !in_single => in_backtick = !in_backtick,
2399 '(' if !in_single && !in_double => paren_depth += 1,
2400 ')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
2401 '{' if !in_single && !in_double => brace_depth += 1,
2402 '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
2403 _ => {}
2404 }
2405
2406 prev_char = Some(ch);
2407 }
2408
2409 false
2410 }
2411
2412 fn scan_source_word(&self, start: Position) -> Option<(String, Position)> {
2413 if start.offset >= self.input.len() {
2414 return None;
2415 }
2416
2417 let source = &self.input[start.offset..];
2418 let mut chars = source.chars().peekable();
2419 let mut cursor = start;
2420 let mut text = String::new();
2421 let mut paren_depth = 0_i32;
2422 let mut brace_depth = 0_i32;
2423 let mut in_single = false;
2424 let mut in_double = false;
2425 let mut in_backtick = false;
2426 let mut escaped = false;
2427
2428 while let Some(&ch) = chars.peek() {
2429 if !in_single
2430 && !in_double
2431 && !in_backtick
2432 && paren_depth == 0
2433 && brace_depth == 0
2434 && matches!(ch, ' ' | '\t' | '\n' | ';' | '|' | '&' | '>' | '<' | ')')
2435 {
2436 break;
2437 }
2438
2439 let ch = Self::next_word_char_unwrap(&mut chars, &mut cursor);
2440 text.push(ch);
2441
2442 if escaped {
2443 escaped = false;
2444 continue;
2445 }
2446
2447 match ch {
2448 '\\' if !in_single => escaped = true,
2449 '\'' if !in_double => in_single = !in_single,
2450 '"' if !in_single => in_double = !in_double,
2451 '`' if !in_single => in_backtick = !in_backtick,
2452 '(' if !in_single && !in_double => paren_depth += 1,
2453 ')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
2454 '{' if !in_single && !in_double => brace_depth += 1,
2455 '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
2456 _ => {}
2457 }
2458 }
2459
2460 (!text.is_empty()).then_some((text, cursor))
2461 }
2462
2463 fn advance_past_word(&mut self, word: &Word) {
2464 let stop_after_synthetic = self
2465 .current_token
2466 .as_ref()
2467 .is_some_and(|token| token.flags.is_synthetic());
2468 while self.current_token.is_some() && self.current_span.start.offset < word.span.end.offset
2469 {
2470 self.advance();
2471 if stop_after_synthetic
2472 && self
2473 .current_token
2474 .as_ref()
2475 .is_none_or(|token| !token.flags.is_synthetic())
2476 {
2477 break;
2478 }
2479 }
2480 }
2481
2482 fn token_source_like_word_text(&self, token: &LexedToken<'a>) -> Option<Cow<'a, str>> {
2483 token
2484 .source_slice(self.input)
2485 .map(Cow::Borrowed)
2486 .or_else(|| {
2487 (token.span.start.offset <= token.span.end.offset
2488 && token.span.end.offset <= self.input.len())
2489 .then(|| Cow::Borrowed(&self.input[token.span.start.offset..token.span.end.offset]))
2490 })
2491 .or_else(|| token.word_string().map(Cow::Owned))
2492 }
2493
2494 fn current_source_like_word_text(&self) -> Option<Cow<'a, str>> {
2495 self.current_token_kind
2496 .filter(|kind| kind.is_word_like())
2497 .and(self.current_token.as_ref())
2498 .and_then(|token| self.token_source_like_word_text(token))
2499 }
2500
2501 fn current_source_like_word_text_or_error(
2502 &self,
2503 context: &'static str,
2504 ) -> Result<Cow<'a, str>> {
2505 self.current_source_like_word_text().ok_or_else(|| {
2506 self.error(format!(
2507 "internal parser error: missing source text for {context}"
2508 ))
2509 })
2510 }
2511
2512 fn keyword_from_token(token: &LexedToken<'_>) -> Option<Keyword> {
2513 (token.kind == TokenKind::Word)
2514 .then(|| token.word_text())
2515 .flatten()
2516 .and_then(Self::classify_keyword)
2517 }
2518
2519 fn current_conditional_literal_word(&self) -> Option<Word> {
2520 match self.current_token_kind? {
2521 TokenKind::LeftBrace | TokenKind::RightBrace => Some(Word::literal_with_span(
2522 self.input[self.current_span.start.offset..self.current_span.end.offset]
2523 .to_string(),
2524 self.current_span,
2525 )),
2526 _ => None,
2527 }
2528 }
2529
2530 fn current_name_token(&self) -> Option<(Name, Span)> {
2531 self.current_token_kind
2532 .filter(|kind| kind.is_word_like())
2533 .and_then(|_| self.current_word_str())
2534 .map(|word| (Name::from(word), self.current_span))
2535 }
2536
2537 fn current_static_token_text(&self) -> Option<(String, bool)> {
2538 let token = self.current_token.as_ref()?;
2539 let raw_text = token.word_string()?;
2540 let text_had_escape_markers = raw_text.contains('\x00');
2541 let text = if text_had_escape_markers {
2542 raw_text.replace('\x00', "")
2543 } else {
2544 raw_text
2545 };
2546
2547 match token.kind {
2548 TokenKind::LiteralWord => Some((text, true)),
2549 TokenKind::QuotedWord if !Self::word_text_needs_parse(&text) => Some((text, true)),
2550 TokenKind::Word if !Self::word_text_needs_parse(&text) => Some((
2551 text,
2552 token.flags.has_cooked_text() || text_had_escape_markers,
2553 )),
2554 _ => None,
2555 }
2556 }
2557
2558 fn nested_stmt_seq_from_source(&mut self, source: &str, base: Position) -> StmtSeq {
2559 let remaining_depth = self.max_depth.saturating_sub(self.current_depth);
2560 let nested_profile = self
2561 .current_zsh_options()
2562 .cloned()
2563 .map(|options| ShellProfile::with_zsh_options(self.dialect, options))
2564 .unwrap_or_else(|| self.shell_profile.clone());
2565 let inner_parser =
2566 Parser::with_limits_and_profile(source, remaining_depth, self.fuel, nested_profile);
2567 let mut output = inner_parser.parse();
2568 if output.is_ok() {
2569 Self::materialize_stmt_seq_source_backing(&mut output.file.body, source);
2570 Self::rebase_file(&mut output.file, base);
2571 output.file.body
2572 } else {
2573 StmtSeq {
2574 leading_comments: Vec::new(),
2575 stmts: Vec::new(),
2576 trailing_comments: Vec::new(),
2577 span: Span::from_positions(base, base),
2578 }
2579 }
2580 }
2581
2582 fn nested_stmt_seq_from_current_input(&mut self, start: Position, end: Position) -> StmtSeq {
2583 if start.offset > end.offset || end.offset > self.input.len() {
2584 return StmtSeq {
2585 leading_comments: Vec::new(),
2586 stmts: Vec::new(),
2587 trailing_comments: Vec::new(),
2588 span: Span::from_positions(start, start),
2589 };
2590 }
2591 let source = &self.input[start.offset..end.offset];
2592 self.nested_stmt_seq_from_source(source, start)
2593 }
2594
2595 fn merge_optional_span(primary: Span, other: Span) -> Span {
2596 if other == Span::new() {
2597 primary
2598 } else {
2599 primary.merge(other)
2600 }
2601 }
2602
2603 fn redirect_span(operator_span: Span, target: &Word) -> Span {
2604 Self::merge_optional_span(operator_span, target.span)
2605 }
2606
2607 fn optional_span(start: Position, end: Position) -> Option<Span> {
2608 (start.offset < end.offset).then(|| Span::from_positions(start, end))
2609 }
2610
2611 fn split_nested_arithmetic_close(&mut self, context: &'static str) -> Result<Span> {
2612 let right_paren_start = self.current_span.start.advanced_by(")");
2613 self.advance();
2614
2615 if self.at(TokenKind::RightParen) {
2616 let right_paren_span = Span::from_positions(right_paren_start, self.current_span.end);
2617 self.advance();
2618 Ok(right_paren_span)
2619 } else {
2620 Err(Error::parse(format!(
2621 "expected ')' after '))' in {context}"
2622 )))
2623 }
2624 }
2625
2626 fn split_double_semicolon(span: Span) -> (Span, Span) {
2627 let middle = span.start.advanced_by(";");
2628 (
2629 Span::from_positions(span.start, middle),
2630 Span::from_positions(middle, span.end),
2631 )
2632 }
2633
2634 fn split_double_left_paren(span: Span) -> (Span, Span) {
2635 let middle = span.start.advanced_by("(");
2636 (
2637 Span::from_positions(span.start, middle),
2638 Span::from_positions(middle, span.end),
2639 )
2640 }
2641
2642 fn split_double_right_paren(span: Span) -> (Span, Span) {
2643 let middle = span.start.advanced_by(")");
2644 (
2645 Span::from_positions(span.start, middle),
2646 Span::from_positions(middle, span.end),
2647 )
2648 }
2649
2650 fn record_arithmetic_for_separator(
2651 semicolon_span: Span,
2652 segment_start: &mut Position,
2653 init_span: &mut Option<Span>,
2654 first_semicolon_span: &mut Option<Span>,
2655 condition_span: &mut Option<Span>,
2656 second_semicolon_span: &mut Option<Span>,
2657 ) -> Result<()> {
2658 if first_semicolon_span.is_none() {
2659 *init_span = Self::optional_span(*segment_start, semicolon_span.start);
2660 *first_semicolon_span = Some(semicolon_span);
2661 *segment_start = semicolon_span.end;
2662 return Ok(());
2663 }
2664
2665 if second_semicolon_span.is_none() {
2666 *condition_span = Self::optional_span(*segment_start, semicolon_span.start);
2667 *second_semicolon_span = Some(semicolon_span);
2668 *segment_start = semicolon_span.end;
2669 return Ok(());
2670 }
2671
2672 Err(Error::parse(
2673 "unexpected ';' in arithmetic for header".to_string(),
2674 ))
2675 }
2676
2677 fn rebase_file(file: &mut File, base: Position) {
2678 file.span = file.span.rebased(base);
2679 Self::rebase_stmt_seq(&mut file.body, base);
2680 }
2681
2682 fn rebase_comments(comments: &mut [Comment], base: Position) {
2683 let base_offset = TextSize::new(base.offset as u32);
2684 for comment in comments {
2685 comment.range = comment.range.offset_by(base_offset);
2686 }
2687 }
2688
2689 fn rebase_stmt_seq(sequence: &mut StmtSeq, base: Position) {
2690 sequence.span = sequence.span.rebased(base);
2691 Self::rebase_comments(&mut sequence.leading_comments, base);
2692 for stmt in &mut sequence.stmts {
2693 Self::rebase_stmt(stmt, base);
2694 }
2695 Self::rebase_comments(&mut sequence.trailing_comments, base);
2696 }
2697
2698 fn rebase_stmt(stmt: &mut Stmt, base: Position) {
2699 stmt.span = stmt.span.rebased(base);
2700 Self::rebase_comments(&mut stmt.leading_comments, base);
2701 stmt.terminator_span = stmt.terminator_span.map(|span| span.rebased(base));
2702 if let Some(comment) = &mut stmt.inline_comment {
2703 let base_offset = TextSize::new(base.offset as u32);
2704 comment.range = comment.range.offset_by(base_offset);
2705 }
2706 Self::rebase_redirects(&mut stmt.redirects, base);
2707 Self::rebase_ast_command(&mut stmt.command, base);
2708 }
2709
2710 fn rebase_ast_command(command: &mut AstCommand, base: Position) {
2711 match command {
2712 AstCommand::Simple(simple) => {
2713 simple.span = simple.span.rebased(base);
2714 Self::rebase_word(&mut simple.name, base);
2715 Self::rebase_words(&mut simple.args, base);
2716 Self::rebase_assignments(&mut simple.assignments, base);
2717 }
2718 AstCommand::Builtin(builtin) => match builtin {
2719 AstBuiltinCommand::Break(command) => {
2720 command.span = command.span.rebased(base);
2721 if let Some(depth) = &mut command.depth {
2722 Self::rebase_word(depth, base);
2723 }
2724 Self::rebase_words(&mut command.extra_args, base);
2725 Self::rebase_assignments(&mut command.assignments, base);
2726 }
2727 AstBuiltinCommand::Continue(command) => {
2728 command.span = command.span.rebased(base);
2729 if let Some(depth) = &mut command.depth {
2730 Self::rebase_word(depth, base);
2731 }
2732 Self::rebase_words(&mut command.extra_args, base);
2733 Self::rebase_assignments(&mut command.assignments, base);
2734 }
2735 AstBuiltinCommand::Return(command) => {
2736 command.span = command.span.rebased(base);
2737 if let Some(code) = &mut command.code {
2738 Self::rebase_word(code, base);
2739 }
2740 Self::rebase_words(&mut command.extra_args, base);
2741 Self::rebase_assignments(&mut command.assignments, base);
2742 }
2743 AstBuiltinCommand::Exit(command) => {
2744 command.span = command.span.rebased(base);
2745 if let Some(code) = &mut command.code {
2746 Self::rebase_word(code, base);
2747 }
2748 Self::rebase_words(&mut command.extra_args, base);
2749 Self::rebase_assignments(&mut command.assignments, base);
2750 }
2751 },
2752 AstCommand::Decl(decl) => {
2753 decl.span = decl.span.rebased(base);
2754 decl.variant_span = decl.variant_span.rebased(base);
2755 Self::rebase_assignments(&mut decl.assignments, base);
2756 for operand in &mut decl.operands {
2757 match operand {
2758 DeclOperand::Flag(word) | DeclOperand::Dynamic(word) => {
2759 Self::rebase_word(word, base);
2760 }
2761 DeclOperand::Name(name) => Self::rebase_var_ref(name, base),
2762 DeclOperand::Assignment(assignment) => {
2763 Self::rebase_assignments(std::slice::from_mut(assignment), base);
2764 }
2765 }
2766 }
2767 }
2768 AstCommand::Binary(binary) => {
2769 binary.span = binary.span.rebased(base);
2770 binary.op_span = binary.op_span.rebased(base);
2771 Self::rebase_stmt(binary.left.as_mut(), base);
2772 Self::rebase_stmt(binary.right.as_mut(), base);
2773 }
2774 AstCommand::Compound(compound) => Self::rebase_compound(compound, base),
2775 AstCommand::Function(function) => {
2776 function.span = function.span.rebased(base);
2777 if let Some(span) = &mut function.header.function_keyword_span {
2778 *span = span.rebased(base);
2779 }
2780 if let Some(span) = &mut function.header.trailing_parens_span {
2781 *span = span.rebased(base);
2782 }
2783 for entry in &mut function.header.entries {
2784 Self::rebase_word(&mut entry.word, base);
2785 }
2786 Self::rebase_stmt(function.body.as_mut(), base);
2787 }
2788 AstCommand::AnonymousFunction(function) => {
2789 function.span = function.span.rebased(base);
2790 function.surface = match function.surface {
2791 AnonymousFunctionSurface::FunctionKeyword {
2792 function_keyword_span,
2793 } => AnonymousFunctionSurface::FunctionKeyword {
2794 function_keyword_span: function_keyword_span.rebased(base),
2795 },
2796 AnonymousFunctionSurface::Parens { parens_span } => {
2797 AnonymousFunctionSurface::Parens {
2798 parens_span: parens_span.rebased(base),
2799 }
2800 }
2801 };
2802 Self::rebase_stmt(function.body.as_mut(), base);
2803 Self::rebase_words(&mut function.args, base);
2804 }
2805 }
2806 }
2807
2808 fn rebase_subscript(subscript: &mut Subscript, base: Position) {
2809 subscript.text.rebased(base);
2810 if let Some(raw) = &mut subscript.raw {
2811 raw.rebased(base);
2812 }
2813 if let Some(word) = &mut subscript.word_ast {
2814 Self::rebase_word(word, base);
2815 }
2816 if let Some(expr) = &mut subscript.arithmetic_ast {
2817 Self::rebase_arithmetic_expr(expr, base);
2818 }
2819 }
2820
2821 fn rebase_var_ref(reference: &mut VarRef, base: Position) {
2822 reference.span = reference.span.rebased(base);
2823 reference.name_span = reference.name_span.rebased(base);
2824 if let Some(subscript) = &mut reference.subscript {
2825 Self::rebase_subscript(subscript, base);
2826 }
2827 }
2828
2829 fn rebase_array_expr(array: &mut ArrayExpr, base: Position) {
2830 array.span = array.span.rebased(base);
2831 for element in &mut array.elements {
2832 match element {
2833 ArrayElem::Sequential(word) => Self::rebase_word(word, base),
2834 ArrayElem::Keyed { key, value } | ArrayElem::KeyedAppend { key, value } => {
2835 Self::rebase_subscript(key, base);
2836 Self::rebase_word(value, base);
2837 }
2838 }
2839 }
2840 }
2841
2842 fn rebase_compound(compound: &mut CompoundCommand, base: Position) {
2843 match compound {
2844 CompoundCommand::If(command) => {
2845 command.span = command.span.rebased(base);
2846 command.syntax = match command.syntax {
2847 IfSyntax::ThenFi { then_span, fi_span } => IfSyntax::ThenFi {
2848 then_span: then_span.rebased(base),
2849 fi_span: fi_span.rebased(base),
2850 },
2851 IfSyntax::Brace {
2852 left_brace_span,
2853 right_brace_span,
2854 } => IfSyntax::Brace {
2855 left_brace_span: left_brace_span.rebased(base),
2856 right_brace_span: right_brace_span.rebased(base),
2857 },
2858 };
2859 Self::rebase_stmt_seq(&mut command.condition, base);
2860 Self::rebase_stmt_seq(&mut command.then_branch, base);
2861 for (condition, body) in &mut command.elif_branches {
2862 Self::rebase_stmt_seq(condition, base);
2863 Self::rebase_stmt_seq(body, base);
2864 }
2865 if let Some(else_branch) = &mut command.else_branch {
2866 Self::rebase_stmt_seq(else_branch, base);
2867 }
2868 }
2869 CompoundCommand::For(command) => {
2870 command.span = command.span.rebased(base);
2871 for target in &mut command.targets {
2872 target.span = target.span.rebased(base);
2873 }
2874 if let Some(words) = &mut command.words {
2875 Self::rebase_words(words, base);
2876 }
2877 command.syntax = match command.syntax {
2878 ForSyntax::InDoDone {
2879 in_span,
2880 do_span,
2881 done_span,
2882 } => ForSyntax::InDoDone {
2883 in_span: in_span.map(|span| span.rebased(base)),
2884 do_span: do_span.rebased(base),
2885 done_span: done_span.rebased(base),
2886 },
2887 ForSyntax::InDirect { in_span } => ForSyntax::InDirect {
2888 in_span: in_span.map(|span| span.rebased(base)),
2889 },
2890 ForSyntax::InBrace {
2891 in_span,
2892 left_brace_span,
2893 right_brace_span,
2894 } => ForSyntax::InBrace {
2895 in_span: in_span.map(|span| span.rebased(base)),
2896 left_brace_span: left_brace_span.rebased(base),
2897 right_brace_span: right_brace_span.rebased(base),
2898 },
2899 ForSyntax::ParenDoDone {
2900 left_paren_span,
2901 right_paren_span,
2902 do_span,
2903 done_span,
2904 } => ForSyntax::ParenDoDone {
2905 left_paren_span: left_paren_span.rebased(base),
2906 right_paren_span: right_paren_span.rebased(base),
2907 do_span: do_span.rebased(base),
2908 done_span: done_span.rebased(base),
2909 },
2910 ForSyntax::ParenDirect {
2911 left_paren_span,
2912 right_paren_span,
2913 } => ForSyntax::ParenDirect {
2914 left_paren_span: left_paren_span.rebased(base),
2915 right_paren_span: right_paren_span.rebased(base),
2916 },
2917 ForSyntax::ParenBrace {
2918 left_paren_span,
2919 right_paren_span,
2920 left_brace_span,
2921 right_brace_span,
2922 } => ForSyntax::ParenBrace {
2923 left_paren_span: left_paren_span.rebased(base),
2924 right_paren_span: right_paren_span.rebased(base),
2925 left_brace_span: left_brace_span.rebased(base),
2926 right_brace_span: right_brace_span.rebased(base),
2927 },
2928 };
2929 Self::rebase_stmt_seq(&mut command.body, base);
2930 }
2931 CompoundCommand::Repeat(command) => {
2932 command.span = command.span.rebased(base);
2933 Self::rebase_word(&mut command.count, base);
2934 command.syntax = match command.syntax {
2935 RepeatSyntax::DoDone { do_span, done_span } => RepeatSyntax::DoDone {
2936 do_span: do_span.rebased(base),
2937 done_span: done_span.rebased(base),
2938 },
2939 RepeatSyntax::Direct => RepeatSyntax::Direct,
2940 RepeatSyntax::Brace {
2941 left_brace_span,
2942 right_brace_span,
2943 } => RepeatSyntax::Brace {
2944 left_brace_span: left_brace_span.rebased(base),
2945 right_brace_span: right_brace_span.rebased(base),
2946 },
2947 };
2948 Self::rebase_stmt_seq(&mut command.body, base);
2949 }
2950 CompoundCommand::Foreach(command) => {
2951 command.span = command.span.rebased(base);
2952 command.variable_span = command.variable_span.rebased(base);
2953 Self::rebase_words(&mut command.words, base);
2954 command.syntax = match command.syntax {
2955 ForeachSyntax::ParenBrace {
2956 left_paren_span,
2957 right_paren_span,
2958 left_brace_span,
2959 right_brace_span,
2960 } => ForeachSyntax::ParenBrace {
2961 left_paren_span: left_paren_span.rebased(base),
2962 right_paren_span: right_paren_span.rebased(base),
2963 left_brace_span: left_brace_span.rebased(base),
2964 right_brace_span: right_brace_span.rebased(base),
2965 },
2966 ForeachSyntax::InDoDone {
2967 in_span,
2968 do_span,
2969 done_span,
2970 } => ForeachSyntax::InDoDone {
2971 in_span: in_span.rebased(base),
2972 do_span: do_span.rebased(base),
2973 done_span: done_span.rebased(base),
2974 },
2975 };
2976 Self::rebase_stmt_seq(&mut command.body, base);
2977 }
2978 CompoundCommand::ArithmeticFor(command) => {
2979 command.span = command.span.rebased(base);
2980 command.left_paren_span = command.left_paren_span.rebased(base);
2981 command.init_span = command.init_span.map(|span| span.rebased(base));
2982 if let Some(expr) = &mut command.init_ast {
2983 Self::rebase_arithmetic_expr(expr, base);
2984 }
2985 command.first_semicolon_span = command.first_semicolon_span.rebased(base);
2986 command.condition_span = command.condition_span.map(|span| span.rebased(base));
2987 if let Some(expr) = &mut command.condition_ast {
2988 Self::rebase_arithmetic_expr(expr, base);
2989 }
2990 command.second_semicolon_span = command.second_semicolon_span.rebased(base);
2991 command.step_span = command.step_span.map(|span| span.rebased(base));
2992 if let Some(expr) = &mut command.step_ast {
2993 Self::rebase_arithmetic_expr(expr, base);
2994 }
2995 command.right_paren_span = command.right_paren_span.rebased(base);
2996 Self::rebase_stmt_seq(&mut command.body, base);
2997 }
2998 CompoundCommand::While(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::Until(command) => {
3004 command.span = command.span.rebased(base);
3005 Self::rebase_stmt_seq(&mut command.condition, base);
3006 Self::rebase_stmt_seq(&mut command.body, base);
3007 }
3008 CompoundCommand::Case(command) => {
3009 command.span = command.span.rebased(base);
3010 Self::rebase_word(&mut command.word, base);
3011 for case in &mut command.cases {
3012 Self::rebase_patterns(&mut case.patterns, base);
3013 Self::rebase_stmt_seq(&mut case.body, base);
3014 }
3015 }
3016 CompoundCommand::Select(command) => {
3017 command.span = command.span.rebased(base);
3018 command.variable_span = command.variable_span.rebased(base);
3019 Self::rebase_words(&mut command.words, base);
3020 Self::rebase_stmt_seq(&mut command.body, base);
3021 }
3022 CompoundCommand::Subshell(commands) | CompoundCommand::BraceGroup(commands) => {
3023 Self::rebase_stmt_seq(commands, base);
3024 }
3025 CompoundCommand::Arithmetic(command) => {
3026 command.span = command.span.rebased(base);
3027 command.left_paren_span = command.left_paren_span.rebased(base);
3028 command.expr_span = command.expr_span.map(|span| span.rebased(base));
3029 if let Some(expr) = &mut command.expr_ast {
3030 Self::rebase_arithmetic_expr(expr, base);
3031 }
3032 command.right_paren_span = command.right_paren_span.rebased(base);
3033 }
3034 CompoundCommand::Time(command) => {
3035 command.span = command.span.rebased(base);
3036 if let Some(inner) = &mut command.command {
3037 Self::rebase_stmt(inner.as_mut(), base);
3038 }
3039 }
3040 CompoundCommand::Conditional(command) => {
3041 command.span = command.span.rebased(base);
3042 command.left_bracket_span = command.left_bracket_span.rebased(base);
3043 command.right_bracket_span = command.right_bracket_span.rebased(base);
3044 Self::rebase_conditional_expr(&mut command.expression, base);
3045 }
3046 CompoundCommand::Coproc(command) => {
3047 command.span = command.span.rebased(base);
3048 command.name_span = command.name_span.map(|span| span.rebased(base));
3049 Self::rebase_stmt(command.body.as_mut(), base);
3050 }
3051 CompoundCommand::Always(command) => {
3052 command.span = command.span.rebased(base);
3053 Self::rebase_stmt_seq(&mut command.body, base);
3054 Self::rebase_stmt_seq(&mut command.always_body, base);
3055 }
3056 }
3057 }
3058
3059 fn materialize_stmt_seq_source_backing(sequence: &mut StmtSeq, source: &str) {
3060 for stmt in &mut sequence.stmts {
3061 Self::materialize_stmt_source_backing(stmt, source);
3062 }
3063 }
3064
3065 fn materialize_stmt_source_backing(stmt: &mut Stmt, source: &str) {
3066 Self::materialize_ast_command_source_backing(&mut stmt.command, source);
3067 }
3068
3069 fn materialize_ast_command_source_backing(command: &mut AstCommand, source: &str) {
3070 match command {
3071 AstCommand::Simple(simple) => {
3072 Self::materialize_word_source_backing(&mut simple.name, source);
3073 }
3074 AstCommand::Builtin(_) | AstCommand::Decl(_) => {}
3075 AstCommand::Binary(binary) => {
3076 Self::materialize_stmt_source_backing(binary.left.as_mut(), source);
3077 Self::materialize_stmt_source_backing(binary.right.as_mut(), source);
3078 }
3079 AstCommand::Compound(compound) => {
3080 Self::materialize_compound_source_backing(compound, source);
3081 }
3082 AstCommand::Function(function) => {
3083 Self::materialize_stmt_source_backing(function.body.as_mut(), source);
3084 }
3085 AstCommand::AnonymousFunction(function) => {
3086 Self::materialize_stmt_source_backing(function.body.as_mut(), source);
3087 }
3088 }
3089 }
3090
3091 fn materialize_compound_source_backing(compound: &mut CompoundCommand, source: &str) {
3092 match compound {
3093 CompoundCommand::If(command) => {
3094 Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
3095 Self::materialize_stmt_seq_source_backing(&mut command.then_branch, source);
3096 for (condition, body) in &mut command.elif_branches {
3097 Self::materialize_stmt_seq_source_backing(condition, source);
3098 Self::materialize_stmt_seq_source_backing(body, source);
3099 }
3100 if let Some(else_branch) = &mut command.else_branch {
3101 Self::materialize_stmt_seq_source_backing(else_branch, source);
3102 }
3103 }
3104 CompoundCommand::For(command) => {
3105 Self::materialize_stmt_seq_source_backing(&mut command.body, source)
3106 }
3107 CompoundCommand::Repeat(command) => {
3108 Self::materialize_stmt_seq_source_backing(&mut command.body, source)
3109 }
3110 CompoundCommand::Foreach(command) => {
3111 Self::materialize_stmt_seq_source_backing(&mut command.body, source)
3112 }
3113 CompoundCommand::ArithmeticFor(command) => {
3114 Self::materialize_stmt_seq_source_backing(&mut command.body, source)
3115 }
3116 CompoundCommand::While(command) => {
3117 Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
3118 Self::materialize_stmt_seq_source_backing(&mut command.body, source);
3119 }
3120 CompoundCommand::Until(command) => {
3121 Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
3122 Self::materialize_stmt_seq_source_backing(&mut command.body, source);
3123 }
3124 CompoundCommand::Case(command) => {
3125 for case in &mut command.cases {
3126 Self::materialize_stmt_seq_source_backing(&mut case.body, source);
3127 }
3128 }
3129 CompoundCommand::Select(command) => {
3130 Self::materialize_stmt_seq_source_backing(&mut command.body, source)
3131 }
3132 CompoundCommand::Subshell(commands) | CompoundCommand::BraceGroup(commands) => {
3133 Self::materialize_stmt_seq_source_backing(commands, source);
3134 }
3135 CompoundCommand::Arithmetic(_) => {}
3136 CompoundCommand::Time(command) => {
3137 if let Some(inner) = &mut command.command {
3138 Self::materialize_stmt_source_backing(inner.as_mut(), source);
3139 }
3140 }
3141 CompoundCommand::Conditional(_) => {}
3142 CompoundCommand::Coproc(command) => {
3143 Self::materialize_stmt_source_backing(command.body.as_mut(), source);
3144 }
3145 CompoundCommand::Always(command) => {
3146 Self::materialize_stmt_seq_source_backing(&mut command.body, source);
3147 Self::materialize_stmt_seq_source_backing(&mut command.always_body, source);
3148 }
3149 }
3150 }
3151
3152 fn rebase_words(words: &mut [Word], base: Position) {
3153 for word in words {
3154 Self::rebase_word(word, base);
3155 }
3156 }
3157
3158 fn rebase_patterns(patterns: &mut [Pattern], base: Position) {
3159 for pattern in patterns {
3160 Self::rebase_pattern(pattern, base);
3161 }
3162 }
3163
3164 fn materialize_literal_text_source_backing(text: &mut LiteralText, span: Span, source: &str) {
3165 match text {
3166 LiteralText::Source => {
3167 *text = LiteralText::owned(span.slice(source).to_string());
3168 }
3169 LiteralText::CookedSource(cooked) => {
3170 *text = LiteralText::owned(cooked.to_string());
3171 }
3172 LiteralText::Owned(_) => {}
3173 }
3174 }
3175
3176 fn materialize_source_text_source_backing(text: &mut SourceText, source: &str) {
3177 if text.is_source_backed() {
3178 let span = text.span();
3179 let cooked = text.slice(source).to_string();
3180 *text = SourceText::cooked(span, cooked);
3181 }
3182 }
3183
3184 fn materialize_word_source_backing(word: &mut Word, source: &str) {
3185 for part in &mut word.parts {
3186 Self::materialize_word_part_source_backing(part, source);
3187 }
3188 }
3189
3190 fn materialize_pattern_source_backing(pattern: &mut Pattern, source: &str) {
3191 for part in &mut pattern.parts {
3192 Self::materialize_pattern_part_source_backing(part, source);
3193 }
3194 }
3195
3196 fn materialize_pattern_part_source_backing(part: &mut PatternPartNode, source: &str) {
3197 match &mut part.kind {
3198 PatternPart::Literal(text) => {
3199 Self::materialize_literal_text_source_backing(text, part.span, source);
3200 }
3201 PatternPart::CharClass(text) => {
3202 Self::materialize_source_text_source_backing(text, source);
3203 }
3204 PatternPart::Group { patterns, .. } => {
3205 for pattern in patterns {
3206 Self::materialize_pattern_source_backing(pattern, source);
3207 }
3208 }
3209 PatternPart::Word(word) => Self::materialize_word_source_backing(word, source),
3210 PatternPart::AnyString | PatternPart::AnyChar => {}
3211 }
3212 }
3213
3214 fn materialize_word_part_source_backing(part: &mut WordPartNode, source: &str) {
3215 match &mut part.kind {
3216 WordPart::Literal(text) => {
3217 Self::materialize_literal_text_source_backing(text, part.span, source);
3218 }
3219 WordPart::ZshQualifiedGlob(glob) => {
3220 Self::materialize_zsh_qualified_glob_source_backing(glob, source);
3221 }
3222 WordPart::SingleQuoted { value, .. } => {
3223 Self::materialize_source_text_source_backing(value, source);
3224 }
3225 WordPart::DoubleQuoted { parts, .. } => {
3226 for part in parts {
3227 Self::materialize_word_part_source_backing(part, source);
3228 }
3229 }
3230 WordPart::Parameter(parameter) => {
3231 Self::materialize_source_text_source_backing(&mut parameter.raw_body, source);
3232 Self::materialize_parameter_expansion_syntax_source_backing(
3233 &mut parameter.syntax,
3234 source,
3235 );
3236 }
3237 WordPart::ParameterExpansion {
3238 reference,
3239 operator,
3240 operand,
3241 operand_word_ast,
3242 ..
3243 } => {
3244 Self::materialize_var_ref_source_backing(reference, source);
3245 Self::materialize_parameter_operator_source_backing(operator, source);
3246 if let Some(operand) = operand {
3247 Self::materialize_source_text_source_backing(operand, source);
3248 }
3249 if let Some(word_ast) = operand_word_ast {
3250 Self::materialize_word_source_backing(word_ast, source);
3251 }
3252 }
3253 WordPart::ArrayAccess(reference)
3254 | WordPart::Length(reference)
3255 | WordPart::ArrayLength(reference)
3256 | WordPart::ArrayIndices(reference)
3257 | WordPart::Transformation { reference, .. } => {
3258 Self::materialize_var_ref_source_backing(reference, source);
3259 }
3260 WordPart::Substring {
3261 reference,
3262 offset,
3263 offset_ast,
3264 offset_word_ast,
3265 length,
3266 length_ast,
3267 length_word_ast,
3268 ..
3269 }
3270 | WordPart::ArraySlice {
3271 reference,
3272 offset,
3273 offset_ast,
3274 offset_word_ast,
3275 length,
3276 length_ast,
3277 length_word_ast,
3278 ..
3279 } => {
3280 Self::materialize_var_ref_source_backing(reference, source);
3281 Self::materialize_source_text_source_backing(offset, source);
3282 Self::materialize_word_source_backing(offset_word_ast, source);
3283 if let Some(expr) = offset_ast {
3284 Self::materialize_arithmetic_expr_source_backing(expr, source);
3285 }
3286 if let Some(length) = length {
3287 Self::materialize_source_text_source_backing(length, source);
3288 }
3289 if let Some(word_ast) = length_word_ast {
3290 Self::materialize_word_source_backing(word_ast, source);
3291 }
3292 if let Some(expr) = length_ast {
3293 Self::materialize_arithmetic_expr_source_backing(expr, source);
3294 }
3295 }
3296 WordPart::IndirectExpansion {
3297 reference,
3298 operator,
3299 operand,
3300 operand_word_ast,
3301 ..
3302 } => {
3303 Self::materialize_var_ref_source_backing(reference, source);
3304 if let Some(operator) = operator {
3305 Self::materialize_parameter_operator_source_backing(operator, source);
3306 }
3307 if let Some(operand) = operand {
3308 Self::materialize_source_text_source_backing(operand, source);
3309 }
3310 if let Some(word_ast) = operand_word_ast {
3311 Self::materialize_word_source_backing(word_ast, source);
3312 }
3313 }
3314 WordPart::ArithmeticExpansion {
3315 expression,
3316 expression_ast,
3317 expression_word_ast,
3318 ..
3319 } => {
3320 Self::materialize_source_text_source_backing(expression, source);
3321 Self::materialize_word_source_backing(expression_word_ast, source);
3322 if let Some(expr) = expression_ast {
3323 Self::materialize_arithmetic_expr_source_backing(expr, source);
3324 }
3325 }
3326 WordPart::CommandSubstitution { .. }
3327 | WordPart::ProcessSubstitution { .. }
3328 | WordPart::Variable(_)
3329 | WordPart::PrefixMatch { .. } => {}
3330 }
3331 }
3332
3333 fn materialize_var_ref_source_backing(reference: &mut VarRef, source: &str) {
3334 if let Some(subscript) = &mut reference.subscript {
3335 Self::materialize_subscript_source_backing(subscript, source);
3336 }
3337 }
3338
3339 fn materialize_subscript_source_backing(subscript: &mut Subscript, source: &str) {
3340 Self::materialize_source_text_source_backing(&mut subscript.text, source);
3341 if let Some(raw) = &mut subscript.raw {
3342 Self::materialize_source_text_source_backing(raw, source);
3343 }
3344 if let Some(word_ast) = &mut subscript.word_ast {
3345 Self::materialize_word_source_backing(word_ast, source);
3346 }
3347 if let Some(expr) = &mut subscript.arithmetic_ast {
3348 Self::materialize_arithmetic_expr_source_backing(expr, source);
3349 }
3350 }
3351
3352 fn materialize_zsh_qualified_glob_source_backing(glob: &mut ZshQualifiedGlob, source: &str) {
3353 for segment in &mut glob.segments {
3354 match segment {
3355 ZshGlobSegment::Pattern(pattern) => {
3356 Self::materialize_pattern_source_backing(pattern, source);
3357 }
3358 ZshGlobSegment::InlineControl(_) => {}
3359 }
3360 }
3361 if let Some(qualifiers) = &mut glob.qualifiers {
3362 for fragment in &mut qualifiers.fragments {
3363 match fragment {
3364 ZshGlobQualifier::LetterSequence { text, .. } => {
3365 Self::materialize_source_text_source_backing(text, source);
3366 }
3367 ZshGlobQualifier::NumericArgument { start, end, .. } => {
3368 Self::materialize_source_text_source_backing(start, source);
3369 if let Some(end) = end {
3370 Self::materialize_source_text_source_backing(end, source);
3371 }
3372 }
3373 ZshGlobQualifier::Negation { .. } | ZshGlobQualifier::Flag { .. } => {}
3374 }
3375 }
3376 }
3377 }
3378
3379 fn materialize_parameter_expansion_syntax_source_backing(
3380 syntax: &mut ParameterExpansionSyntax,
3381 source: &str,
3382 ) {
3383 match syntax {
3384 ParameterExpansionSyntax::Bourne(syntax) => match syntax {
3385 BourneParameterExpansion::Access { reference }
3386 | BourneParameterExpansion::Length { reference }
3387 | BourneParameterExpansion::Indices { reference }
3388 | BourneParameterExpansion::Transformation { reference, .. } => {
3389 Self::materialize_var_ref_source_backing(reference, source);
3390 }
3391 BourneParameterExpansion::Indirect {
3392 reference,
3393 operator,
3394 operand,
3395 operand_word_ast,
3396 ..
3397 } => {
3398 Self::materialize_var_ref_source_backing(reference, source);
3399 if let Some(operator) = operator {
3400 Self::materialize_parameter_operator_source_backing(operator, source);
3401 }
3402 if let Some(operand) = operand {
3403 Self::materialize_source_text_source_backing(operand, source);
3404 }
3405 if let Some(word_ast) = operand_word_ast {
3406 Self::materialize_word_source_backing(word_ast, source);
3407 }
3408 }
3409 BourneParameterExpansion::PrefixMatch { .. } => {}
3410 BourneParameterExpansion::Slice {
3411 reference,
3412 offset,
3413 offset_ast,
3414 offset_word_ast,
3415 length,
3416 length_ast,
3417 length_word_ast,
3418 } => {
3419 Self::materialize_var_ref_source_backing(reference, source);
3420 Self::materialize_source_text_source_backing(offset, source);
3421 Self::materialize_word_source_backing(offset_word_ast, source);
3422 if let Some(expr) = offset_ast {
3423 Self::materialize_arithmetic_expr_source_backing(expr, source);
3424 }
3425 if let Some(length) = length {
3426 Self::materialize_source_text_source_backing(length, source);
3427 }
3428 if let Some(word_ast) = length_word_ast {
3429 Self::materialize_word_source_backing(word_ast, source);
3430 }
3431 if let Some(expr) = length_ast {
3432 Self::materialize_arithmetic_expr_source_backing(expr, source);
3433 }
3434 }
3435 BourneParameterExpansion::Operation {
3436 reference,
3437 operator,
3438 operand,
3439 operand_word_ast,
3440 ..
3441 } => {
3442 Self::materialize_var_ref_source_backing(reference, source);
3443 Self::materialize_parameter_operator_source_backing(operator, source);
3444 if let Some(operand) = operand {
3445 Self::materialize_source_text_source_backing(operand, source);
3446 }
3447 if let Some(word_ast) = operand_word_ast {
3448 Self::materialize_word_source_backing(word_ast, source);
3449 }
3450 }
3451 },
3452 ParameterExpansionSyntax::Zsh(syntax) => {
3453 match &mut syntax.target {
3454 ZshExpansionTarget::Reference(reference) => {
3455 Self::materialize_var_ref_source_backing(reference, source);
3456 }
3457 ZshExpansionTarget::Word(word) => {
3458 Self::materialize_word_source_backing(word, source);
3459 }
3460 ZshExpansionTarget::Nested(parameter) => {
3461 Self::materialize_source_text_source_backing(
3462 &mut parameter.raw_body,
3463 source,
3464 );
3465 Self::materialize_parameter_expansion_syntax_source_backing(
3466 &mut parameter.syntax,
3467 source,
3468 );
3469 }
3470 ZshExpansionTarget::Empty => {}
3471 }
3472 for modifier in &mut syntax.modifiers {
3473 if let Some(argument) = &mut modifier.argument {
3474 Self::materialize_source_text_source_backing(argument, source);
3475 }
3476 if let Some(argument_word_ast) = &mut modifier.argument_word_ast {
3477 Self::materialize_word_source_backing(argument_word_ast, source);
3478 }
3479 }
3480 if let Some(operation) = &mut syntax.operation {
3481 match operation {
3482 ZshExpansionOperation::PatternOperation {
3483 operand,
3484 operand_word_ast,
3485 ..
3486 }
3487 | ZshExpansionOperation::Defaulting {
3488 operand,
3489 operand_word_ast,
3490 ..
3491 }
3492 | ZshExpansionOperation::TrimOperation {
3493 operand,
3494 operand_word_ast,
3495 ..
3496 } => {
3497 Self::materialize_source_text_source_backing(operand, source);
3498 Self::materialize_word_source_backing(operand_word_ast, source);
3499 }
3500 ZshExpansionOperation::Unknown { text, word_ast } => {
3501 Self::materialize_source_text_source_backing(text, source);
3502 Self::materialize_word_source_backing(word_ast, source);
3503 }
3504 ZshExpansionOperation::ReplacementOperation {
3505 pattern,
3506 pattern_word_ast,
3507 replacement,
3508 replacement_word_ast,
3509 ..
3510 } => {
3511 Self::materialize_source_text_source_backing(pattern, source);
3512 Self::materialize_word_source_backing(pattern_word_ast, source);
3513 if let Some(replacement) = replacement {
3514 Self::materialize_source_text_source_backing(replacement, source);
3515 }
3516 if let Some(replacement_word_ast) = replacement_word_ast {
3517 Self::materialize_word_source_backing(replacement_word_ast, source);
3518 }
3519 }
3520 ZshExpansionOperation::Slice {
3521 offset,
3522 offset_word_ast,
3523 length,
3524 length_word_ast,
3525 } => {
3526 Self::materialize_source_text_source_backing(offset, source);
3527 Self::materialize_word_source_backing(offset_word_ast, source);
3528 if let Some(length) = length {
3529 Self::materialize_source_text_source_backing(length, source);
3530 }
3531 if let Some(length_word_ast) = length_word_ast {
3532 Self::materialize_word_source_backing(length_word_ast, source);
3533 }
3534 }
3535 }
3536 }
3537 }
3538 }
3539 }
3540
3541 fn materialize_parameter_operator_source_backing(operator: &mut ParameterOp, source: &str) {
3542 match operator {
3543 ParameterOp::RemovePrefixShort { pattern }
3544 | ParameterOp::RemovePrefixLong { pattern }
3545 | ParameterOp::RemoveSuffixShort { pattern }
3546 | ParameterOp::RemoveSuffixLong { pattern } => {
3547 Self::materialize_pattern_source_backing(pattern, source);
3548 }
3549 ParameterOp::ReplaceFirst {
3550 pattern,
3551 replacement,
3552 replacement_word_ast,
3553 }
3554 | ParameterOp::ReplaceAll {
3555 pattern,
3556 replacement,
3557 replacement_word_ast,
3558 } => {
3559 Self::materialize_pattern_source_backing(pattern, source);
3560 Self::materialize_source_text_source_backing(replacement, source);
3561 Self::materialize_word_source_backing(replacement_word_ast, source);
3562 }
3563 ParameterOp::UseDefault
3564 | ParameterOp::AssignDefault
3565 | ParameterOp::UseReplacement
3566 | ParameterOp::Error
3567 | ParameterOp::UpperFirst
3568 | ParameterOp::UpperAll
3569 | ParameterOp::LowerFirst
3570 | ParameterOp::LowerAll => {}
3571 }
3572 }
3573
3574 fn materialize_arithmetic_expr_source_backing(expr: &mut ArithmeticExprNode, source: &str) {
3575 match &mut expr.kind {
3576 ArithmeticExpr::Number(text) => {
3577 Self::materialize_source_text_source_backing(text, source);
3578 }
3579 ArithmeticExpr::Variable(_) => {}
3580 ArithmeticExpr::Indexed { index, .. } => {
3581 Self::materialize_arithmetic_expr_source_backing(index, source);
3582 }
3583 ArithmeticExpr::ShellWord(word) => {
3584 Self::materialize_word_source_backing(word, source);
3585 }
3586 ArithmeticExpr::Parenthesized { expression } => {
3587 Self::materialize_arithmetic_expr_source_backing(expression, source);
3588 }
3589 ArithmeticExpr::Unary { expr, .. } | ArithmeticExpr::Postfix { expr, .. } => {
3590 Self::materialize_arithmetic_expr_source_backing(expr, source);
3591 }
3592 ArithmeticExpr::Binary { left, right, .. } => {
3593 Self::materialize_arithmetic_expr_source_backing(left, source);
3594 Self::materialize_arithmetic_expr_source_backing(right, source);
3595 }
3596 ArithmeticExpr::Conditional {
3597 condition,
3598 then_expr,
3599 else_expr,
3600 } => {
3601 Self::materialize_arithmetic_expr_source_backing(condition, source);
3602 Self::materialize_arithmetic_expr_source_backing(then_expr, source);
3603 Self::materialize_arithmetic_expr_source_backing(else_expr, source);
3604 }
3605 ArithmeticExpr::Assignment { target, value, .. } => {
3606 Self::materialize_arithmetic_lvalue_source_backing(target, source);
3607 Self::materialize_arithmetic_expr_source_backing(value, source);
3608 }
3609 }
3610 }
3611
3612 fn materialize_arithmetic_lvalue_source_backing(target: &mut ArithmeticLvalue, source: &str) {
3613 match target {
3614 ArithmeticLvalue::Variable(_) => {}
3615 ArithmeticLvalue::Indexed { index, .. } => {
3616 Self::materialize_arithmetic_expr_source_backing(index, source);
3617 }
3618 }
3619 }
3620
3621 fn rebase_word(word: &mut Word, base: Position) {
3622 word.span = word.span.rebased(base);
3623 for brace in &mut word.brace_syntax {
3624 brace.span = brace.span.rebased(base);
3625 }
3626 Self::rebase_word_parts(&mut word.parts, base);
3627 }
3628
3629 fn rebase_heredoc_body(body: &mut HeredocBody, base: Position) {
3630 body.span = body.span.rebased(base);
3631 for part in &mut body.parts {
3632 Self::rebase_heredoc_body_part(part, base);
3633 }
3634 }
3635
3636 fn rebase_pattern(pattern: &mut Pattern, base: Position) {
3637 pattern.span = pattern.span.rebased(base);
3638 Self::rebase_pattern_parts(&mut pattern.parts, base);
3639 }
3640
3641 fn rebase_word_parts(parts: &mut [WordPartNode], base: Position) {
3642 for part in parts {
3643 Self::rebase_word_part(part, base);
3644 }
3645 }
3646
3647 fn rebase_pattern_parts(parts: &mut [PatternPartNode], base: Position) {
3648 for part in parts {
3649 part.span = part.span.rebased(base);
3650 match &mut part.kind {
3651 PatternPart::CharClass(text) => text.rebased(base),
3652 PatternPart::Group { patterns, .. } => Self::rebase_patterns(patterns, base),
3653 PatternPart::Word(word) => Self::rebase_word(word, base),
3654 PatternPart::Literal(_) | PatternPart::AnyString | PatternPart::AnyChar => {}
3655 }
3656 }
3657 }
3658
3659 fn rebase_heredoc_body_part(part: &mut HeredocBodyPartNode, base: Position) {
3660 part.span = part.span.rebased(base);
3661 match &mut part.kind {
3662 HeredocBodyPart::Literal(_) | HeredocBodyPart::Variable(_) => {}
3663 HeredocBodyPart::CommandSubstitution { body, .. } => Self::rebase_stmt_seq(body, base),
3664 HeredocBodyPart::ArithmeticExpansion {
3665 expression,
3666 expression_ast,
3667 expression_word_ast,
3668 ..
3669 } => {
3670 expression.rebased(base);
3671 Self::rebase_word(expression_word_ast, base);
3672 if let Some(expr) = expression_ast {
3673 Self::rebase_arithmetic_expr(expr, base);
3674 }
3675 }
3676 HeredocBodyPart::Parameter(parameter) => {
3677 parameter.span = parameter.span.rebased(base);
3678 parameter.raw_body.rebased(base);
3679 Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
3680 }
3681 }
3682 }
3683
3684 fn rebase_word_part(part: &mut WordPartNode, base: Position) {
3685 part.span = part.span.rebased(base);
3686 match &mut part.kind {
3687 WordPart::ZshQualifiedGlob(glob) => Self::rebase_zsh_qualified_glob(glob, base),
3688 WordPart::SingleQuoted { value, .. } => value.rebased(base),
3689 WordPart::DoubleQuoted { parts, .. } => Self::rebase_word_parts(parts, base),
3690 WordPart::Parameter(parameter) => {
3691 parameter.span = parameter.span.rebased(base);
3692 parameter.raw_body.rebased(base);
3693 Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
3694 }
3695 WordPart::ParameterExpansion {
3696 reference,
3697 operator,
3698 operand,
3699 operand_word_ast,
3700 ..
3701 } => {
3702 Self::rebase_var_ref(reference, base);
3703 match operator {
3704 ParameterOp::RemovePrefixShort { pattern }
3705 | ParameterOp::RemovePrefixLong { pattern }
3706 | ParameterOp::RemoveSuffixShort { pattern }
3707 | ParameterOp::RemoveSuffixLong { pattern } => {
3708 Self::rebase_pattern(pattern, base);
3709 }
3710 ParameterOp::ReplaceFirst {
3711 pattern,
3712 replacement,
3713 ..
3714 }
3715 | ParameterOp::ReplaceAll {
3716 pattern,
3717 replacement,
3718 ..
3719 } => {
3720 Self::rebase_pattern(pattern, base);
3721 replacement.rebased(base);
3722 }
3723 ParameterOp::UseDefault
3724 | ParameterOp::AssignDefault
3725 | ParameterOp::UseReplacement
3726 | ParameterOp::Error
3727 | ParameterOp::UpperFirst
3728 | ParameterOp::UpperAll
3729 | ParameterOp::LowerFirst
3730 | ParameterOp::LowerAll => {}
3731 }
3732 if let Some(operand) = operand {
3733 operand.rebased(base);
3734 }
3735 if let Some(word_ast) = operand_word_ast {
3736 Self::rebase_word(word_ast, base);
3737 }
3738 }
3739 WordPart::ArrayAccess(reference)
3740 | WordPart::Length(reference)
3741 | WordPart::ArrayLength(reference)
3742 | WordPart::ArrayIndices(reference)
3743 | WordPart::Transformation { reference, .. } => Self::rebase_var_ref(reference, base),
3744 WordPart::Substring {
3745 reference,
3746 offset,
3747 offset_ast,
3748 offset_word_ast,
3749 length,
3750 length_ast,
3751 length_word_ast,
3752 ..
3753 }
3754 | WordPart::ArraySlice {
3755 reference,
3756 offset,
3757 offset_ast,
3758 offset_word_ast,
3759 length,
3760 length_ast,
3761 length_word_ast,
3762 ..
3763 } => {
3764 Self::rebase_var_ref(reference, base);
3765 offset.rebased(base);
3766 Self::rebase_word(offset_word_ast, base);
3767 if let Some(expr) = offset_ast {
3768 Self::rebase_arithmetic_expr(expr, base);
3769 }
3770 if let Some(length) = length {
3771 length.rebased(base);
3772 }
3773 if let Some(word_ast) = length_word_ast {
3774 Self::rebase_word(word_ast, base);
3775 }
3776 if let Some(expr) = length_ast {
3777 Self::rebase_arithmetic_expr(expr, base);
3778 }
3779 }
3780 WordPart::IndirectExpansion {
3781 reference,
3782 operator,
3783 operand,
3784 operand_word_ast,
3785 ..
3786 } => {
3787 Self::rebase_var_ref(reference, base);
3788 if let Some(operator) = operator {
3789 Self::rebase_parameter_operator(operator, base);
3790 }
3791 if let Some(operand) = operand {
3792 operand.rebased(base);
3793 }
3794 if let Some(word_ast) = operand_word_ast {
3795 Self::rebase_word(word_ast, base);
3796 }
3797 }
3798 WordPart::ArithmeticExpansion {
3799 expression,
3800 expression_ast,
3801 expression_word_ast,
3802 ..
3803 } => {
3804 expression.rebased(base);
3805 Self::rebase_word(expression_word_ast, base);
3806 if let Some(expr) = expression_ast {
3807 Self::rebase_arithmetic_expr(expr, base);
3808 }
3809 }
3810 WordPart::CommandSubstitution { body, .. }
3811 | WordPart::ProcessSubstitution { body, .. } => Self::rebase_stmt_seq(body, base),
3812 WordPart::Literal(_) | WordPart::Variable(_) | WordPart::PrefixMatch { .. } => {}
3813 }
3814 }
3815
3816 fn rebase_zsh_qualified_glob(glob: &mut ZshQualifiedGlob, base: Position) {
3817 glob.span = glob.span.rebased(base);
3818 for segment in &mut glob.segments {
3819 Self::rebase_zsh_glob_segment(segment, base);
3820 }
3821 if let Some(qualifiers) = &mut glob.qualifiers {
3822 Self::rebase_zsh_glob_qualifier_group(qualifiers, base);
3823 }
3824 }
3825
3826 fn rebase_zsh_glob_segment(segment: &mut ZshGlobSegment, base: Position) {
3827 match segment {
3828 ZshGlobSegment::Pattern(pattern) => Self::rebase_pattern(pattern, base),
3829 ZshGlobSegment::InlineControl(control) => {
3830 Self::rebase_zsh_inline_glob_control(control, base)
3831 }
3832 }
3833 }
3834
3835 fn rebase_zsh_inline_glob_control(control: &mut ZshInlineGlobControl, base: Position) {
3836 match control {
3837 ZshInlineGlobControl::CaseInsensitive { span }
3838 | ZshInlineGlobControl::Backreferences { span }
3839 | ZshInlineGlobControl::StartAnchor { span }
3840 | ZshInlineGlobControl::EndAnchor { span } => {
3841 *span = span.rebased(base);
3842 }
3843 }
3844 }
3845
3846 fn rebase_zsh_glob_qualifier_group(group: &mut ZshGlobQualifierGroup, base: Position) {
3847 group.span = group.span.rebased(base);
3848 for fragment in &mut group.fragments {
3849 match fragment {
3850 ZshGlobQualifier::Negation { span } | ZshGlobQualifier::Flag { span, .. } => {
3851 *span = span.rebased(base);
3852 }
3853 ZshGlobQualifier::LetterSequence { text, span } => {
3854 *span = span.rebased(base);
3855 text.rebased(base);
3856 }
3857 ZshGlobQualifier::NumericArgument { span, start, end } => {
3858 *span = span.rebased(base);
3859 start.rebased(base);
3860 if let Some(end) = end {
3861 end.rebased(base);
3862 }
3863 }
3864 }
3865 }
3866 }
3867
3868 fn rebase_parameter_expansion_syntax(syntax: &mut ParameterExpansionSyntax, base: Position) {
3869 match syntax {
3870 ParameterExpansionSyntax::Bourne(syntax) => match syntax {
3871 BourneParameterExpansion::Access { reference }
3872 | BourneParameterExpansion::Length { reference }
3873 | BourneParameterExpansion::Indices { reference }
3874 | BourneParameterExpansion::Transformation { reference, .. } => {
3875 Self::rebase_var_ref(reference, base);
3876 }
3877 BourneParameterExpansion::Indirect {
3878 reference,
3879 operand,
3880 operator,
3881 operand_word_ast,
3882 ..
3883 } => {
3884 Self::rebase_var_ref(reference, base);
3885 if let Some(operator) = operator {
3886 Self::rebase_parameter_operator(operator, base);
3887 }
3888 if let Some(operand) = operand {
3889 operand.rebased(base);
3890 }
3891 if let Some(word_ast) = operand_word_ast {
3892 Self::rebase_word(word_ast, base);
3893 }
3894 }
3895 BourneParameterExpansion::PrefixMatch { .. } => {}
3896 BourneParameterExpansion::Slice {
3897 reference,
3898 offset,
3899 offset_ast,
3900 offset_word_ast,
3901 length,
3902 length_ast,
3903 length_word_ast,
3904 } => {
3905 Self::rebase_var_ref(reference, base);
3906 offset.rebased(base);
3907 Self::rebase_word(offset_word_ast, base);
3908 if let Some(expr) = offset_ast {
3909 Self::rebase_arithmetic_expr(expr, base);
3910 }
3911 if let Some(length) = length {
3912 length.rebased(base);
3913 }
3914 if let Some(word_ast) = length_word_ast {
3915 Self::rebase_word(word_ast, base);
3916 }
3917 if let Some(expr) = length_ast {
3918 Self::rebase_arithmetic_expr(expr, base);
3919 }
3920 }
3921 BourneParameterExpansion::Operation {
3922 reference,
3923 operator,
3924 operand,
3925 operand_word_ast,
3926 ..
3927 } => {
3928 Self::rebase_var_ref(reference, base);
3929 Self::rebase_parameter_operator(operator, base);
3930 if let Some(operand) = operand {
3931 operand.rebased(base);
3932 }
3933 if let Some(word_ast) = operand_word_ast {
3934 Self::rebase_word(word_ast, base);
3935 }
3936 }
3937 },
3938 ParameterExpansionSyntax::Zsh(syntax) => {
3939 match &mut syntax.target {
3940 ZshExpansionTarget::Reference(reference) => {
3941 Self::rebase_var_ref(reference, base)
3942 }
3943 ZshExpansionTarget::Word(word) => Self::rebase_word(word, base),
3944 ZshExpansionTarget::Nested(parameter) => {
3945 parameter.span = parameter.span.rebased(base);
3946 parameter.raw_body.rebased(base);
3947 Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
3948 }
3949 ZshExpansionTarget::Empty => {}
3950 }
3951 for modifier in &mut syntax.modifiers {
3952 modifier.span = modifier.span.rebased(base);
3953 if let Some(argument) = &mut modifier.argument {
3954 argument.rebased(base);
3955 }
3956 if let Some(argument_word_ast) = &mut modifier.argument_word_ast {
3957 Self::rebase_word(argument_word_ast, base);
3958 }
3959 }
3960 if let Some(length_prefix) = &mut syntax.length_prefix {
3961 *length_prefix = length_prefix.rebased(base);
3962 }
3963 if let Some(operation) = &mut syntax.operation {
3964 match operation {
3965 ZshExpansionOperation::PatternOperation {
3966 operand,
3967 operand_word_ast,
3968 ..
3969 }
3970 | ZshExpansionOperation::Defaulting {
3971 operand,
3972 operand_word_ast,
3973 ..
3974 }
3975 | ZshExpansionOperation::TrimOperation {
3976 operand,
3977 operand_word_ast,
3978 ..
3979 } => {
3980 operand.rebased(base);
3981 Self::rebase_word(operand_word_ast, base);
3982 }
3983 ZshExpansionOperation::Unknown { text, word_ast } => {
3984 text.rebased(base);
3985 Self::rebase_word(word_ast, base);
3986 }
3987 ZshExpansionOperation::ReplacementOperation {
3988 pattern,
3989 pattern_word_ast,
3990 replacement,
3991 replacement_word_ast,
3992 ..
3993 } => {
3994 pattern.rebased(base);
3995 Self::rebase_word(pattern_word_ast, base);
3996 if let Some(replacement) = replacement {
3997 replacement.rebased(base);
3998 }
3999 if let Some(replacement_word_ast) = replacement_word_ast {
4000 Self::rebase_word(replacement_word_ast, base);
4001 }
4002 }
4003 ZshExpansionOperation::Slice {
4004 offset,
4005 offset_word_ast,
4006 length,
4007 length_word_ast,
4008 } => {
4009 offset.rebased(base);
4010 Self::rebase_word(offset_word_ast, base);
4011 if let Some(length) = length {
4012 length.rebased(base);
4013 }
4014 if let Some(length_word_ast) = length_word_ast {
4015 Self::rebase_word(length_word_ast, base);
4016 }
4017 }
4018 }
4019 }
4020 }
4021 }
4022 }
4023
4024 fn rebase_parameter_operator(operator: &mut ParameterOp, base: Position) {
4025 match operator {
4026 ParameterOp::RemovePrefixShort { pattern }
4027 | ParameterOp::RemovePrefixLong { pattern }
4028 | ParameterOp::RemoveSuffixShort { pattern }
4029 | ParameterOp::RemoveSuffixLong { pattern } => {
4030 Self::rebase_pattern(pattern, base);
4031 }
4032 ParameterOp::ReplaceFirst {
4033 pattern,
4034 replacement,
4035 replacement_word_ast,
4036 }
4037 | ParameterOp::ReplaceAll {
4038 pattern,
4039 replacement,
4040 replacement_word_ast,
4041 } => {
4042 Self::rebase_pattern(pattern, base);
4043 replacement.rebased(base);
4044 Self::rebase_word(replacement_word_ast, base);
4045 }
4046 ParameterOp::UseDefault
4047 | ParameterOp::AssignDefault
4048 | ParameterOp::UseReplacement
4049 | ParameterOp::Error
4050 | ParameterOp::UpperFirst
4051 | ParameterOp::UpperAll
4052 | ParameterOp::LowerFirst
4053 | ParameterOp::LowerAll => {}
4054 }
4055 }
4056
4057 fn rebase_conditional_expr(expr: &mut ConditionalExpr, base: Position) {
4058 match expr {
4059 ConditionalExpr::Binary(binary) => {
4060 binary.op_span = binary.op_span.rebased(base);
4061 Self::rebase_conditional_expr(&mut binary.left, base);
4062 Self::rebase_conditional_expr(&mut binary.right, base);
4063 }
4064 ConditionalExpr::Unary(unary) => {
4065 unary.op_span = unary.op_span.rebased(base);
4066 Self::rebase_conditional_expr(&mut unary.expr, base);
4067 }
4068 ConditionalExpr::Parenthesized(paren) => {
4069 paren.left_paren_span = paren.left_paren_span.rebased(base);
4070 paren.right_paren_span = paren.right_paren_span.rebased(base);
4071 Self::rebase_conditional_expr(&mut paren.expr, base);
4072 }
4073 ConditionalExpr::Word(word) | ConditionalExpr::Regex(word) => {
4074 Self::rebase_word(word, base);
4075 }
4076 ConditionalExpr::Pattern(pattern) => Self::rebase_pattern(pattern, base),
4077 ConditionalExpr::VarRef(var_ref) => Self::rebase_var_ref(var_ref, base),
4078 }
4079 }
4080
4081 fn rebase_arithmetic_expr(expr: &mut ArithmeticExprNode, base: Position) {
4082 expr.span = expr.span.rebased(base);
4083 match &mut expr.kind {
4084 ArithmeticExpr::Number(text) => text.rebased(base),
4085 ArithmeticExpr::Variable(_) => {}
4086 ArithmeticExpr::Indexed { index, .. } => Self::rebase_arithmetic_expr(index, base),
4087 ArithmeticExpr::ShellWord(word) => Self::rebase_word(word, base),
4088 ArithmeticExpr::Parenthesized { expression } => {
4089 Self::rebase_arithmetic_expr(expression, base)
4090 }
4091 ArithmeticExpr::Unary { expr, .. } | ArithmeticExpr::Postfix { expr, .. } => {
4092 Self::rebase_arithmetic_expr(expr, base)
4093 }
4094 ArithmeticExpr::Binary { left, right, .. } => {
4095 Self::rebase_arithmetic_expr(left, base);
4096 Self::rebase_arithmetic_expr(right, base);
4097 }
4098 ArithmeticExpr::Conditional {
4099 condition,
4100 then_expr,
4101 else_expr,
4102 } => {
4103 Self::rebase_arithmetic_expr(condition, base);
4104 Self::rebase_arithmetic_expr(then_expr, base);
4105 Self::rebase_arithmetic_expr(else_expr, base);
4106 }
4107 ArithmeticExpr::Assignment { target, value, .. } => {
4108 Self::rebase_arithmetic_lvalue(target, base);
4109 Self::rebase_arithmetic_expr(value, base);
4110 }
4111 }
4112 }
4113
4114 fn rebase_arithmetic_lvalue(target: &mut ArithmeticLvalue, base: Position) {
4115 match target {
4116 ArithmeticLvalue::Variable(_) => {}
4117 ArithmeticLvalue::Indexed { index, .. } => Self::rebase_arithmetic_expr(index, base),
4118 }
4119 }
4120
4121 fn push_word_part(parts: &mut WordPartBuffer, part: WordPart, start: Position, end: Position) {
4122 Self::push_word_part_node(
4123 parts,
4124 WordPartNode::new(part, Span::from_positions(start, end)),
4125 );
4126 }
4127
4128 fn push_word_part_node(parts: &mut WordPartBuffer, part: WordPartNode) {
4129 parts.push(part);
4130 }
4131
4132 fn flush_literal_part(
4133 &self,
4134 parts: &mut WordPartBuffer,
4135 current: &mut String,
4136 current_start: Position,
4137 end: Position,
4138 source_backed: bool,
4139 ) {
4140 if !current.is_empty() {
4141 Self::push_word_part(
4142 parts,
4143 WordPart::Literal(self.literal_text(
4144 std::mem::take(current),
4145 current_start,
4146 end,
4147 source_backed,
4148 )),
4149 current_start,
4150 end,
4151 );
4152 }
4153 }
4154
4155 fn word_part_buffer_with_capacity(capacity: usize) -> WordPartBuffer {
4156 if capacity <= 2 {
4157 WordPartBuffer::new()
4158 } else {
4159 WordPartBuffer::with_capacity(capacity)
4160 }
4161 }
4162
4163 fn literal_text(
4164 &self,
4165 text: String,
4166 start: Position,
4167 end: Position,
4168 source_backed: bool,
4169 ) -> LiteralText {
4170 let span = Span::from_positions(start, end);
4171 if self.source_matches(span, &text) {
4172 LiteralText::source()
4173 } else if source_backed {
4174 LiteralText::cooked_source(text)
4175 } else {
4176 LiteralText::owned(text)
4177 }
4178 }
4179
4180 fn literal_text_from_str(
4181 &self,
4182 text: &str,
4183 start: Position,
4184 end: Position,
4185 source_backed: bool,
4186 ) -> LiteralText {
4187 self.literal_text_impl(text, None, start, end, source_backed)
4188 }
4189
4190 fn literal_text_impl(
4191 &self,
4192 text: &str,
4193 owned: Option<String>,
4194 start: Position,
4195 end: Position,
4196 source_backed: bool,
4197 ) -> LiteralText {
4198 let span = Span::from_positions(start, end);
4199 if self.source_matches(span, text) {
4200 LiteralText::source()
4201 } else if source_backed {
4202 LiteralText::cooked_source(owned.unwrap_or_else(|| text.to_owned()))
4203 } else {
4204 LiteralText::owned(owned.unwrap_or_else(|| text.to_owned()))
4205 }
4206 }
4207
4208 fn source_text(&self, text: String, start: Position, end: Position) -> SourceText {
4209 let span = Span::from_positions(start, end);
4210 if self.source_matches(span, &text) {
4211 SourceText::source(span)
4212 } else {
4213 SourceText::cooked(span, text)
4214 }
4215 }
4216
4217 fn source_text_from_str(&self, text: &str, start: Position, end: Position) -> SourceText {
4218 self.source_text_impl(text, None, start, end)
4219 }
4220
4221 fn source_text_impl(
4222 &self,
4223 text: &str,
4224 owned: Option<String>,
4225 start: Position,
4226 end: Position,
4227 ) -> SourceText {
4228 let span = Span::from_positions(start, end);
4229 if self.source_matches(span, text) {
4230 SourceText::source(span)
4231 } else {
4232 SourceText::cooked(span, owned.unwrap_or_else(|| text.to_owned()))
4233 }
4234 }
4235
4236 fn empty_source_text(&self, pos: Position) -> SourceText {
4237 SourceText::source(Span::from_positions(pos, pos))
4238 }
4239
4240 fn input_prefix_ends_with(&self, end_offset: usize, ch: char) -> bool {
4241 self.input
4242 .get(..end_offset)
4243 .is_some_and(|prefix| prefix.ends_with(ch))
4244 }
4245
4246 fn input_span_ends_with(&self, start: Position, end: Position, ch: char) -> bool {
4247 self.input
4248 .get(start.offset..end.offset)
4249 .is_some_and(|slice| slice.ends_with(ch))
4250 }
4251
4252 fn input_suffix_starts_with(&self, start_offset: usize, ch: char) -> bool {
4253 self.input
4254 .get(start_offset..)
4255 .is_some_and(|suffix| suffix.starts_with(ch))
4256 }
4257
4258 fn subscript_source_text(&self, raw: &str, span: Span) -> (SourceText, Option<SourceText>) {
4259 if raw.len() >= 2
4260 && ((raw.starts_with('"') && raw.ends_with('"'))
4261 || (raw.starts_with('\'') && raw.ends_with('\'')))
4262 {
4263 let raw_text = raw.to_string();
4264 let raw = if self.source_matches(span, raw) {
4265 SourceText::source(span)
4266 } else {
4267 SourceText::cooked(span, raw_text.clone())
4268 };
4269 let cooked = raw_text[1..raw_text.len() - 1].to_string();
4270 return (self.source_text(cooked, span.start, span.end), Some(raw));
4271 }
4272
4273 let text = if self.source_matches(span, raw) {
4274 SourceText::source(span)
4275 } else {
4276 SourceText::cooked(span, raw.to_string())
4277 };
4278 (text, None)
4279 }
4280
4281 fn subscript_from_source_text(
4282 &self,
4283 text: SourceText,
4284 raw: Option<SourceText>,
4285 interpretation: SubscriptInterpretation,
4286 ) -> Subscript {
4287 let kind = match text.slice(self.input).trim() {
4288 "@" => SubscriptKind::Selector(SubscriptSelector::At),
4289 "*" => SubscriptKind::Selector(SubscriptSelector::Star),
4290 _ => SubscriptKind::Ordinary,
4291 };
4292 let word_ast = if matches!(kind, SubscriptKind::Ordinary) {
4293 Some(self.parse_source_text_as_word(raw.as_ref().unwrap_or(&text)))
4294 } else {
4295 None
4296 };
4297 let arithmetic_ast = if matches!(kind, SubscriptKind::Ordinary) {
4298 self.simple_subscript_arithmetic_ast(&text)
4299 .or_else(|| self.maybe_parse_source_text_as_arithmetic(&text))
4300 } else {
4301 None
4302 };
4303 Subscript {
4304 text,
4305 raw,
4306 kind,
4307 interpretation,
4308 word_ast,
4309 arithmetic_ast,
4310 }
4311 }
4312
4313 fn simple_subscript_arithmetic_ast(&self, text: &SourceText) -> Option<ArithmeticExprNode> {
4314 if !text.is_source_backed() {
4315 return None;
4316 }
4317
4318 let raw = text.slice(self.input);
4319 if raw.is_empty() || raw.trim() != raw {
4320 return None;
4321 }
4322
4323 let span = text.span();
4324 if raw.bytes().all(|byte| byte.is_ascii_digit()) {
4325 return Some(ArithmeticExprNode::new(
4326 ArithmeticExpr::Number(SourceText::source(span)),
4327 span,
4328 ));
4329 }
4330
4331 if Self::is_valid_identifier(raw) {
4332 return Some(ArithmeticExprNode::new(
4333 ArithmeticExpr::Variable(Name::from(raw)),
4334 span,
4335 ));
4336 }
4337
4338 None
4339 }
4340
4341 fn subscript_from_text(
4342 &self,
4343 raw: &str,
4344 span: Span,
4345 interpretation: SubscriptInterpretation,
4346 ) -> Subscript {
4347 let (text, raw) = self.subscript_source_text(raw, span);
4348 self.subscript_from_source_text(text, raw, interpretation)
4349 }
4350
4351 fn var_ref(
4352 &self,
4353 name: impl Into<Name>,
4354 name_span: Span,
4355 subscript: Option<Subscript>,
4356 span: Span,
4357 ) -> VarRef {
4358 VarRef {
4359 name: name.into(),
4360 name_span,
4361 subscript: subscript.map(Box::new),
4362 span,
4363 }
4364 }
4365
4366 fn parameter_var_ref(
4367 &self,
4368 part_start: Position,
4369 prefix: &str,
4370 name: &str,
4371 subscript: Option<Subscript>,
4372 part_end: Position,
4373 ) -> VarRef {
4374 let name_start = part_start.advanced_by(prefix);
4375 let name_span = Span::from_positions(name_start, name_start.advanced_by(name));
4376 self.var_ref(
4377 Name::from(name),
4378 name_span,
4379 subscript,
4380 Span::from_positions(part_start, part_end),
4381 )
4382 }
4383
4384 fn parameter_word_part_from_legacy(
4385 &self,
4386 part: WordPart,
4387 part_start: Position,
4388 part_end: Position,
4389 source_backed: bool,
4390 ) -> WordPart {
4391 let span = Span::from_positions(part_start, part_end);
4392 let raw_body = self.parameter_raw_body_from_legacy(&part, span, source_backed);
4393 let raw_body_text = raw_body.slice(self.input).to_string();
4394
4395 let syntax = match part {
4396 WordPart::ParameterExpansion {
4397 reference,
4398 operator,
4399 operand,
4400 operand_word_ast,
4401 colon_variant,
4402 } => Some(BourneParameterExpansion::Operation {
4403 reference,
4404 operator: self.enrich_parameter_operator(operator),
4405 operand,
4406 operand_word_ast,
4407 colon_variant,
4408 }),
4409 WordPart::Length(reference) | WordPart::ArrayLength(reference) => {
4410 Some(BourneParameterExpansion::Length { reference })
4411 }
4412 WordPart::ArrayAccess(reference) => {
4413 Some(BourneParameterExpansion::Access { reference })
4414 }
4415 WordPart::ArrayIndices(reference) => {
4416 Some(BourneParameterExpansion::Indices { reference })
4417 }
4418 WordPart::Substring {
4419 reference,
4420 offset,
4421 offset_ast,
4422 offset_word_ast,
4423 length,
4424 length_ast,
4425 length_word_ast,
4426 }
4427 | WordPart::ArraySlice {
4428 reference,
4429 offset,
4430 offset_ast,
4431 offset_word_ast,
4432 length,
4433 length_ast,
4434 length_word_ast,
4435 } => Some(BourneParameterExpansion::Slice {
4436 reference,
4437 offset,
4438 offset_ast,
4439 offset_word_ast,
4440 length,
4441 length_ast,
4442 length_word_ast,
4443 }),
4444 WordPart::IndirectExpansion {
4445 reference,
4446 operator,
4447 operand,
4448 operand_word_ast,
4449 colon_variant,
4450 } => Some(BourneParameterExpansion::Indirect {
4451 reference,
4452 operator: operator.map(|operator| self.enrich_parameter_operator(operator)),
4453 operand,
4454 operand_word_ast,
4455 colon_variant,
4456 }),
4457 WordPart::PrefixMatch { prefix, kind } => {
4458 Some(BourneParameterExpansion::PrefixMatch { prefix, kind })
4459 }
4460 WordPart::Transformation {
4461 reference,
4462 operator,
4463 } => Some(BourneParameterExpansion::Transformation {
4464 reference,
4465 operator,
4466 }),
4467 WordPart::Variable(name) if raw_body_text == name.as_str() => {
4468 Some(BourneParameterExpansion::Access {
4469 reference: self.parameter_var_ref(
4470 part_start,
4471 "${",
4472 name.as_str(),
4473 None,
4474 part_end,
4475 ),
4476 })
4477 }
4478 other => return other,
4479 };
4480
4481 let Some(syntax) = syntax else {
4482 unreachable!("matched Some above");
4483 };
4484 WordPart::Parameter(ParameterExpansion {
4485 syntax: ParameterExpansionSyntax::Bourne(syntax),
4486 span,
4487 raw_body,
4488 })
4489 }
4490
4491 fn enrich_parameter_operator(&self, operator: ParameterOp) -> ParameterOp {
4492 match operator {
4493 ParameterOp::ReplaceFirst {
4494 pattern,
4495 replacement,
4496 ..
4497 } => ParameterOp::ReplaceFirst {
4498 pattern,
4499 replacement_word_ast: self.parse_source_text_as_word(&replacement),
4500 replacement,
4501 },
4502 ParameterOp::ReplaceAll {
4503 pattern,
4504 replacement,
4505 ..
4506 } => ParameterOp::ReplaceAll {
4507 pattern,
4508 replacement_word_ast: self.parse_source_text_as_word(&replacement),
4509 replacement,
4510 },
4511 ParameterOp::UseDefault
4512 | ParameterOp::AssignDefault
4513 | ParameterOp::UseReplacement
4514 | ParameterOp::Error
4515 | ParameterOp::RemovePrefixShort { .. }
4516 | ParameterOp::RemovePrefixLong { .. }
4517 | ParameterOp::RemoveSuffixShort { .. }
4518 | ParameterOp::RemoveSuffixLong { .. }
4519 | ParameterOp::UpperFirst
4520 | ParameterOp::UpperAll
4521 | ParameterOp::LowerFirst
4522 | ParameterOp::LowerAll => operator,
4523 }
4524 }
4525
4526 fn parameter_raw_body_from_legacy(
4527 &self,
4528 part: &WordPart,
4529 span: Span,
4530 source_backed: bool,
4531 ) -> SourceText {
4532 if source_backed && span.end.offset <= self.input.len() {
4533 let syntax = span.slice(self.input);
4534 if let Some(body) = syntax
4535 .strip_prefix("${")
4536 .and_then(|syntax| syntax.strip_suffix('}'))
4537 {
4538 let start = span.start.advanced_by("${");
4539 let end = start.advanced_by(body);
4540 return SourceText::source(Span::from_positions(start, end));
4541 }
4542 }
4543
4544 let mut syntax = String::new();
4545 self.push_word_part_syntax(&mut syntax, part, span);
4546 let body = syntax
4547 .strip_prefix("${")
4548 .and_then(|syntax| syntax.strip_suffix('}'))
4549 .unwrap_or(syntax.as_str())
4550 .to_string();
4551 SourceText::from(body)
4552 }
4553
4554 fn zsh_parameter_word_part(
4555 &mut self,
4556 raw_body: SourceText,
4557 part_start: Position,
4558 part_end: Position,
4559 ) -> WordPart {
4560 let syntax = self.parse_zsh_parameter_syntax(&raw_body, raw_body.span().start);
4561 WordPart::Parameter(ParameterExpansion {
4562 syntax: ParameterExpansionSyntax::Zsh(syntax),
4563 span: Span::from_positions(part_start, part_end),
4564 raw_body,
4565 })
4566 }
4567
4568 fn parse_zsh_modifier_group(
4569 &self,
4570 text: &str,
4571 base: Position,
4572 start: usize,
4573 ) -> Option<(usize, Vec<ZshModifier>)> {
4574 let rest = text.get(start..)?;
4575 if !rest.starts_with('(') {
4576 return None;
4577 }
4578
4579 let close_rel = rest[1..].find(')')?;
4580 let close = start + 1 + close_rel;
4581 let group_text = &text[start..=close];
4582 let inner = &text[start + 1..close];
4583 let group_start = base.advanced_by(&text[..start]);
4584 let group_span = Span::from_positions(group_start, group_start.advanced_by(group_text));
4585 let mut modifiers = Vec::new();
4586 let mut index = 0usize;
4587
4588 while index < inner.len() {
4589 let name = inner[index..].chars().next()?;
4590 index += name.len_utf8();
4591
4592 let mut argument_delimiter = None;
4593 let mut argument = None;
4594 if matches!(name, 's' | 'j')
4595 && let Some(delimiter) = inner[index..].chars().next()
4596 {
4597 index += delimiter.len_utf8();
4598 let argument_start = index;
4599 while index < inner.len() {
4600 let ch = inner[index..].chars().next()?;
4601 if ch == delimiter {
4602 let argument_text = &inner[argument_start..index];
4603 let argument_base =
4604 group_start.advanced_by(&group_text[..1 + argument_start]);
4605 let argument_end = argument_base.advanced_by(argument_text);
4606 argument_delimiter = Some(delimiter);
4607 argument = Some(self.source_text(
4608 argument_text.to_string(),
4609 argument_base,
4610 argument_end,
4611 ));
4612 index += delimiter.len_utf8();
4613 break;
4614 }
4615 index += ch.len_utf8();
4616 }
4617 }
4618
4619 let argument_word_ast = argument
4620 .as_ref()
4621 .map(|argument| self.parse_source_text_as_word(argument));
4622
4623 modifiers.push(ZshModifier {
4624 name,
4625 argument,
4626 argument_word_ast,
4627 argument_delimiter,
4628 span: group_span,
4629 });
4630 }
4631
4632 Some((close + 1, modifiers))
4633 }
4634
4635 fn parse_zsh_parameter_syntax(
4636 &mut self,
4637 raw_body: &SourceText,
4638 base: Position,
4639 ) -> ZshParameterExpansion {
4640 let text = raw_body.slice(self.input);
4641 let mut index = 0;
4642 let mut modifiers = Vec::new();
4643 let mut length_prefix = None;
4644 let source_backed = raw_body.is_source_backed();
4645
4646 while text[index..].starts_with('(')
4647 && let Some((next_index, group_modifiers)) =
4648 self.parse_zsh_modifier_group(text, base, index)
4649 {
4650 modifiers.extend(group_modifiers);
4651 index = next_index;
4652 }
4653
4654 while index < text.len() {
4655 let Some(flag) = text[index..].chars().next() else {
4656 break;
4657 };
4658 match flag {
4659 '=' | '~' | '^' => {
4660 let modifier_start = base.advanced_by(&text[..index]);
4661 let modifier_end =
4662 modifier_start.advanced_by(&text[index..index + flag.len_utf8()]);
4663 modifiers.push(ZshModifier {
4664 name: flag,
4665 argument: None,
4666 argument_word_ast: None,
4667 argument_delimiter: None,
4668 span: Span::from_positions(modifier_start, modifier_end),
4669 });
4670 index += flag.len_utf8();
4671 }
4672 '#' if length_prefix.is_none() => {
4673 let prefix_start = base.advanced_by(&text[..index]);
4674 let prefix_end = prefix_start.advanced_by("#");
4675 length_prefix = Some(Span::from_positions(prefix_start, prefix_end));
4676 index += '#'.len_utf8();
4677 }
4678 _ => break,
4679 }
4680 }
4681
4682 let (target, operation_index) = if text[index..].starts_with("${") {
4683 let end = self
4684 .find_matching_parameter_end(&text[index..])
4685 .unwrap_or(text.len() - index);
4686 let nested_text = &text[index..index + end];
4687 let target =
4688 self.parse_nested_parameter_target(nested_text, base.advanced_by(&text[..index]));
4689 (target, index + end)
4690 } else if text[index..].starts_with(':') || text[index..].is_empty() {
4691 (ZshExpansionTarget::Empty, index)
4692 } else {
4693 let end = self
4694 .find_zsh_operation_start(&text[index..])
4695 .map(|offset| index + offset)
4696 .unwrap_or(text.len());
4697 let raw_target = &text[index..end];
4698 let trimmed = raw_target.trim();
4699 let target = if trimmed.is_empty() {
4700 ZshExpansionTarget::Empty
4701 } else {
4702 let leading = raw_target
4703 .len()
4704 .saturating_sub(raw_target.trim_start().len());
4705 let target_base = base.advanced_by(&text[..index + leading]);
4706 self.parse_zsh_target_from_text(
4707 trimmed,
4708 target_base,
4709 source_backed && leading == 0 && trimmed.len() == raw_target.len(),
4710 )
4711 };
4712 (target, end)
4713 };
4714
4715 let operation = (operation_index < text.len()).then(|| {
4716 self.parse_zsh_parameter_operation(
4717 &text[operation_index..],
4718 base.advanced_by(&text[..operation_index]),
4719 )
4720 });
4721
4722 ZshParameterExpansion {
4723 target,
4724 modifiers,
4725 length_prefix,
4726 operation,
4727 }
4728 }
4729
4730 fn parse_zsh_target_from_text(
4731 &mut self,
4732 text: &str,
4733 base: Position,
4734 source_backed: bool,
4735 ) -> ZshExpansionTarget {
4736 let trimmed = text.trim();
4737 if trimmed.is_empty() {
4738 return ZshExpansionTarget::Empty;
4739 }
4740
4741 if trimmed.starts_with("${") && trimmed.ends_with('}') {
4742 return self.parse_nested_parameter_target(trimmed, base);
4743 }
4744
4745 if let Some(reference) = self.maybe_parse_loose_var_ref_target(trimmed) {
4746 return ZshExpansionTarget::Reference(reference);
4747 }
4748
4749 let span = Span::from_positions(base, base.advanced_by(trimmed));
4750 let word = self.parse_word_with_context(trimmed, span, base, source_backed);
4751 if let Some(reference) =
4752 self.parse_var_ref_from_word(&word, SubscriptInterpretation::Contextual)
4753 {
4754 ZshExpansionTarget::Reference(reference)
4755 } else {
4756 ZshExpansionTarget::Word(word)
4757 }
4758 }
4759
4760 fn maybe_parse_loose_var_ref_target(&self, text: &str) -> Option<VarRef> {
4761 let trimmed = text.trim();
4762 Self::looks_like_plain_parameter_access(trimmed).then(|| self.parse_loose_var_ref(trimmed))
4763 }
4764
4765 fn is_plain_special_parameter_name(name: &str) -> bool {
4766 matches!(name, "#" | "$" | "!" | "*" | "@" | "?" | "-") || name == "0"
4767 }
4768
4769 fn looks_like_plain_parameter_access(text: &str) -> bool {
4770 let trimmed = text.trim();
4771 if trimmed.is_empty() {
4772 return false;
4773 }
4774
4775 let name = if let Some(open) = trimmed.find('[') {
4776 if !trimmed.ends_with(']') {
4777 return false;
4778 }
4779 &trimmed[..open]
4780 } else {
4781 trimmed
4782 };
4783
4784 Self::is_valid_identifier(name)
4785 || name.bytes().all(|byte| byte.is_ascii_digit())
4786 || Self::is_plain_special_parameter_name(name)
4787 }
4788
4789 fn parse_nested_parameter_target(&mut self, text: &str, base: Position) -> ZshExpansionTarget {
4790 if !(text.starts_with("${") && text.ends_with('}')) {
4791 return self.parse_zsh_target_from_text(text, base, false);
4792 }
4793
4794 let raw_body_start = base.advanced_by("${");
4795 let raw_body = self.source_text(
4796 text[2..text.len() - 1].to_string(),
4797 raw_body_start,
4798 base.advanced_by(&text[..text.len() - 1]),
4799 );
4800 let raw_body_text = raw_body.slice(self.input);
4801 let has_operation = self.find_zsh_operation_start(raw_body_text).is_some();
4802 let syntax = if Self::looks_like_plain_parameter_access(raw_body_text) && !has_operation {
4803 ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access {
4804 reference: self.parse_loose_var_ref(raw_body_text),
4805 })
4806 } else if 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 || raw_body_text.starts_with('.')
4812 || raw_body_text.starts_with('#')
4813 || raw_body_text.starts_with('"')
4814 || raw_body_text.starts_with('\'')
4815 || raw_body_text.starts_with('$')
4816 || has_operation
4817 {
4818 ParameterExpansionSyntax::Zsh(
4819 self.parse_zsh_parameter_syntax(&raw_body, raw_body_start),
4820 )
4821 } else {
4822 ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access {
4823 reference: self.parse_loose_var_ref(raw_body_text),
4824 })
4825 };
4826
4827 ZshExpansionTarget::Nested(Box::new(ParameterExpansion {
4828 syntax,
4829 span: Span::from_positions(base, base.advanced_by(text)),
4830 raw_body,
4831 }))
4832 }
4833
4834 fn parse_loose_var_ref(&self, text: &str) -> VarRef {
4835 let trimmed = text.trim();
4836 if let Some(open) = trimmed.find('[')
4837 && trimmed.ends_with(']')
4838 {
4839 let name = &trimmed[..open];
4840 let subscript_text = &trimmed[open + 1..trimmed.len() - 1];
4841 let subscript = self.subscript_from_source_text(
4842 SourceText::from(subscript_text.to_string()),
4843 None,
4844 SubscriptInterpretation::Contextual,
4845 );
4846 return VarRef {
4847 name: Name::from(name),
4848 name_span: Span::new(),
4849 subscript: Some(Box::new(subscript)),
4850 span: Span::new(),
4851 };
4852 }
4853
4854 VarRef {
4855 name: Name::from(trimmed),
4856 name_span: Span::new(),
4857 subscript: None,
4858 span: Span::new(),
4859 }
4860 }
4861
4862 fn find_matching_parameter_end(&self, text: &str) -> Option<usize> {
4863 let mut depth = 0_i32;
4864 let mut chars = text.char_indices().peekable();
4865
4866 while let Some((index, ch)) = chars.next() {
4867 match ch {
4868 '$' if chars.peek().is_some_and(|(_, next)| *next == '{') => {
4869 depth += 1;
4870 }
4871 '}' => {
4872 depth -= 1;
4873 if depth == 0 {
4874 return Some(index + ch.len_utf8());
4875 }
4876 }
4877 _ => {}
4878 }
4879 }
4880
4881 None
4882 }
4883
4884 fn find_zsh_operation_start(&self, text: &str) -> Option<usize> {
4885 let mut bracket_depth = 0_usize;
4886 let mut in_single = false;
4887 let mut in_double = false;
4888 let mut escaped = false;
4889
4890 for (index, ch) in text.char_indices() {
4891 if escaped {
4892 escaped = false;
4893 continue;
4894 }
4895
4896 match ch {
4897 '\\' if !in_single => escaped = true,
4898 '\'' if !in_double => in_single = !in_single,
4899 '"' if !in_single => in_double = !in_double,
4900 '[' if !in_single && !in_double => bracket_depth += 1,
4901 ']' if !in_single && !in_double && bracket_depth > 0 => bracket_depth -= 1,
4902 ':' if !in_single && !in_double && bracket_depth == 0 => return Some(index),
4903 '#' | '%' | '/' | '^' | ',' | '~'
4904 if !in_single && !in_double && bracket_depth == 0 && index > 0 =>
4905 {
4906 return Some(index);
4907 }
4908 _ => {}
4909 }
4910 }
4911
4912 None
4913 }
4914
4915 fn zsh_operation_source_text(
4916 &self,
4917 text: &str,
4918 base: Position,
4919 start: usize,
4920 end: usize,
4921 ) -> SourceText {
4922 self.source_text(
4923 text[start..end].to_string(),
4924 base.advanced_by(&text[..start]),
4925 base.advanced_by(&text[..end]),
4926 )
4927 }
4928
4929 fn find_zsh_top_level_delimiter(&self, text: &str, delimiter: char) -> Option<usize> {
4930 let mut chars = text.char_indices().peekable();
4931 let mut in_single = false;
4932 let mut in_double = false;
4933 let mut escaped = false;
4934 let mut brace_depth = 0_usize;
4935 let mut paren_depth = 0_usize;
4936
4937 while let Some((index, ch)) = chars.next() {
4938 if escaped {
4939 escaped = false;
4940 continue;
4941 }
4942
4943 match ch {
4944 '\\' if !in_single => escaped = true,
4945 '\'' if !in_double => in_single = !in_single,
4946 '"' if !in_single => in_double = !in_double,
4947 '$' if !in_single => {
4948 if let Some((_, next)) = chars.peek() {
4949 if *next == '{' {
4950 brace_depth += 1;
4951 chars.next();
4952 } else if *next == '(' {
4953 paren_depth += 1;
4954 chars.next();
4955 if let Some((_, after)) = chars.peek()
4956 && *after == '('
4957 {
4958 paren_depth += 1;
4959 chars.next();
4960 }
4961 }
4962 }
4963 }
4964 '}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
4965 ')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
4966 _ if ch == delimiter
4967 && !in_single
4968 && !in_double
4969 && brace_depth == 0
4970 && paren_depth == 0 =>
4971 {
4972 return Some(index);
4973 }
4974 _ => {}
4975 }
4976 }
4977
4978 None
4979 }
4980
4981 fn zsh_simple_modifier_suffix_segment(segment: &str) -> bool {
4982 let mut chars = segment.chars();
4983 let Some(first) = chars.next() else {
4984 return false;
4985 };
4986
4987 match first {
4988 'a' | 'A' | 'c' | 'e' | 'l' | 'P' | 'q' | 'Q' | 'r' | 'u' => chars.next().is_none(),
4989 'h' | 't' => chars.all(|ch| ch.is_ascii_digit()),
4990 _ => false,
4991 }
4992 }
4993
4994 fn zsh_modifier_suffix_candidate(rest: &str) -> bool {
4995 if rest.is_empty() {
4996 return false;
4997 }
4998
4999 let Some(first) = rest.chars().next() else {
5000 return false;
5001 };
5002 if first.is_ascii_digit()
5003 || first.is_ascii_whitespace()
5004 || matches!(first, '$' | '\'' | '"' | '(' | '{')
5005 {
5006 return false;
5007 }
5008
5009 rest.split(':')
5010 .all(Self::zsh_simple_modifier_suffix_segment)
5011 }
5012
5013 fn zsh_slice_candidate(rest: &str) -> bool {
5014 let Some(first) = rest.chars().next() else {
5015 return false;
5016 };
5017
5018 !Self::zsh_modifier_suffix_candidate(rest)
5019 && (first.is_ascii_alphanumeric()
5020 || first == '_'
5021 || first.is_ascii_whitespace()
5022 || matches!(first, '$' | '\'' | '"' | '(' | '{'))
5023 }
5024
5025 fn parse_zsh_parameter_operation(&self, text: &str, base: Position) -> ZshExpansionOperation {
5026 if let Some(operand) = text.strip_prefix(":#") {
5027 let operand = self.source_text(
5028 operand.to_string(),
5029 base.advanced_by(":#"),
5030 base.advanced_by(text),
5031 );
5032 return ZshExpansionOperation::PatternOperation {
5033 kind: ZshPatternOp::Filter,
5034 operand_word_ast: self.parse_source_text_as_word(&operand),
5035 operand,
5036 };
5037 }
5038
5039 if let Some((kind, operand)) = text
5040 .strip_prefix(":-")
5041 .map(|operand| (ZshDefaultingOp::UseDefault, operand))
5042 .or_else(|| {
5043 text.strip_prefix(":=")
5044 .map(|operand| (ZshDefaultingOp::AssignDefault, operand))
5045 })
5046 .or_else(|| {
5047 text.strip_prefix(":+")
5048 .map(|operand| (ZshDefaultingOp::UseReplacement, operand))
5049 })
5050 .or_else(|| {
5051 text.strip_prefix(":?")
5052 .map(|operand| (ZshDefaultingOp::Error, operand))
5053 })
5054 {
5055 let operand = self.source_text(
5056 operand.to_string(),
5057 base.advanced_by(&text[..2]),
5058 base.advanced_by(text),
5059 );
5060 return ZshExpansionOperation::Defaulting {
5061 kind,
5062 operand_word_ast: self.parse_source_text_as_word(&operand),
5063 operand,
5064 colon_variant: true,
5065 };
5066 }
5067
5068 if let Some((kind, prefix_len)) = [
5069 ("##", ZshTrimOp::RemovePrefixLong),
5070 ("#", ZshTrimOp::RemovePrefixShort),
5071 ("%%", ZshTrimOp::RemoveSuffixLong),
5072 ("%", ZshTrimOp::RemoveSuffixShort),
5073 ]
5074 .into_iter()
5075 .find_map(|(prefix, kind)| text.starts_with(prefix).then_some((kind, prefix.len())))
5076 {
5077 let operand = self.zsh_operation_source_text(text, base, prefix_len, text.len());
5078 return ZshExpansionOperation::TrimOperation {
5079 kind,
5080 operand_word_ast: self.parse_source_text_as_word(&operand),
5081 operand,
5082 };
5083 }
5084
5085 if let Some((kind, prefix_len)) = [
5086 ("//", ZshReplacementOp::ReplaceAll),
5087 ("/#", ZshReplacementOp::ReplacePrefix),
5088 ("/%", ZshReplacementOp::ReplaceSuffix),
5089 ("/", ZshReplacementOp::ReplaceFirst),
5090 ]
5091 .into_iter()
5092 .find_map(|(prefix, kind)| text.starts_with(prefix).then_some((kind, prefix.len())))
5093 {
5094 let rest = &text[prefix_len..];
5095 let separator = self.find_zsh_top_level_delimiter(rest, '/');
5096 let pattern_end = separator.unwrap_or(rest.len());
5097 let pattern =
5098 self.zsh_operation_source_text(text, base, prefix_len, prefix_len + pattern_end);
5099 let replacement = separator.map(|separator| {
5100 self.zsh_operation_source_text(text, base, prefix_len + separator + 1, text.len())
5101 });
5102 return ZshExpansionOperation::ReplacementOperation {
5103 kind,
5104 pattern_word_ast: self.parse_source_text_as_word(&pattern),
5105 replacement_word_ast: self.parse_optional_source_text_as_word(replacement.as_ref()),
5106 pattern,
5107 replacement,
5108 };
5109 }
5110
5111 if let Some(rest) = text.strip_prefix(':') {
5112 if Self::zsh_modifier_suffix_candidate(rest) {
5113 let text = self.source_text(text.to_string(), base, base.advanced_by(text));
5114 return ZshExpansionOperation::Unknown {
5115 word_ast: self.parse_source_text_as_word(&text),
5116 text,
5117 };
5118 }
5119
5120 if Self::zsh_slice_candidate(rest) {
5121 let separator = self.find_zsh_top_level_delimiter(rest, ':');
5122 let offset_end = separator.unwrap_or(rest.len());
5123 let offset = self.zsh_operation_source_text(text, base, 1, 1 + offset_end);
5124 let length = separator.map(|separator| {
5125 self.zsh_operation_source_text(text, base, 1 + separator + 1, text.len())
5126 });
5127 return ZshExpansionOperation::Slice {
5128 offset_word_ast: self.parse_source_text_as_word(&offset),
5129 length_word_ast: self.parse_optional_source_text_as_word(length.as_ref()),
5130 offset,
5131 length,
5132 };
5133 }
5134 }
5135
5136 let text = self.source_text(text.to_string(), base, base.advanced_by(text));
5137 ZshExpansionOperation::Unknown {
5138 word_ast: self.parse_source_text_as_word(&text),
5139 text,
5140 }
5141 }
5142
5143 fn parse_explicit_arithmetic_span(
5144 &self,
5145 span: Option<Span>,
5146 context: &'static str,
5147 ) -> Result<Option<ArithmeticExprNode>> {
5148 let Some(span) = span else {
5149 return Ok(None);
5150 };
5151 if span.slice(self.input).trim().is_empty() {
5152 return Ok(None);
5153 }
5154 arithmetic::parse_expression(
5155 span.slice(self.input),
5156 span,
5157 self.dialect,
5158 self.max_depth.saturating_sub(self.current_depth),
5159 self.fuel,
5160 )
5161 .map(Some)
5162 .map_err(|error| match error {
5163 Error::Parse { message, .. } => self.error(format!("{context}: {message}")),
5164 })
5165 }
5166
5167 fn parse_source_text_as_arithmetic(&self, text: &SourceText) -> Result<ArithmeticExprNode> {
5168 arithmetic::parse_expression(
5169 text.slice(self.input),
5170 text.span(),
5171 self.dialect,
5172 self.max_depth.saturating_sub(self.current_depth),
5173 self.fuel,
5174 )
5175 }
5176
5177 fn maybe_parse_source_text_as_arithmetic(
5178 &self,
5179 text: &SourceText,
5180 ) -> Option<ArithmeticExprNode> {
5181 if !text.is_source_backed() {
5182 return None;
5183 }
5184 self.parse_source_text_as_arithmetic(text).ok()
5185 }
5186
5187 fn parse_source_text_as_word(&self, text: &SourceText) -> Word {
5188 if let Some(word) = self.simple_source_text_as_word(text) {
5189 return word;
5190 }
5191
5192 let span = text.span();
5193 let remaining_depth = self.max_depth.saturating_sub(self.current_depth);
5194 if remaining_depth == 0 {
5195 return self.word_with_single_part(
5196 self.literal_part_from_text(text.slice(self.input), span, text.is_source_backed()),
5197 span,
5198 );
5199 }
5200 let reparse_depth = (remaining_depth - 1).min(SOURCE_TEXT_WORD_REPARSE_MAX_DEPTH);
5201
5202 if !text.is_source_backed()
5203 && span.start.offset <= span.end.offset
5204 && span.end.offset <= self.input.len()
5205 {
5206 let raw = span.slice(self.input);
5207 if raw.contains("\\\"") {
5208 return Self::parse_word_fragment_with_limits(
5209 self.input,
5210 raw,
5211 span,
5212 reparse_depth,
5213 self.fuel,
5214 self.shell_profile.clone(),
5215 );
5216 }
5217 }
5218
5219 Self::parse_word_fragment_with_limits(
5220 self.input,
5221 text.slice(self.input),
5222 text.span(),
5223 reparse_depth,
5224 self.fuel,
5225 self.shell_profile.clone(),
5226 )
5227 }
5228
5229 fn simple_source_text_as_word(&self, text: &SourceText) -> Option<Word> {
5230 if !text.is_source_backed() {
5231 return None;
5232 }
5233
5234 let span = text.span();
5235 let raw = text.slice(self.input);
5236 if raw.is_empty() {
5237 return Some(Word::literal_with_span("", span));
5238 }
5239
5240 if let Some(word) = self.simple_quoted_source_text_as_word(raw, span) {
5241 return Some(word);
5242 }
5243
5244 if Self::word_text_needs_parse(raw)
5245 || raw.contains(['\'', '"', '\\'])
5246 || self.zsh_glob_qualifiers_enabled_at(span.start.offset)
5247 {
5248 return None;
5249 }
5250
5251 Some(self.word_with_single_part(self.literal_part_from_text(raw, span, true), span))
5252 }
5253
5254 fn simple_quoted_source_text_as_word(&self, raw: &str, span: Span) -> Option<Word> {
5255 if raw.len() < 2 {
5256 return None;
5257 }
5258
5259 let quote = raw.as_bytes()[0];
5260 if quote != b'\'' && quote != b'"' || raw.as_bytes().last().copied() != Some(quote) {
5261 return None;
5262 }
5263
5264 let inner = &raw[1..raw.len() - 1];
5265 if quote == b'\'' && inner.contains('\'') {
5266 return None;
5267 }
5268
5269 let inner_start = span.start.advanced_by(&raw[..1]);
5270 let inner_end = inner_start.advanced_by(inner);
5271 let inner_span = Span::from_positions(inner_start, inner_end);
5272 let part = match quote {
5273 b'\'' => self.single_quoted_part_from_text(inner, inner_span, span, false),
5274 b'"' => {
5275 if Self::word_text_needs_parse(inner) || inner.contains(['\\', '"']) {
5276 return None;
5277 }
5278 self.double_quoted_literal_part_from_text(inner, inner_span, span, true, false)
5279 }
5280 _ => unreachable!("quote is checked above"),
5281 };
5282 Some(self.word_with_single_part(part, span))
5283 }
5284
5285 fn parse_optional_source_text_as_word(&self, text: Option<&SourceText>) -> Option<Word> {
5286 text.map(|text| self.parse_source_text_as_word(text))
5287 }
5288
5289 fn source_matches(&self, span: Span, text: &str) -> bool {
5290 span.start.offset <= span.end.offset
5291 && self
5292 .input
5293 .get(span.start.offset..span.end.offset)
5294 .is_some_and(|slice| slice == text)
5295 }
5296
5297 fn checkpoint(&self) -> ParserCheckpoint<'a> {
5298 ParserCheckpoint {
5299 lexer: self.lexer.clone(),
5300 synthetic_tokens: self.synthetic_tokens.clone(),
5301 alias_replays: self.alias_replays.clone(),
5302 current_token: self.current_token.clone(),
5303 current_token_kind: self.current_token_kind,
5304 current_keyword: self.current_keyword,
5305 current_span: self.current_span,
5306 peeked_token: self.peeked_token.clone(),
5307 current_depth: self.current_depth,
5308 fuel: self.fuel,
5309 source_text_pattern_depth: self.source_text_pattern_depth,
5310 comments_len: self.comments.len(),
5311 expand_next_word: self.expand_next_word,
5312 brace_group_depth: self.brace_group_depth,
5313 brace_body_stack_len: self.brace_body_stack.len(),
5314 syntax_facts_zsh_brace_if_spans_len: self.syntax_facts.zsh_brace_if_spans.len(),
5315 syntax_facts_zsh_always_spans_len: self.syntax_facts.zsh_always_spans.len(),
5316 syntax_facts_zsh_case_group_parts_len: self.syntax_facts.zsh_case_group_parts.len(),
5317 #[cfg(feature = "benchmarking")]
5318 benchmark_counters: self.benchmark_counters,
5319 }
5320 }
5321
5322 fn restore(&mut self, checkpoint: ParserCheckpoint<'a>) {
5323 self.lexer = checkpoint.lexer;
5324 self.synthetic_tokens = checkpoint.synthetic_tokens;
5325 self.alias_replays = checkpoint.alias_replays;
5326 self.current_token = checkpoint.current_token;
5327 self.current_word_cache = None;
5329 self.current_token_kind = checkpoint.current_token_kind;
5330 self.current_keyword = checkpoint.current_keyword;
5331 self.current_span = checkpoint.current_span;
5332 self.peeked_token = checkpoint.peeked_token;
5333 self.current_depth = checkpoint.current_depth;
5334 self.fuel = checkpoint.fuel;
5335 self.source_text_pattern_depth = checkpoint.source_text_pattern_depth;
5336 self.comments.truncate(checkpoint.comments_len);
5337 self.expand_next_word = checkpoint.expand_next_word;
5338 self.brace_group_depth = checkpoint.brace_group_depth;
5339 self.brace_body_stack
5340 .truncate(checkpoint.brace_body_stack_len);
5341 self.syntax_facts
5342 .zsh_brace_if_spans
5343 .truncate(checkpoint.syntax_facts_zsh_brace_if_spans_len);
5344 self.syntax_facts
5345 .zsh_always_spans
5346 .truncate(checkpoint.syntax_facts_zsh_always_spans_len);
5347 self.syntax_facts
5348 .zsh_case_group_parts
5349 .truncate(checkpoint.syntax_facts_zsh_case_group_parts_len);
5350 #[cfg(feature = "benchmarking")]
5351 {
5352 self.benchmark_counters = checkpoint.benchmark_counters;
5353 }
5354 }
5355
5356 fn set_current_spanned(&mut self, token: LexedToken<'a>) {
5357 #[cfg(feature = "benchmarking")]
5358 self.maybe_record_set_current_spanned_call();
5359 let span = token.span;
5360 self.current_token_kind = Some(token.kind);
5361 self.current_keyword = Self::keyword_from_token(&token);
5362 self.current_token = Some(token);
5363 self.current_word_cache = None;
5364 self.current_span = span;
5365 }
5366
5367 fn set_current_kind(&mut self, kind: TokenKind, span: Span) {
5368 self.current_token_kind = Some(kind);
5369 self.current_keyword = None;
5370 self.current_token = Some(LexedToken::punctuation(kind).with_span(span));
5371 self.current_word_cache = None;
5372 self.current_span = span;
5373 }
5374
5375 fn clear_current_token(&mut self) {
5376 self.current_token = None;
5377 self.current_word_cache = None;
5378 self.current_token_kind = None;
5379 self.current_keyword = None;
5380 }
5381
5382 fn next_pending_token(&mut self) -> Option<LexedToken<'a>> {
5383 if let Some(token) = self.synthetic_tokens.pop_front() {
5384 return Some(token.materialize());
5385 }
5386
5387 loop {
5388 let replay = self.alias_replays.last_mut()?;
5389 if let Some(token) = replay.next_token() {
5390 return Some(token);
5391 }
5392 self.alias_replays.pop();
5393 }
5394 }
5395
5396 fn next_spanned_token_with_comments(&mut self) -> Option<LexedToken<'a>> {
5397 self.next_pending_token()
5398 .or_else(|| self.lexer.next_lexed_token_with_comments())
5399 }
5400
5401 fn compile_alias_definition(&self, value: &str) -> AliasDefinition {
5402 let source = Arc::<str>::from(value.to_string());
5403 let mut lexer = Lexer::with_max_subst_depth(source.as_ref(), self.max_depth);
5404 let mut tokens = Vec::new();
5405
5406 while let Some(token) = lexer.next_lexed_token_with_comments() {
5407 tokens.push(token.into_shared(&source));
5408 }
5409
5410 AliasDefinition {
5411 tokens: tokens.into(),
5412 expands_next_word: value.chars().last().is_some_and(char::is_whitespace),
5413 }
5414 }
5415
5416 fn maybe_expand_current_alias_chain(&mut self) {
5417 if !self.expand_aliases {
5418 self.expand_next_word = false;
5419 return;
5420 }
5421
5422 let mut seen = HashSet::new();
5423 let mut expands_next_word = false;
5424
5425 loop {
5426 if self.current_token_kind != Some(TokenKind::Word) {
5427 break;
5428 }
5429 let Some(name) = self.current_token.as_ref().and_then(LexedToken::word_text) else {
5430 break;
5431 };
5432 if self.current_source_word_starts_posix_function_header(name) {
5433 break;
5434 }
5435 let Some(alias) = self.aliases.get(name).cloned() else {
5436 break;
5437 };
5438 if !seen.insert(name.to_string()) {
5439 break;
5440 }
5441
5442 expands_next_word = alias.expands_next_word;
5443 self.peeked_token = None;
5444 self.alias_replays
5445 .push(AliasReplay::new(&alias, self.current_span.start));
5446 self.advance_raw();
5447 }
5448
5449 self.expand_next_word = expands_next_word;
5450 }
5451
5452 fn current_source_word_starts_posix_function_header(&self, name: &str) -> bool {
5453 if name.contains('=') || name.contains('[') {
5454 return false;
5455 }
5456
5457 if self
5458 .current_token
5459 .as_ref()
5460 .is_some_and(|token| token.flags.is_synthetic())
5461 {
5462 return false;
5463 }
5464
5465 let Some(tail) = self.input.get(self.current_span.end.offset..) else {
5466 return false;
5467 };
5468 let tail = tail.trim_start_matches([' ', '\t']);
5469 let Some(after_left) = tail.strip_prefix('(') else {
5470 return false;
5471 };
5472 let after_left = after_left.trim_start_matches([' ', '\t']);
5473 after_left.starts_with(')')
5474 }
5475
5476 fn next_word_char(
5477 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
5478 cursor: &mut Position,
5479 ) -> Option<char> {
5480 let ch = chars.next()?;
5481 cursor.advance(ch);
5482 Some(ch)
5483 }
5484
5485 fn next_word_char_unwrap(
5486 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
5487 cursor: &mut Position,
5488 ) -> char {
5489 let Some(ch) = Self::next_word_char(chars, cursor) else {
5490 unreachable!("word parser should only consume characters that were already peeked");
5491 };
5492 ch
5493 }
5494
5495 fn consume_word_char_if(
5496 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
5497 cursor: &mut Position,
5498 expected: char,
5499 ) -> bool {
5500 if chars.peek() == Some(&expected) {
5501 Self::next_word_char_unwrap(chars, cursor);
5502 true
5503 } else {
5504 false
5505 }
5506 }
5507
5508 fn read_word_while<F>(
5509 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
5510 cursor: &mut Position,
5511 mut predicate: F,
5512 ) -> String
5513 where
5514 F: FnMut(char) -> bool,
5515 {
5516 let mut text = String::new();
5517 while let Some(&ch) = chars.peek() {
5518 if !predicate(ch) {
5519 break;
5520 }
5521 text.push(Self::next_word_char_unwrap(chars, cursor));
5522 }
5523 text
5524 }
5525
5526 fn rebase_redirects(redirects: &mut [Redirect], base: Position) {
5527 for redirect in redirects {
5528 redirect.span = redirect.span.rebased(base);
5529 redirect.fd_var_span = redirect.fd_var_span.map(|span| span.rebased(base));
5530 match &mut redirect.target {
5531 RedirectTarget::Word(word) => Self::rebase_word(word, base),
5532 RedirectTarget::Heredoc(heredoc) => {
5533 heredoc.delimiter.span = heredoc.delimiter.span.rebased(base);
5534 Self::rebase_word(&mut heredoc.delimiter.raw, base);
5535 Self::rebase_heredoc_body(&mut heredoc.body, base);
5536 }
5537 }
5538 }
5539 }
5540
5541 fn rebase_assignments(assignments: &mut [Assignment], base: Position) {
5542 for assignment in assignments {
5543 assignment.span = assignment.span.rebased(base);
5544 Self::rebase_var_ref(&mut assignment.target, base);
5545 match &mut assignment.value {
5546 AssignmentValue::Scalar(word) => Self::rebase_word(word, base),
5547 AssignmentValue::Compound(array) => Self::rebase_array_expr(array, base),
5548 }
5549 }
5550 }
5551
5552 fn error(&self, message: impl Into<String>) -> Error {
5554 Error::parse_at(
5555 message,
5556 self.current_span.start.line,
5557 self.current_span.start.column,
5558 )
5559 }
5560
5561 fn ensure_feature(
5562 &self,
5563 enabled: bool,
5564 feature: &str,
5565 unsupported_message: &str,
5566 ) -> Result<()> {
5567 if enabled {
5568 Ok(())
5569 } else {
5570 Err(self.error(format!("{feature} {unsupported_message}")))
5571 }
5572 }
5573
5574 fn ensure_double_bracket(&self) -> Result<()> {
5575 self.ensure_feature(
5576 self.dialect.features().double_bracket,
5577 "[[ ]] conditionals",
5578 "are not available in this shell mode",
5579 )
5580 }
5581
5582 fn ensure_arithmetic_for(&self) -> Result<()> {
5583 self.ensure_feature(
5584 self.dialect.features().arithmetic_for,
5585 "c-style for loops",
5586 "are not available in this shell mode",
5587 )
5588 }
5589
5590 fn ensure_coproc(&self) -> Result<()> {
5591 self.ensure_feature(
5592 self.dialect.features().coproc_keyword,
5593 "coprocess commands",
5594 "are not available in this shell mode",
5595 )
5596 }
5597
5598 fn ensure_arithmetic_command(&self) -> Result<()> {
5599 self.ensure_feature(
5600 self.dialect.features().arithmetic_command,
5601 "arithmetic commands",
5602 "are not available in this shell mode",
5603 )
5604 }
5605
5606 fn ensure_select_loop(&self) -> Result<()> {
5607 self.ensure_feature(
5608 self.dialect.features().select_loop,
5609 "select loops",
5610 "are not available in this shell mode",
5611 )
5612 }
5613
5614 fn ensure_repeat_loop(&self) -> Result<()> {
5615 self.ensure_feature(
5616 self.zsh_short_repeat_enabled(),
5617 "repeat loops",
5618 "are not available in this shell mode",
5619 )
5620 }
5621
5622 fn ensure_foreach_loop(&self) -> Result<()> {
5623 self.ensure_feature(
5624 self.zsh_short_loops_enabled(),
5625 "foreach loops",
5626 "are not available in this shell mode",
5627 )
5628 }
5629
5630 fn ensure_function_keyword(&self) -> Result<()> {
5631 self.ensure_feature(
5632 self.dialect.features().function_keyword,
5633 "function keyword definitions",
5634 "are not available in this shell mode",
5635 )
5636 }
5637
5638 fn tick(&mut self) -> Result<()> {
5640 if self.fuel == 0 {
5641 let used = self.max_fuel - self.fuel;
5642 return Err(Error::parse(format!(
5643 "parser fuel exhausted ({} operations, max {})",
5644 used, self.max_fuel
5645 )));
5646 }
5647 self.fuel -= 1;
5648 Ok(())
5649 }
5650
5651 fn push_depth(&mut self) -> Result<()> {
5653 self.current_depth += 1;
5654 if self.current_depth > self.max_depth {
5655 return Err(Error::parse(format!(
5656 "AST nesting too deep ({} levels, max {})",
5657 self.current_depth, self.max_depth
5658 )));
5659 }
5660 Ok(())
5661 }
5662
5663 fn pop_depth(&mut self) {
5665 if self.current_depth > 0 {
5666 self.current_depth -= 1;
5667 }
5668 }
5669
5670 fn check_error_token(&self) -> Result<()> {
5672 if self.current_token_kind == Some(TokenKind::Error) {
5673 let msg = self
5674 .current_token
5675 .as_ref()
5676 .and_then(LexedToken::error_kind)
5677 .map(|kind| kind.message())
5678 .unwrap_or("unknown lexer error");
5679 return Err(self.error(format!("syntax error: {}", msg)));
5680 }
5681 Ok(())
5682 }
5683
5684 fn parse_diagnostic_from_error(&self, error: Error) -> ParseDiagnostic {
5685 let Error::Parse { message, .. } = error;
5686 ParseDiagnostic {
5687 message,
5688 span: self.current_span,
5689 }
5690 }
5691
5692 fn compound_span(compound: &CompoundCommand) -> Span {
5693 match compound {
5694 CompoundCommand::If(command) => command.span,
5695 CompoundCommand::For(command) => command.span,
5696 CompoundCommand::Repeat(command) => command.span,
5697 CompoundCommand::Foreach(command) => command.span,
5698 CompoundCommand::ArithmeticFor(command) => command.span,
5699 CompoundCommand::While(command) => command.span,
5700 CompoundCommand::Until(command) => command.span,
5701 CompoundCommand::Case(command) => command.span,
5702 CompoundCommand::Select(command) => command.span,
5703 CompoundCommand::Subshell(body) | CompoundCommand::BraceGroup(body) => body.span,
5704 CompoundCommand::Arithmetic(command) => command.span,
5705 CompoundCommand::Time(command) => command.span,
5706 CompoundCommand::Conditional(command) => command.span,
5707 CompoundCommand::Coproc(command) => command.span,
5708 CompoundCommand::Always(command) => command.span,
5709 }
5710 }
5711
5712 fn stmt_seq_with_span(span: Span, stmts: Vec<Stmt>) -> StmtSeq {
5713 StmtSeq {
5714 leading_comments: Vec::new(),
5715 stmts,
5716 trailing_comments: Vec::new(),
5717 span,
5718 }
5719 }
5720
5721 fn binary_stmt(left: Stmt, op: BinaryOp, op_span: Span, right: Stmt) -> Stmt {
5722 let span = left.span.merge(right.span);
5723 Stmt {
5724 leading_comments: Vec::new(),
5725 command: AstCommand::Binary(BinaryCommand {
5726 left: Box::new(left),
5727 op,
5728 op_span,
5729 right: Box::new(right),
5730 span,
5731 }),
5732 negated: false,
5733 redirects: Box::default(),
5734 terminator: None,
5735 terminator_span: None,
5736 inline_comment: None,
5737 span,
5738 }
5739 }
5740
5741 fn lower_builtin_command(
5742 builtin: BuiltinCommand,
5743 ) -> (AstBuiltinCommand, SmallVec<[Redirect; 1]>, Span) {
5744 match builtin {
5745 BuiltinCommand::Break(command) => {
5746 let span = command.span;
5747 let redirects = command.redirects;
5748 (
5749 AstBuiltinCommand::Break(AstBreakCommand {
5750 depth: command.depth,
5751 extra_args: command.extra_args.into_vec(),
5752 assignments: command.assignments.into_boxed_slice(),
5753 span,
5754 }),
5755 redirects,
5756 span,
5757 )
5758 }
5759 BuiltinCommand::Continue(command) => {
5760 let span = command.span;
5761 let redirects = command.redirects;
5762 (
5763 AstBuiltinCommand::Continue(AstContinueCommand {
5764 depth: command.depth,
5765 extra_args: command.extra_args.into_vec(),
5766 assignments: command.assignments.into_boxed_slice(),
5767 span,
5768 }),
5769 redirects,
5770 span,
5771 )
5772 }
5773 BuiltinCommand::Return(command) => {
5774 let span = command.span;
5775 let redirects = command.redirects;
5776 (
5777 AstBuiltinCommand::Return(AstReturnCommand {
5778 code: command.code,
5779 extra_args: command.extra_args.into_vec(),
5780 assignments: command.assignments.into_boxed_slice(),
5781 span,
5782 }),
5783 redirects,
5784 span,
5785 )
5786 }
5787 BuiltinCommand::Exit(command) => {
5788 let span = command.span;
5789 let redirects = command.redirects;
5790 (
5791 AstBuiltinCommand::Exit(AstExitCommand {
5792 code: command.code,
5793 extra_args: command.extra_args.into_vec(),
5794 assignments: command.assignments.into_boxed_slice(),
5795 span,
5796 }),
5797 redirects,
5798 span,
5799 )
5800 }
5801 }
5802 }
5803
5804 fn lower_non_sequence_command_to_stmt(command: Command) -> Stmt {
5805 match command {
5806 Command::Simple(command) => Stmt {
5807 leading_comments: Vec::new(),
5808 command: AstCommand::Simple(AstSimpleCommand {
5809 name: command.name,
5810 args: command.args.into_vec(),
5811 assignments: command.assignments.into_boxed_slice(),
5812 span: command.span,
5813 }),
5814 negated: false,
5815 redirects: command.redirects.into_boxed_slice(),
5816 terminator: None,
5817 terminator_span: None,
5818 inline_comment: None,
5819 span: command.span,
5820 },
5821 Command::Builtin(command) => {
5822 let (command, redirects, span) = Self::lower_builtin_command(command);
5823 Stmt {
5824 leading_comments: Vec::new(),
5825 command: AstCommand::Builtin(command),
5826 negated: false,
5827 redirects: redirects.into_boxed_slice(),
5828 terminator: None,
5829 terminator_span: None,
5830 inline_comment: None,
5831 span,
5832 }
5833 }
5834 Command::Decl(command) => {
5835 let command = *command;
5836 Stmt {
5837 leading_comments: Vec::new(),
5838 command: AstCommand::Decl(AstDeclClause {
5839 variant: command.variant,
5840 variant_span: command.variant_span,
5841 operands: command.operands.into_vec(),
5842 assignments: command.assignments.into_boxed_slice(),
5843 span: command.span,
5844 }),
5845 negated: false,
5846 redirects: command.redirects.into_boxed_slice(),
5847 terminator: None,
5848 terminator_span: None,
5849 inline_comment: None,
5850 span: command.span,
5851 }
5852 }
5853 Command::Compound(compound, redirects) => {
5854 let span = Self::compound_span(&compound);
5855 Stmt {
5856 leading_comments: Vec::new(),
5857 command: AstCommand::Compound(*compound),
5858 negated: false,
5859 redirects: redirects.into_boxed_slice(),
5860 terminator: None,
5861 terminator_span: None,
5862 inline_comment: None,
5863 span,
5864 }
5865 }
5866 Command::Function(function) => Stmt {
5867 leading_comments: Vec::new(),
5868 span: function.span,
5869 command: AstCommand::Function(function),
5870 negated: false,
5871 redirects: Box::default(),
5872 terminator: None,
5873 terminator_span: None,
5874 inline_comment: None,
5875 },
5876 Command::AnonymousFunction(function, redirects) => Stmt {
5877 leading_comments: Vec::new(),
5878 span: function.span,
5879 command: AstCommand::AnonymousFunction(function),
5880 negated: false,
5881 redirects: redirects.into_boxed_slice(),
5882 terminator: None,
5883 terminator_span: None,
5884 inline_comment: None,
5885 },
5886 }
5887 }
5888
5889 fn comment_start(comment: Comment) -> usize {
5890 usize::from(comment.range.start())
5891 }
5892
5893 fn is_inline_comment(source: &str, stmt: &Stmt, comment: Comment) -> bool {
5894 let comment_start = Self::comment_start(comment);
5895 if comment_start < stmt.span.end.offset {
5896 return false;
5897 }
5898 source
5899 .get(stmt.span.end.offset..comment_start)
5900 .is_some_and(|gap| !gap.contains('\n'))
5901 }
5902
5903 fn take_comments_before(
5904 comments: &mut VecDeque<Comment>,
5905 end_offset: usize,
5906 ) -> VecDeque<Comment> {
5907 let mut taken = VecDeque::new();
5908 while comments
5909 .front()
5910 .is_some_and(|comment| Self::comment_start(*comment) < end_offset)
5911 {
5912 let Some(comment) = comments.pop_front() else {
5913 unreachable!("front comment should exist while draining");
5914 };
5915 taken.push_back(comment);
5916 }
5917 taken
5918 }
5919
5920 fn attach_comments_to_file(&self, file: &mut File) {
5921 let mut comments = self.comments.iter().copied().collect::<VecDeque<_>>();
5922 Self::attach_comments_to_stmt_seq_with_source(self.input, &mut file.body, &mut comments);
5923 file.body.trailing_comments.extend(comments);
5924 }
5925
5926 fn attach_comments_to_stmt_seq_with_source(
5927 source: &str,
5928 sequence: &mut StmtSeq,
5929 comments: &mut VecDeque<Comment>,
5930 ) {
5931 if sequence.stmts.is_empty() {
5932 sequence
5933 .trailing_comments
5934 .extend(Self::take_comments_before(
5935 comments,
5936 sequence.span.end.offset,
5937 ));
5938 return;
5939 }
5940
5941 for (index, stmt) in sequence.stmts.iter_mut().enumerate() {
5942 let leading = Self::take_comments_before(comments, stmt.span.start.offset);
5943 if index == 0 {
5944 sequence.leading_comments.extend(leading);
5945 } else {
5946 stmt.leading_comments.extend(leading);
5947 }
5948
5949 let mut nested = Self::take_comments_before(comments, stmt.span.end.offset);
5950 Self::attach_comments_to_stmt_with_source(source, stmt, &mut nested);
5951 if !nested.is_empty() {
5952 stmt.leading_comments.extend(nested);
5953 }
5954
5955 if stmt.inline_comment.is_none()
5956 && comments
5957 .front()
5958 .is_some_and(|comment| Self::is_inline_comment(source, stmt, *comment))
5959 {
5960 stmt.inline_comment = comments.pop_front();
5961 }
5962 }
5963
5964 sequence
5965 .trailing_comments
5966 .extend(Self::take_comments_before(
5967 comments,
5968 sequence.span.end.offset,
5969 ));
5970 }
5971
5972 fn attach_comments_to_stmt_with_source(
5973 source: &str,
5974 stmt: &mut Stmt,
5975 comments: &mut VecDeque<Comment>,
5976 ) {
5977 match &mut stmt.command {
5978 AstCommand::Binary(binary) => {
5979 let mut left_comments =
5980 Self::take_comments_before(comments, binary.left.span.end.offset);
5981 Self::attach_comments_to_stmt_with_source(
5982 source,
5983 binary.left.as_mut(),
5984 &mut left_comments,
5985 );
5986 if !left_comments.is_empty() {
5987 binary.left.leading_comments.extend(left_comments);
5988 }
5989
5990 let mut right_comments = std::mem::take(comments);
5991 Self::attach_comments_to_stmt_with_source(
5992 source,
5993 binary.right.as_mut(),
5994 &mut right_comments,
5995 );
5996 if !right_comments.is_empty() {
5997 binary.right.leading_comments.extend(right_comments);
5998 }
5999 }
6000 AstCommand::Compound(compound) => {
6001 Self::attach_comments_to_compound_with_source(source, compound, comments);
6002 }
6003 AstCommand::Function(function) => {
6004 let mut body_comments = std::mem::take(comments);
6005 Self::attach_comments_to_stmt_with_source(
6006 source,
6007 function.body.as_mut(),
6008 &mut body_comments,
6009 );
6010 if !body_comments.is_empty() {
6011 function.body.leading_comments.extend(body_comments);
6012 }
6013 }
6014 AstCommand::AnonymousFunction(function) => {
6015 let mut body_comments = std::mem::take(comments);
6016 Self::attach_comments_to_stmt_with_source(
6017 source,
6018 function.body.as_mut(),
6019 &mut body_comments,
6020 );
6021 if !body_comments.is_empty() {
6022 function.body.leading_comments.extend(body_comments);
6023 }
6024 }
6025 AstCommand::Simple(_) | AstCommand::Builtin(_) | AstCommand::Decl(_) => {}
6026 }
6027 }
6028
6029 fn attach_comments_to_compound_with_source(
6030 source: &str,
6031 command: &mut CompoundCommand,
6032 comments: &mut VecDeque<Comment>,
6033 ) {
6034 match command {
6035 CompoundCommand::If(command) => {
6036 let mut condition =
6037 Self::take_comments_before(comments, command.condition.span.end.offset);
6038 Self::attach_comments_to_stmt_seq_with_source(
6039 source,
6040 &mut command.condition,
6041 &mut condition,
6042 );
6043 command.condition.trailing_comments.extend(condition);
6044
6045 let mut then_branch =
6046 Self::take_comments_before(comments, command.then_branch.span.end.offset);
6047 Self::attach_comments_to_stmt_seq_with_source(
6048 source,
6049 &mut command.then_branch,
6050 &mut then_branch,
6051 );
6052 command.then_branch.trailing_comments.extend(then_branch);
6053
6054 for (condition_seq, body_seq) in &mut command.elif_branches {
6055 let mut elif_condition =
6056 Self::take_comments_before(comments, condition_seq.span.end.offset);
6057 Self::attach_comments_to_stmt_seq_with_source(
6058 source,
6059 condition_seq,
6060 &mut elif_condition,
6061 );
6062 condition_seq.trailing_comments.extend(elif_condition);
6063
6064 let mut elif_body =
6065 Self::take_comments_before(comments, body_seq.span.end.offset);
6066 Self::attach_comments_to_stmt_seq_with_source(source, body_seq, &mut elif_body);
6067 body_seq.trailing_comments.extend(elif_body);
6068 }
6069
6070 if let Some(else_branch) = &mut command.else_branch {
6071 let mut else_comments = std::mem::take(comments);
6072 Self::attach_comments_to_stmt_seq_with_source(
6073 source,
6074 else_branch,
6075 &mut else_comments,
6076 );
6077 else_branch.trailing_comments.extend(else_comments);
6078 }
6079 }
6080 CompoundCommand::For(command) => {
6081 let mut body_comments = std::mem::take(comments);
6082 Self::attach_comments_to_stmt_seq_with_source(
6083 source,
6084 &mut command.body,
6085 &mut body_comments,
6086 );
6087 command.body.trailing_comments.extend(body_comments);
6088 }
6089 CompoundCommand::Repeat(command) => {
6090 let mut body_comments = std::mem::take(comments);
6091 Self::attach_comments_to_stmt_seq_with_source(
6092 source,
6093 &mut command.body,
6094 &mut body_comments,
6095 );
6096 command.body.trailing_comments.extend(body_comments);
6097 }
6098 CompoundCommand::Foreach(command) => {
6099 let mut body_comments = std::mem::take(comments);
6100 Self::attach_comments_to_stmt_seq_with_source(
6101 source,
6102 &mut command.body,
6103 &mut body_comments,
6104 );
6105 command.body.trailing_comments.extend(body_comments);
6106 }
6107 CompoundCommand::ArithmeticFor(command) => {
6108 let mut body_comments = std::mem::take(comments);
6109 Self::attach_comments_to_stmt_seq_with_source(
6110 source,
6111 &mut command.body,
6112 &mut body_comments,
6113 );
6114 command.body.trailing_comments.extend(body_comments);
6115 }
6116 CompoundCommand::While(command) => {
6117 let mut condition =
6118 Self::take_comments_before(comments, command.condition.span.end.offset);
6119 Self::attach_comments_to_stmt_seq_with_source(
6120 source,
6121 &mut command.condition,
6122 &mut condition,
6123 );
6124 command.condition.trailing_comments.extend(condition);
6125
6126 let mut body_comments = std::mem::take(comments);
6127 Self::attach_comments_to_stmt_seq_with_source(
6128 source,
6129 &mut command.body,
6130 &mut body_comments,
6131 );
6132 command.body.trailing_comments.extend(body_comments);
6133 }
6134 CompoundCommand::Until(command) => {
6135 let mut condition =
6136 Self::take_comments_before(comments, command.condition.span.end.offset);
6137 Self::attach_comments_to_stmt_seq_with_source(
6138 source,
6139 &mut command.condition,
6140 &mut condition,
6141 );
6142 command.condition.trailing_comments.extend(condition);
6143
6144 let mut body_comments = std::mem::take(comments);
6145 Self::attach_comments_to_stmt_seq_with_source(
6146 source,
6147 &mut command.body,
6148 &mut body_comments,
6149 );
6150 command.body.trailing_comments.extend(body_comments);
6151 }
6152 CompoundCommand::Case(command) => {
6153 for case in &mut command.cases {
6154 let mut body_comments =
6155 Self::take_comments_before(comments, case.body.span.end.offset);
6156 Self::attach_comments_to_stmt_seq_with_source(
6157 source,
6158 &mut case.body,
6159 &mut body_comments,
6160 );
6161 case.body.trailing_comments.extend(body_comments);
6162 }
6163 }
6164 CompoundCommand::Select(command) => {
6165 let mut body_comments = std::mem::take(comments);
6166 Self::attach_comments_to_stmt_seq_with_source(
6167 source,
6168 &mut command.body,
6169 &mut body_comments,
6170 );
6171 command.body.trailing_comments.extend(body_comments);
6172 }
6173 CompoundCommand::Subshell(body) | CompoundCommand::BraceGroup(body) => {
6174 let mut body_comments = std::mem::take(comments);
6175 Self::attach_comments_to_stmt_seq_with_source(source, body, &mut body_comments);
6176 body.trailing_comments.extend(body_comments);
6177 }
6178 CompoundCommand::Always(command) => {
6179 let mut body_comments =
6180 Self::take_comments_before(comments, command.body.span.end.offset);
6181 Self::attach_comments_to_stmt_seq_with_source(
6182 source,
6183 &mut command.body,
6184 &mut body_comments,
6185 );
6186 command.body.trailing_comments.extend(body_comments);
6187
6188 let mut always_comments = std::mem::take(comments);
6189 Self::attach_comments_to_stmt_seq_with_source(
6190 source,
6191 &mut command.always_body,
6192 &mut always_comments,
6193 );
6194 command
6195 .always_body
6196 .trailing_comments
6197 .extend(always_comments);
6198 }
6199 CompoundCommand::Time(command) => {
6200 if let Some(inner) = &mut command.command {
6201 let mut inner_comments = std::mem::take(comments);
6202 Self::attach_comments_to_stmt_with_source(
6203 source,
6204 inner.as_mut(),
6205 &mut inner_comments,
6206 );
6207 if !inner_comments.is_empty() {
6208 inner.leading_comments.extend(inner_comments);
6209 }
6210 }
6211 }
6212 CompoundCommand::Coproc(command) => {
6213 let mut body_comments = std::mem::take(comments);
6214 Self::attach_comments_to_stmt_with_source(
6215 source,
6216 command.body.as_mut(),
6217 &mut body_comments,
6218 );
6219 if !body_comments.is_empty() {
6220 command.body.leading_comments.extend(body_comments);
6221 }
6222 }
6223 CompoundCommand::Arithmetic(_) | CompoundCommand::Conditional(_) => {}
6224 }
6225 }
6226
6227 fn advance_raw(&mut self) {
6228 #[cfg(feature = "benchmarking")]
6229 self.maybe_record_advance_raw_call();
6230 if let Some(peeked) = self.peeked_token.take() {
6231 self.set_current_spanned(peeked);
6232 } else {
6233 loop {
6234 match self.next_spanned_token_with_comments() {
6235 Some(st) if st.kind == TokenKind::Comment => {
6236 self.maybe_record_comment(&st);
6237 }
6238 Some(st) => {
6239 self.set_current_spanned(st);
6240 break;
6241 }
6242 None => {
6243 self.clear_current_token();
6244 break;
6246 }
6247 }
6248 }
6249 }
6250 }
6251
6252 #[cfg(feature = "benchmarking")]
6253 fn maybe_record_set_current_spanned_call(&mut self) {
6254 if let Some(counters) = &mut self.benchmark_counters {
6255 counters.parser_set_current_spanned_calls += 1;
6256 }
6257 }
6258
6259 #[cfg(feature = "benchmarking")]
6260 fn maybe_record_advance_raw_call(&mut self) {
6261 if let Some(counters) = &mut self.benchmark_counters {
6262 counters.parser_advance_raw_calls += 1;
6263 }
6264 }
6265
6266 #[cfg(feature = "benchmarking")]
6267 fn finish_benchmark_counters(&self) -> ParserBenchmarkCounters {
6268 let mut counters = self.benchmark_counters.unwrap_or_default();
6269 counters.lexer_current_position_calls =
6270 self.lexer.benchmark_counters().current_position_calls;
6271 counters
6272 }
6273
6274 fn advance(&mut self) {
6275 let should_expand = std::mem::take(&mut self.expand_next_word);
6276 self.advance_raw();
6277 if should_expand {
6278 if self
6279 .current_token
6280 .as_ref()
6281 .is_some_and(|token| token.flags.is_synthetic())
6282 {
6283 self.expand_next_word = true;
6284 } else {
6285 self.maybe_expand_current_alias_chain();
6286 }
6287 }
6288 }
6289
6290 fn peek_next(&mut self) -> Option<&LexedToken<'a>> {
6292 if self.peeked_token.is_none() {
6293 loop {
6294 match self.next_spanned_token_with_comments() {
6295 Some(st) if st.kind == TokenKind::Comment => {
6296 self.maybe_record_comment(&st);
6297 }
6298 other => {
6299 self.peeked_token = other;
6300 break;
6301 }
6302 }
6303 }
6304 }
6305 self.peeked_token.as_ref()
6306 }
6307
6308 fn peek_next_kind(&mut self) -> Option<TokenKind> {
6309 self.peek_next()?;
6310 self.peeked_token.as_ref().map(|st| st.kind)
6311 }
6312
6313 fn peek_next_is(&mut self, kind: TokenKind) -> bool {
6314 self.peek_next_kind() == Some(kind)
6315 }
6316
6317 fn at(&self, kind: TokenKind) -> bool {
6318 self.current_token_kind == Some(kind)
6319 }
6320
6321 fn current_token_has_leading_whitespace(&self) -> bool {
6322 self.current_span.start.offset > 0
6323 && self.input[..self.current_span.start.offset]
6324 .chars()
6325 .next_back()
6326 .is_some_and(|ch| matches!(ch, ' ' | '\t' | '\n'))
6327 }
6328
6329 fn current_token_is_tight_to_next_token(&mut self) -> bool {
6330 let current_end = self.current_span.end.offset;
6331 self.peek_next()
6332 .is_some_and(|token| token.span.start.offset == current_end)
6333 }
6334
6335 fn at_in_set(&self, set: TokenSet) -> bool {
6336 self.current_token_kind
6337 .is_some_and(|kind| set.contains(kind))
6338 }
6339
6340 fn at_word_like(&self) -> bool {
6341 self.current_token_kind.is_some_and(TokenKind::is_word_like)
6342 }
6343
6344 fn current_word_str(&self) -> Option<&str> {
6345 self.current_token_kind
6346 .filter(|kind| kind.is_word_like())
6347 .and(self.current_token.as_ref())
6348 .and_then(LexedToken::word_text)
6349 }
6350
6351 fn classify_keyword(word: &str) -> Option<Keyword> {
6352 match word.as_bytes() {
6353 b"if" => Some(Keyword::If),
6354 b"for" => Some(Keyword::For),
6355 b"repeat" => Some(Keyword::Repeat),
6356 b"foreach" => Some(Keyword::Foreach),
6357 b"while" => Some(Keyword::While),
6358 b"until" => Some(Keyword::Until),
6359 b"case" => Some(Keyword::Case),
6360 b"select" => Some(Keyword::Select),
6361 b"time" => Some(Keyword::Time),
6362 b"coproc" => Some(Keyword::Coproc),
6363 b"function" => Some(Keyword::Function),
6364 b"always" => Some(Keyword::Always),
6365 b"then" => Some(Keyword::Then),
6366 b"else" => Some(Keyword::Else),
6367 b"elif" => Some(Keyword::Elif),
6368 b"fi" => Some(Keyword::Fi),
6369 b"do" => Some(Keyword::Do),
6370 b"done" => Some(Keyword::Done),
6371 b"esac" => Some(Keyword::Esac),
6372 b"in" => Some(Keyword::In),
6373 _ => None,
6374 }
6375 }
6376
6377 fn current_keyword(&self) -> Option<Keyword> {
6378 self.current_keyword
6379 }
6380
6381 fn looks_like_disabled_repeat_loop(&mut self) -> Result<bool> {
6382 if self.current_keyword() != Some(Keyword::Repeat) {
6383 return Ok(false);
6384 }
6385
6386 let checkpoint = self.checkpoint();
6387 self.advance();
6388 if !self.at_word_like() {
6389 self.restore(checkpoint);
6390 return Ok(false);
6391 }
6392 self.advance();
6393
6394 let result = match self.current_token_kind {
6395 Some(TokenKind::LeftBrace) => Ok(true),
6396 Some(TokenKind::Semicolon) => {
6397 self.advance();
6398 if let Err(error) = self.skip_newlines() {
6399 self.restore(checkpoint);
6400 return Err(error);
6401 }
6402 Ok(self.current_keyword() == Some(Keyword::Do))
6403 }
6404 Some(TokenKind::Newline) => {
6405 if let Err(error) = self.skip_newlines() {
6406 self.restore(checkpoint);
6407 return Err(error);
6408 }
6409 Ok(self.current_keyword() == Some(Keyword::Do))
6410 }
6411 _ => Ok(false),
6412 };
6413 self.restore(checkpoint);
6414 result
6415 }
6416
6417 fn looks_like_disabled_foreach_loop(&mut self) -> Result<bool> {
6418 if self.current_keyword() != Some(Keyword::Foreach) {
6419 return Ok(false);
6420 }
6421
6422 let checkpoint = self.checkpoint();
6423 self.advance();
6424 if self.current_name_token().is_none() {
6425 self.restore(checkpoint);
6426 return Ok(false);
6427 }
6428 self.advance();
6429
6430 let result = if self.at(TokenKind::LeftParen) {
6431 self.advance();
6432 let mut saw_word = false;
6433 while !self.at(TokenKind::RightParen) {
6434 if !self.at_word_like() {
6435 self.restore(checkpoint);
6436 return Ok(false);
6437 }
6438 saw_word = true;
6439 self.advance();
6440 }
6441 if !saw_word {
6442 self.restore(checkpoint);
6443 return Ok(false);
6444 }
6445 self.advance();
6446 Ok(self.at(TokenKind::LeftBrace))
6447 } else {
6448 if self.current_keyword() != Some(Keyword::In) {
6449 self.restore(checkpoint);
6450 return Ok(false);
6451 }
6452 self.advance();
6453
6454 let mut saw_word = false;
6455 let saw_separator = loop {
6456 if self.current_keyword() == Some(Keyword::Do) {
6457 break false;
6458 }
6459
6460 match self.current_token_kind {
6461 Some(kind) if kind.is_word_like() => {
6462 saw_word = true;
6463 self.advance();
6464 }
6465 Some(TokenKind::Semicolon) => {
6466 self.advance();
6467 break true;
6468 }
6469 Some(TokenKind::Newline) => {
6470 if let Err(error) = self.skip_newlines() {
6471 self.restore(checkpoint);
6472 return Err(error);
6473 }
6474 break true;
6475 }
6476 _ => break false,
6477 }
6478 };
6479
6480 Ok(saw_word && saw_separator && self.current_keyword() == Some(Keyword::Do))
6481 };
6482 self.restore(checkpoint);
6483 result
6484 }
6485
6486 fn skip_newlines_with_flag(&mut self) -> Result<bool> {
6487 let mut skipped = false;
6488 while self.at(TokenKind::Newline) {
6489 self.tick()?;
6490 self.advance();
6491 skipped = true;
6492 }
6493 Ok(skipped)
6494 }
6495
6496 fn skip_newlines(&mut self) -> Result<()> {
6497 self.skip_newlines_with_flag().map(|_| ())
6498 }
6499}
6500#[cfg(test)]
6501mod tests;