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