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