#![cfg_attr(not(test), warn(clippy::unwrap_used))]
mod arithmetic;
mod commands;
mod heredocs;
mod keywords;
mod lexer;
mod parser_state;
mod profile;
mod redirects;
mod result;
mod words;
mod zsh_options;
mod zsh_prescan;
use std::{
borrow::Cow,
collections::{HashMap, HashSet, VecDeque},
sync::Arc,
};
pub use lexer::{LexedToken, Lexer};
pub(crate) use lexer::{LexedWordSegment, LexedWordSegmentKind};
pub use profile::{ShellDialect, ShellProfile};
pub use result::{ParseDiagnostic, ParseResult, ParseStatus, SyntaxFacts, ZshCaseGroupPart};
pub use zsh_options::{OptionValue, ZshEmulationMode, ZshOptionState};
use keywords::*;
use memchr::{memchr, memchr2, memchr3};
pub use parser_state::Parser;
#[cfg(feature = "benchmarking")]
pub use parser_state::ParserBenchmarkCounters;
use parser_state::*;
use smallvec::SmallVec;
use zsh_prescan::ZshOptionTimeline;
use shuck_ast::{
AlwaysCommand, AnonymousFunctionCommand, AnonymousFunctionSurface, ArithmeticCommand,
ArithmeticExpansionSyntax, ArithmeticExpr, ArithmeticExprNode, ArithmeticForCommand,
ArithmeticLvalue, ArrayElem, ArrayExpr, ArrayKind, Assignment, AssignmentValue,
BackgroundOperator, BinaryCommand, BinaryOp, BourneParameterExpansion, BraceExpansionKind,
BraceQuoteContext, BraceSyntax, BraceSyntaxKind, BreakCommand as AstBreakCommand,
BuiltinCommand as AstBuiltinCommand, CaseCommand, CaseItem, CaseTerminator,
Command as AstCommand, CommandSubstitutionSyntax, Comment, CompoundCommand,
ConditionalBinaryExpr, ConditionalBinaryOp, ConditionalCommand, ConditionalExpr,
ConditionalParenExpr, ConditionalUnaryExpr, ConditionalUnaryOp,
ContinueCommand as AstContinueCommand, CoprocCommand, DeclClause as AstDeclClause, DeclOperand,
ExitCommand as AstExitCommand, File, ForCommand, ForSyntax, ForTarget, ForeachCommand,
ForeachSyntax, FunctionDef, FunctionHeader, FunctionHeaderEntry, Heredoc, HeredocBody,
HeredocBodyMode, HeredocBodyPart, HeredocBodyPartNode, HeredocDelimiter, IfCommand, IfSyntax,
LiteralText, Name, ParameterExpansion, ParameterExpansionSyntax, ParameterOp, Pattern,
PatternGroupKind, PatternPart, PatternPartNode, Position, PrefixMatchKind, Redirect,
RedirectKind, RedirectTarget, RepeatCommand, RepeatSyntax, ReturnCommand as AstReturnCommand,
SelectCommand, SimpleCommand as AstSimpleCommand, SourceText, Span, Stmt, StmtSeq,
StmtTerminator, Subscript, SubscriptInterpretation, SubscriptKind, SubscriptSelector, TextSize,
TimeCommand, TokenKind, UntilCommand, VarRef, WhileCommand, Word, WordPart, WordPartNode,
ZshDefaultingOp, ZshExpansionOperation, ZshExpansionTarget, ZshGlobQualifier,
ZshGlobQualifierGroup, ZshGlobQualifierKind, ZshGlobSegment, ZshInlineGlobControl, ZshModifier,
ZshParameterExpansion, ZshPatternOp, ZshQualifiedGlob, ZshReplacementOp, ZshTrimOp,
};
use crate::error::{Error, Result};
type WordPartBuffer = SmallVec<[WordPartNode; 2]>;
#[derive(Debug, Clone, Copy, Default)]
struct ZshGlobParseFeatures {
classic_qualifiers: bool,
extended_glob: bool,
ksh_groups: bool,
bare_groups: bool,
}
impl ZshGlobParseFeatures {
const fn zsh_word_parsing_enabled(self) -> bool {
self.classic_qualifiers || self.extended_glob || self.ksh_groups || self.bare_groups
}
}
const DEFAULT_MAX_AST_DEPTH: usize = 100;
const HARD_MAX_AST_DEPTH: usize = 100;
const SOURCE_TEXT_WORD_REPARSE_MAX_DEPTH: usize = 8;
const SOURCE_TEXT_PATTERN_REPARSE_MAX_DEPTH: usize = 4;
const DEFAULT_MAX_PARSER_OPERATIONS: usize = 100_000;
pub fn text_looks_like_nontrivial_arithmetic_expression(text: &str) -> bool {
let text = text.trim();
if text.is_empty() {
return false;
}
let source = format!("(( {text} ))");
let file = Parser::new(&source).parse();
if file.is_err() {
return false;
}
let Some(statement) = file.file.body.first() else {
return false;
};
let AstCommand::Compound(CompoundCommand::Arithmetic(command)) = &statement.command else {
return false;
};
command.expr_ast.as_ref().is_some_and(|expr| {
!matches!(
expr.kind,
ArithmeticExpr::Number(_) | ArithmeticExpr::Variable(_)
)
})
}
pub fn text_is_self_contained_arithmetic_expression(text: &str) -> bool {
let text = text.trim();
if text.is_empty() {
return false;
}
let source = format!("(( {text} ))");
let file = Parser::new(&source).parse();
if file.is_err() {
return false;
}
let Some(statement) = file.file.body.first() else {
return false;
};
let AstCommand::Compound(CompoundCommand::Arithmetic(command)) = &statement.command else {
return false;
};
command
.expr_ast
.as_ref()
.is_some_and(arithmetic_expr_is_self_contained)
}
fn arithmetic_expr_is_self_contained(expr: &ArithmeticExprNode) -> bool {
match &expr.kind {
ArithmeticExpr::Number(_) => true,
ArithmeticExpr::Variable(_)
| ArithmeticExpr::Indexed { .. }
| ArithmeticExpr::ShellWord(_)
| ArithmeticExpr::Assignment { .. } => false,
ArithmeticExpr::Parenthesized { expression } => {
arithmetic_expr_is_self_contained(expression)
}
ArithmeticExpr::Unary { expr, .. } | ArithmeticExpr::Postfix { expr, .. } => {
arithmetic_expr_is_self_contained(expr)
}
ArithmeticExpr::Binary { left, right, .. } => {
arithmetic_expr_is_self_contained(left) && arithmetic_expr_is_self_contained(right)
}
ArithmeticExpr::Conditional {
condition,
then_expr,
else_expr,
} => {
arithmetic_expr_is_self_contained(condition)
&& arithmetic_expr_is_self_contained(then_expr)
&& arithmetic_expr_is_self_contained(else_expr)
}
}
}
#[cfg(test)]
mod arithmetic_text_helper_tests {
use super::{
text_is_self_contained_arithmetic_expression,
text_looks_like_nontrivial_arithmetic_expression,
};
#[test]
fn requires_nontrivial_expressions() {
assert!(text_looks_like_nontrivial_arithmetic_expression("1 + 2"));
assert!(text_looks_like_nontrivial_arithmetic_expression("arr[1]"));
assert!(text_looks_like_nontrivial_arithmetic_expression("++count"));
assert!(!text_looks_like_nontrivial_arithmetic_expression("123"));
assert!(!text_looks_like_nontrivial_arithmetic_expression("name"));
assert!(!text_looks_like_nontrivial_arithmetic_expression(
"latest value"
));
}
#[test]
fn distinguishes_self_contained_expressions() {
assert!(text_is_self_contained_arithmetic_expression("1 + 2"));
assert!(text_is_self_contained_arithmetic_expression("(1 + 2)"));
assert!(!text_is_self_contained_arithmetic_expression("name"));
assert!(!text_is_self_contained_arithmetic_expression("arr[1]"));
assert!(!text_is_self_contained_arithmetic_expression("foo + 1"));
assert!(!text_is_self_contained_arithmetic_expression(
"latest value"
));
}
}
impl<'a> Parser<'a> {
pub fn new(input: &'a str) -> Self {
Self::with_limits_and_profile(
input,
DEFAULT_MAX_AST_DEPTH,
DEFAULT_MAX_PARSER_OPERATIONS,
ShellProfile::native(ShellDialect::Bash),
)
}
pub fn with_dialect(input: &'a str, dialect: ShellDialect) -> Self {
Self::with_profile(input, ShellProfile::native(dialect))
}
pub fn with_profile(input: &'a str, shell_profile: ShellProfile) -> Self {
Self::with_limits_and_profile(
input,
DEFAULT_MAX_AST_DEPTH,
DEFAULT_MAX_PARSER_OPERATIONS,
shell_profile,
)
}
pub fn with_max_depth(input: &'a str, max_depth: usize) -> Self {
Self::with_limits_and_profile(
input,
max_depth,
DEFAULT_MAX_PARSER_OPERATIONS,
ShellProfile::native(ShellDialect::Bash),
)
}
pub fn with_fuel(input: &'a str, max_fuel: usize) -> Self {
Self::with_limits_and_profile(
input,
DEFAULT_MAX_AST_DEPTH,
max_fuel,
ShellProfile::native(ShellDialect::Bash),
)
}
pub fn with_limits(input: &'a str, max_depth: usize, max_fuel: usize) -> Self {
Self::with_limits_and_profile(
input,
max_depth,
max_fuel,
ShellProfile::native(ShellDialect::Bash),
)
}
pub fn with_limits_and_dialect(
input: &'a str,
max_depth: usize,
max_fuel: usize,
dialect: ShellDialect,
) -> Self {
Self::with_limits_and_profile(input, max_depth, max_fuel, ShellProfile::native(dialect))
}
pub fn with_limits_and_profile(
input: &'a str,
max_depth: usize,
max_fuel: usize,
shell_profile: ShellProfile,
) -> Self {
Self::with_limits_and_profile_and_benchmarking(
input,
max_depth,
max_fuel,
shell_profile,
false,
)
}
fn with_limits_and_profile_and_benchmarking(
input: &'a str,
max_depth: usize,
max_fuel: usize,
shell_profile: ShellProfile,
benchmark_counters_enabled: bool,
) -> Self {
#[cfg(not(feature = "benchmarking"))]
let _ = benchmark_counters_enabled;
let zsh_timeline = (shell_profile.dialect == ShellDialect::Zsh)
.then(|| ZshOptionTimeline::build(input, &shell_profile))
.flatten()
.map(Arc::new);
let mut lexer = Lexer::with_max_subst_depth_and_profile(
input,
max_depth.min(HARD_MAX_AST_DEPTH),
&shell_profile,
zsh_timeline.clone(),
);
#[cfg(feature = "benchmarking")]
if benchmark_counters_enabled {
lexer.enable_benchmark_counters();
}
let mut comments = Vec::new();
let (current_token, current_token_kind, current_keyword, current_span) = loop {
match lexer.next_lexed_token_with_comments() {
Some(st) if st.kind == TokenKind::Comment => {
comments.push(Comment {
range: st.span.to_range(),
});
}
Some(st) => {
break (
Some(st.clone()),
Some(st.kind),
Self::keyword_from_token(&st),
st.span,
);
}
None => break (None, None, None, Span::new()),
}
};
Self {
input,
lexer,
synthetic_tokens: VecDeque::new(),
alias_replays: Vec::new(),
current_token,
current_word_cache: None,
current_token_kind,
current_keyword,
current_span,
peeked_token: None,
max_depth: max_depth.min(HARD_MAX_AST_DEPTH),
current_depth: 0,
fuel: max_fuel,
max_fuel,
source_text_pattern_depth: 0,
comments,
aliases: HashMap::new(),
expand_aliases: false,
expand_next_word: false,
brace_group_depth: 0,
brace_body_stack: Vec::new(),
syntax_facts: SyntaxFacts::default(),
dialect: shell_profile.dialect,
shell_profile,
zsh_timeline,
#[cfg(feature = "benchmarking")]
benchmark_counters: benchmark_counters_enabled.then(ParserBenchmarkCounters::default),
}
}
#[cfg(feature = "benchmarking")]
fn rebuild_with_benchmark_counters(&self) -> Self {
Self::with_limits_and_profile_and_benchmarking(
self.input,
self.max_depth,
self.max_fuel,
self.shell_profile.clone(),
true,
)
}
fn zsh_options_at_offset(&self, offset: usize) -> Option<&ZshOptionState> {
self.zsh_timeline
.as_ref()
.map(|timeline| timeline.options_at(offset))
.or_else(|| self.shell_profile.zsh_options())
}
fn current_zsh_options(&self) -> Option<&ZshOptionState> {
self.zsh_options_at_offset(self.current_span.start.offset)
}
fn zsh_short_loops_enabled(&self) -> bool {
self.dialect.features().zsh_foreach_loop
&& !self
.current_zsh_options()
.is_some_and(|options| options.short_loops.is_definitely_off())
}
fn zsh_short_repeat_enabled(&self) -> bool {
self.dialect.features().zsh_repeat_loop
&& !self
.current_zsh_options()
.is_some_and(|options| options.short_repeat.is_definitely_off())
}
fn zsh_brace_bodies_enabled(&self) -> bool {
self.dialect.features().zsh_brace_if
&& !self
.current_zsh_options()
.is_some_and(|options| options.ignore_braces.is_definitely_on())
}
fn zsh_brace_if_enabled(&self) -> bool {
self.zsh_brace_bodies_enabled()
}
fn zsh_glob_parse_features_at(&self, offset: usize) -> ZshGlobParseFeatures {
let options = self.zsh_options_at_offset(offset);
let is_zsh = self.dialect == ShellDialect::Zsh;
ZshGlobParseFeatures {
classic_qualifiers: self.dialect.features().zsh_glob_qualifiers
&& !options.is_some_and(|options| {
options.ignore_braces.is_definitely_on()
|| options.bare_glob_qual.is_definitely_off()
}),
extended_glob: is_zsh
&& !options.is_some_and(|options| options.extended_glob.is_definitely_off()),
ksh_groups: !is_zsh
|| !options.is_some_and(|options| options.ksh_glob.is_definitely_off()),
bare_groups: is_zsh
&& !options.is_some_and(|options| options.sh_glob.is_definitely_on()),
}
}
fn zsh_glob_word_parsing_enabled_at(&self, offset: usize) -> bool {
self.dialect == ShellDialect::Zsh
&& self
.zsh_glob_parse_features_at(offset)
.zsh_word_parsing_enabled()
}
fn brace_syntax_enabled_at(&self, offset: usize) -> bool {
!self.zsh_options_at_offset(offset).is_some_and(|options| {
options.ignore_braces.is_definitely_on()
|| options.ignore_close_braces.is_definitely_on()
})
}
fn brace_ccl_enabled_at(&self, offset: usize) -> bool {
self.zsh_options_at_offset(offset)
.is_some_and(|options| options.brace_ccl.is_definitely_on())
}
#[cfg(test)]
fn current_span(&self) -> Span {
self.current_span
}
pub fn parse_word_string(input: &str) -> Word {
let mut parser = Parser::new(input);
let start = Position::new();
parser.parse_word_with_context(
input,
Span::from_positions(start, start.advanced_by(input)),
start,
true,
)
}
pub fn parse_assignment_word_group(
source: &str,
words: &[&Word],
explicit_array_kind: Option<ArrayKind>,
subscript_interpretation: SubscriptInterpretation,
) -> Option<Assignment> {
let first = words.first()?;
let last = words.last()?;
let span = Span::from_positions(first.span.start, last.span.end);
let raw = span.slice(source);
let mut parser = Parser::new(source);
parser.parse_assignment_from_text(raw, span, explicit_array_kind, subscript_interpretation)
}
fn parse_word_string_with_limits_and_dialect(
input: &str,
max_depth: usize,
max_fuel: usize,
dialect: ShellDialect,
) -> Word {
let mut parser = Parser::with_limits_and_profile(
input,
max_depth,
max_fuel,
ShellProfile::native(dialect),
);
let start = Position::new();
parser.parse_word_with_context(
input,
Span::from_positions(start, start.advanced_by(input)),
start,
true,
)
}
#[cfg(test)]
fn parse_word_fragment(source: &str, text: &str, span: Span) -> Word {
Self::parse_word_fragment_with_limits(
source,
text,
span,
DEFAULT_MAX_AST_DEPTH,
DEFAULT_MAX_PARSER_OPERATIONS,
ShellProfile::native(ShellDialect::Bash),
)
}
fn parse_word_fragment_with_limits(
source: &str,
text: &str,
span: Span,
max_depth: usize,
max_fuel: usize,
shell_profile: ShellProfile,
) -> Word {
let mut parser = Parser::with_limits_and_profile(text, max_depth, max_fuel, shell_profile);
let source_backed = span.end.offset <= source.len() && span.slice(source) == text;
let start = Position::new();
let fragment_span = Span::from_positions(start, start.advanced_by(text));
let mut word = parser.parse_word_with_context(text, fragment_span, start, source_backed);
if !source_backed {
Self::materialize_word_source_backing(&mut word, text);
}
Self::rebase_word(&mut word, span.start);
word.span = span;
word
}
fn maybe_record_comment(&mut self, token: &LexedToken<'_>) {
if token.kind == TokenKind::Comment && !token.flags.is_synthetic() {
self.comments.push(Comment {
range: token.span.to_range(),
});
}
}
fn record_zsh_brace_if_span(&mut self, span: Span) {
if !self.syntax_facts.zsh_brace_if_spans.contains(&span) {
self.syntax_facts.zsh_brace_if_spans.push(span);
}
}
fn record_zsh_always_span(&mut self, span: Span) {
if !self.syntax_facts.zsh_always_spans.contains(&span) {
self.syntax_facts.zsh_always_spans.push(span);
}
}
fn record_zsh_case_group_part(&mut self, pattern_part_index: usize, span: Span) {
if !self
.syntax_facts
.zsh_case_group_parts
.iter()
.any(|fact| fact.pattern_part_index == pattern_part_index && fact.span == span)
{
self.syntax_facts
.zsh_case_group_parts
.push(ZshCaseGroupPart {
pattern_part_index,
span,
});
}
}
fn word_text_needs_parse(text: &str) -> bool {
memchr3(b'$', b'`', b'\0', text.as_bytes()).is_some()
}
fn word_with_parts(&self, parts: Vec<WordPartNode>, span: Span) -> Word {
let brace_syntax = self.brace_syntax_from_parts(&parts, span.start.offset);
Word {
parts,
span,
brace_syntax,
}
}
fn word_with_part_buffer(&self, parts: WordPartBuffer, span: Span) -> Word {
let brace_syntax = self.brace_syntax_from_parts(&parts, span.start.offset);
let parts = if parts.spilled() {
parts.into_vec()
} else {
let mut vec = Vec::with_capacity(parts.len());
vec.extend(parts);
vec
};
Word {
parts,
span,
brace_syntax,
}
}
fn word_with_single_part(&self, part: WordPartNode, span: Span) -> Word {
let mut parts = WordPartBuffer::new();
parts.push(part);
self.word_with_part_buffer(parts, span)
}
fn heredoc_body_with_parts(
&self,
parts: Vec<HeredocBodyPartNode>,
span: Span,
mode: HeredocBodyMode,
source_backed: bool,
) -> HeredocBody {
HeredocBody {
mode,
source_backed,
parts,
span,
}
}
fn heredoc_body_part_from_word_part_node(
&self,
part: WordPartNode,
source_backed: bool,
) -> HeredocBodyPartNode {
let span = part.span;
let kind = match part.kind {
WordPart::Literal(text) => HeredocBodyPart::Literal(text),
WordPart::Variable(name) => HeredocBodyPart::Variable(name),
WordPart::CommandSubstitution { body, syntax } => {
HeredocBodyPart::CommandSubstitution { body, syntax }
}
WordPart::ArithmeticExpansion {
expression,
expression_ast,
expression_word_ast,
syntax,
} => HeredocBodyPart::ArithmeticExpansion {
expression,
expression_ast: expression_ast.map(|ast| *ast),
expression_word_ast: *expression_word_ast,
syntax,
},
WordPart::Parameter(parameter) => HeredocBodyPart::Parameter(parameter),
part @ (WordPart::ParameterExpansion { .. }
| WordPart::Length(_)
| WordPart::ArrayAccess(_)
| WordPart::ArrayLength(_)
| WordPart::ArrayIndices(_)
| WordPart::Substring { .. }
| WordPart::ArraySlice { .. }
| WordPart::IndirectExpansion { .. }
| WordPart::PrefixMatch { .. }
| WordPart::Transformation { .. }) => {
match self.parameter_word_part_from_legacy(
part,
span.start,
span.end,
source_backed,
) {
WordPart::Parameter(parameter) => HeredocBodyPart::Parameter(parameter),
other => self.literal_heredoc_body_part_from_word_part(other, span),
}
}
other => self.literal_heredoc_body_part_from_word_part(other, span),
};
HeredocBodyPartNode::new(kind, span)
}
fn literal_heredoc_body_part_from_word_part(
&self,
part: WordPart,
span: Span,
) -> HeredocBodyPart {
if span.end.offset <= self.input.len() {
return HeredocBodyPart::Literal(LiteralText::source());
}
let mut syntax = String::new();
self.push_word_part_syntax(&mut syntax, &part, span);
HeredocBodyPart::Literal(LiteralText::owned(syntax))
}
fn brace_syntax_from_parts(&self, parts: &[WordPartNode], offset: usize) -> Vec<BraceSyntax> {
if !self.brace_syntax_enabled_at(offset) {
return Vec::new();
}
let brace_ccl_enabled = self.brace_ccl_enabled_at(offset);
let mut brace_syntax = Vec::new();
self.collect_brace_syntax_from_parts(
parts,
BraceQuoteContext::Unquoted,
brace_ccl_enabled,
&mut brace_syntax,
);
brace_syntax.sort_by_key(|brace| (brace.span.start.offset, brace.span.end.offset));
brace_syntax.dedup_by_key(|brace| {
(
brace.span.start.offset,
brace.span.end.offset,
brace.quote_context,
brace.kind,
)
});
brace_syntax
}
fn collect_brace_syntax_from_parts(
&self,
parts: &[WordPartNode],
quote_context: BraceQuoteContext,
brace_ccl_enabled: bool,
out: &mut Vec<BraceSyntax>,
) {
for part in parts {
match &part.kind {
WordPart::Literal(text) => Self::scan_brace_syntax_text(
text.syntax_str(self.input, part.span),
part.span.start,
quote_context,
brace_ccl_enabled,
out,
),
WordPart::SingleQuoted { .. } => Self::scan_brace_syntax_text(
part.span.slice(self.input),
part.span.start,
BraceQuoteContext::SingleQuoted,
brace_ccl_enabled,
out,
),
WordPart::DoubleQuoted { parts, .. } => self.collect_brace_syntax_from_parts(
parts,
BraceQuoteContext::DoubleQuoted,
brace_ccl_enabled,
out,
),
WordPart::ZshQualifiedGlob(glob) => self
.collect_brace_syntax_from_zsh_qualified_glob(
glob,
quote_context,
brace_ccl_enabled,
out,
),
WordPart::Variable(_)
| WordPart::CommandSubstitution { .. }
| WordPart::ArithmeticExpansion { .. }
| WordPart::Parameter(_)
| WordPart::ParameterExpansion { .. }
| WordPart::Length(_)
| WordPart::ArrayAccess(_)
| WordPart::ArrayLength(_)
| WordPart::ArrayIndices(_)
| WordPart::Substring { .. }
| WordPart::ArraySlice { .. }
| WordPart::IndirectExpansion { .. }
| WordPart::PrefixMatch { .. }
| WordPart::ProcessSubstitution { .. }
| WordPart::Transformation { .. } => {}
}
}
if self.needs_cross_part_brace_scan(parts) {
let mut chars = Vec::new();
self.collect_brace_scan_chars_from_parts(parts, &mut chars);
Self::scan_brace_syntax_chars(&chars, quote_context, brace_ccl_enabled, out);
}
}
fn needs_cross_part_brace_scan(&self, parts: &[WordPartNode]) -> bool {
if parts.len() < 2 {
return false;
}
let mut cursor = parts[0].span.start.offset;
for part in parts {
if part.span.start.offset > cursor
&& self
.input
.get(cursor..part.span.start.offset)
.is_some_and(|raw| raw.contains(['{', '}']))
{
return true;
}
let has_brace_text = match &part.kind {
WordPart::Literal(text) => {
text.syntax_str(self.input, part.span).contains(['{', '}'])
}
WordPart::SingleQuoted { .. }
| WordPart::DoubleQuoted { .. }
| WordPart::ZshQualifiedGlob(_) => self
.input
.get(part.span.start.offset..part.span.end.offset)
.is_some_and(|raw| raw.contains(['{', '}'])),
WordPart::Variable(_)
| WordPart::CommandSubstitution { .. }
| WordPart::ArithmeticExpansion { .. }
| WordPart::Parameter(_)
| WordPart::ParameterExpansion { .. }
| WordPart::Length(_)
| WordPart::ArrayAccess(_)
| WordPart::ArrayLength(_)
| WordPart::ArrayIndices(_)
| WordPart::Substring { .. }
| WordPart::ArraySlice { .. }
| WordPart::IndirectExpansion { .. }
| WordPart::PrefixMatch { .. }
| WordPart::ProcessSubstitution { .. }
| WordPart::Transformation { .. } => false,
};
if has_brace_text {
return true;
}
cursor = part.span.end.offset;
}
false
}
fn collect_brace_scan_chars_from_parts(
&self,
parts: &[WordPartNode],
out: &mut Vec<(char, Position)>,
) {
for part in parts {
match &part.kind {
WordPart::Literal(text) => {
Self::push_brace_scan_text(
text.syntax_str(self.input, part.span),
part.span.start,
out,
);
}
WordPart::SingleQuoted { .. } => {
if let Some(raw) = self.input.get(part.span.start.offset..part.span.end.offset)
{
Self::push_brace_scan_text(raw, part.span.start, out);
}
}
WordPart::DoubleQuoted { parts, .. } => {
self.collect_brace_scan_chars_from_double_quoted_part(part.span, parts, out);
}
WordPart::ZshQualifiedGlob(_) => {}
WordPart::Variable(_)
| WordPart::CommandSubstitution { .. }
| WordPart::ArithmeticExpansion { .. }
| WordPart::Parameter(_)
| WordPart::ParameterExpansion { .. }
| WordPart::Length(_)
| WordPart::ArrayAccess(_)
| WordPart::ArrayLength(_)
| WordPart::ArrayIndices(_)
| WordPart::Substring { .. }
| WordPart::ArraySlice { .. }
| WordPart::IndirectExpansion { .. }
| WordPart::PrefixMatch { .. }
| WordPart::ProcessSubstitution { .. }
| WordPart::Transformation { .. } => {
Self::push_brace_scan_boundary(part.span.start, out);
}
}
}
}
fn collect_brace_scan_chars_from_double_quoted_part(
&self,
span: Span,
parts: &[WordPartNode],
out: &mut Vec<(char, Position)>,
) {
let mut cursor_offset = span.start.offset;
let mut cursor_position = span.start;
for part in parts {
if part.span.start.offset > cursor_offset
&& let Some(raw) = self.input.get(cursor_offset..part.span.start.offset)
{
Self::push_brace_scan_text(raw, cursor_position, out);
}
match &part.kind {
WordPart::Literal(text) => {
Self::push_brace_scan_text(
text.syntax_str(self.input, part.span),
part.span.start,
out,
);
}
WordPart::SingleQuoted { .. } => {
if let Some(raw) = self.input.get(part.span.start.offset..part.span.end.offset)
{
Self::push_brace_scan_text(raw, part.span.start, out);
}
}
WordPart::DoubleQuoted { parts, .. } => {
self.collect_brace_scan_chars_from_double_quoted_part(part.span, parts, out);
}
WordPart::ZshQualifiedGlob(_) => {}
WordPart::Variable(_)
| WordPart::CommandSubstitution { .. }
| WordPart::ArithmeticExpansion { .. }
| WordPart::Parameter(_)
| WordPart::ParameterExpansion { .. }
| WordPart::Length(_)
| WordPart::ArrayAccess(_)
| WordPart::ArrayLength(_)
| WordPart::ArrayIndices(_)
| WordPart::Substring { .. }
| WordPart::ArraySlice { .. }
| WordPart::IndirectExpansion { .. }
| WordPart::PrefixMatch { .. }
| WordPart::ProcessSubstitution { .. }
| WordPart::Transformation { .. } => {
Self::push_brace_scan_boundary(part.span.start, out);
}
}
cursor_offset = part.span.end.offset;
cursor_position = part.span.end;
}
if cursor_offset < span.end.offset
&& let Some(raw) = self.input.get(cursor_offset..span.end.offset)
{
Self::push_brace_scan_text(raw, cursor_position, out);
}
}
fn push_brace_scan_text(text: &str, start: Position, out: &mut Vec<(char, Position)>) {
let mut position = start;
for ch in text.chars() {
out.push((ch, position));
position.advance(ch);
}
}
fn push_brace_scan_boundary(position: Position, out: &mut Vec<(char, Position)>) {
out.push(('\0', position));
}
fn scan_brace_syntax_chars(
chars: &[(char, Position)],
initial_quote_context: BraceQuoteContext,
brace_ccl_enabled: bool,
out: &mut Vec<BraceSyntax>,
) {
#[derive(Clone, Copy, PartialEq, Eq)]
enum QuoteState {
Single,
AnsiSingle,
Double,
}
#[derive(Clone, Copy)]
struct Candidate {
start: Position,
quote_context: BraceQuoteContext,
has_comma: bool,
has_dot_dot: bool,
saw_unquoted_whitespace: bool,
has_brace_ccl_content: bool,
saw_quote_boundary: bool,
prev_char: Option<char>,
}
fn quote_state(context: BraceQuoteContext) -> Option<QuoteState> {
match context {
BraceQuoteContext::Unquoted => None,
BraceQuoteContext::SingleQuoted => Some(QuoteState::Single),
BraceQuoteContext::DoubleQuoted => Some(QuoteState::Double),
}
}
fn quote_context(state: Option<QuoteState>) -> BraceQuoteContext {
match state {
None => BraceQuoteContext::Unquoted,
Some(QuoteState::Single | QuoteState::AnsiSingle) => {
BraceQuoteContext::SingleQuoted
}
Some(QuoteState::Double) => BraceQuoteContext::DoubleQuoted,
}
}
fn quote_context_is_active(context: BraceQuoteContext, state: Option<QuoteState>) -> bool {
match context {
BraceQuoteContext::Unquoted => state.is_none(),
BraceQuoteContext::SingleQuoted => {
matches!(state, Some(QuoteState::Single | QuoteState::AnsiSingle))
}
BraceQuoteContext::DoubleQuoted => matches!(state, Some(QuoteState::Double)),
}
}
fn last_active_candidate_index(
stack: &[Candidate],
state: Option<QuoteState>,
) -> Option<usize> {
stack
.iter()
.rposition(|candidate| quote_context_is_active(candidate.quote_context, state))
}
fn mark_brace_ccl_content(stack: &mut [Candidate], state: Option<QuoteState>) {
if let Some(candidate_index) = last_active_candidate_index(stack, state) {
stack[candidate_index].has_brace_ccl_content = true;
}
}
fn template_placeholder_end(
chars: &[(char, Position)],
start: usize,
quote_context: BraceQuoteContext,
) -> Option<usize> {
if chars.get(start)?.0 != '{' || chars.get(start + 1)?.0 != '{' {
return None;
}
let mut index = start + 2;
let mut depth = 1usize;
while index < chars.len() {
if matches!(quote_context, BraceQuoteContext::Unquoted) && chars[index].0 == '\\' {
index += 1;
if index < chars.len() {
index += 1;
}
continue;
}
if chars.get(index).is_some_and(|(ch, _)| *ch == '{')
&& chars.get(index + 1).is_some_and(|(ch, _)| *ch == '{')
{
depth += 1;
index += 2;
continue;
}
if chars.get(index).is_some_and(|(ch, _)| *ch == '}')
&& chars.get(index + 1).is_some_and(|(ch, _)| *ch == '}')
{
depth -= 1;
index += 2;
if depth == 0 {
return Some(index);
}
continue;
}
index += 1;
}
None
}
fn brace_span(start: Position, end: (char, Position)) -> Span {
let mut end_position = end.1;
end_position.advance(end.0);
Span::from_positions(start, end_position)
}
let mut index = 0usize;
let mut quote_state = quote_state(initial_quote_context);
let mut stack = Vec::<Candidate>::new();
while index < chars.len() {
let ch = chars[index].0;
if matches!(
quote_state,
None | Some(QuoteState::AnsiSingle) | Some(QuoteState::Double)
) && ch == '\\'
{
if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state) {
if index + 1 < chars.len() {
stack[candidate_index].has_brace_ccl_content = true;
}
stack[candidate_index].prev_char = None;
}
index += 1;
if index < chars.len() {
index += 1;
}
continue;
}
let current_quote_context = quote_context(quote_state);
if ch == '{'
&& let Some(end_index) =
template_placeholder_end(chars, index, current_quote_context)
{
out.push(BraceSyntax {
kind: BraceSyntaxKind::TemplatePlaceholder,
span: brace_span(chars[index].1, chars[end_index - 1]),
quote_context: current_quote_context,
});
if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state) {
stack[candidate_index].prev_char = None;
}
index = end_index;
continue;
}
match quote_state {
None => match ch {
'\'' => {
quote_state = if index > 0 && chars[index - 1].0 == '$' {
Some(QuoteState::AnsiSingle)
} else {
Some(QuoteState::Single)
};
for candidate in &mut stack {
if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
candidate.saw_quote_boundary = true;
}
}
}
'"' => {
quote_state = Some(QuoteState::Double);
for candidate in &mut stack {
if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
candidate.saw_quote_boundary = true;
}
}
}
_ => {}
},
Some(QuoteState::Single) => {
if ch == '\'' {
quote_state = None;
for candidate in &mut stack {
if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
candidate.saw_quote_boundary = true;
}
}
}
}
Some(QuoteState::AnsiSingle) => {
if ch == '\'' {
quote_state = None;
for candidate in &mut stack {
if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
candidate.saw_quote_boundary = true;
}
}
}
}
Some(QuoteState::Double) => {
if ch == '"' {
quote_state = None;
for candidate in &mut stack {
if matches!(candidate.quote_context, BraceQuoteContext::Unquoted) {
candidate.saw_quote_boundary = true;
}
}
}
}
}
match ch {
'{' => {
mark_brace_ccl_content(&mut stack, quote_state);
stack.push(Candidate {
start: chars[index].1,
quote_context: current_quote_context,
has_comma: false,
has_dot_dot: false,
saw_unquoted_whitespace: false,
has_brace_ccl_content: false,
saw_quote_boundary: false,
prev_char: Some(ch),
});
index += 1;
continue;
}
'}' => {
if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state)
{
let candidate = stack.remove(candidate_index);
let span = brace_span(candidate.start, chars[index]);
let kind = Self::classify_brace_construct_kind(
candidate.quote_context,
brace_ccl_enabled,
candidate.has_comma,
candidate.has_dot_dot,
candidate.saw_unquoted_whitespace,
candidate.has_brace_ccl_content,
);
if !matches!(kind, BraceSyntaxKind::Literal)
|| !candidate.saw_quote_boundary
{
out.push(BraceSyntax {
kind,
span,
quote_context: candidate.quote_context,
});
}
}
}
_ => {
let is_quote_syntax = match quote_state {
None => {
matches!(ch, '\'' | '"')
|| (ch == '$'
&& chars.get(index + 1).is_some_and(|(next, _)| *next == '\''))
}
Some(QuoteState::Single | QuoteState::AnsiSingle) => ch == '\'',
Some(QuoteState::Double) => ch == '"',
};
if ch != '\0' && !is_quote_syntax {
mark_brace_ccl_content(&mut stack, quote_state);
}
if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state)
{
let candidate = &mut stack[candidate_index];
let counts_as_top_level =
quote_context_is_active(candidate.quote_context, quote_state);
if counts_as_top_level {
match ch {
',' => candidate.has_comma = true,
'.' if candidate.prev_char == Some('.') => {
candidate.has_dot_dot = true;
}
c if matches!(
candidate.quote_context,
BraceQuoteContext::Unquoted
) && quote_state.is_none()
&& c.is_whitespace() =>
{
candidate.saw_unquoted_whitespace = true;
}
_ => {}
}
}
}
}
}
if let Some(candidate_index) = last_active_candidate_index(&stack, quote_state) {
stack[candidate_index].prev_char = Some(ch);
}
index += 1;
}
}
fn collect_brace_syntax_from_pattern(
&self,
pattern: &Pattern,
quote_context: BraceQuoteContext,
brace_ccl_enabled: bool,
out: &mut Vec<BraceSyntax>,
) {
for (part, span) in pattern.parts_with_spans() {
match part {
PatternPart::Literal(text) => Self::scan_brace_syntax_text(
text.as_str(self.input, span),
span.start,
quote_context,
brace_ccl_enabled,
out,
),
PatternPart::CharClass(text) => Self::scan_brace_syntax_text(
text.slice(self.input),
text.span().start,
quote_context,
brace_ccl_enabled,
out,
),
PatternPart::Group { patterns, .. } => {
for pattern in patterns {
self.collect_brace_syntax_from_pattern(
pattern,
quote_context,
brace_ccl_enabled,
out,
);
}
}
PatternPart::Word(word) => self.collect_brace_syntax_from_parts(
&word.parts,
quote_context,
brace_ccl_enabled,
out,
),
PatternPart::AnyString | PatternPart::AnyChar => {}
}
}
}
fn collect_brace_syntax_from_zsh_qualified_glob(
&self,
glob: &ZshQualifiedGlob,
quote_context: BraceQuoteContext,
brace_ccl_enabled: bool,
out: &mut Vec<BraceSyntax>,
) {
for segment in &glob.segments {
if let ZshGlobSegment::Pattern(pattern) = segment {
self.collect_brace_syntax_from_pattern(
pattern,
quote_context,
brace_ccl_enabled,
out,
);
}
}
}
fn scan_brace_syntax_text(
text: &str,
base: Position,
quote_context: BraceQuoteContext,
brace_ccl_enabled: bool,
out: &mut Vec<BraceSyntax>,
) {
if memchr(b'{', text.as_bytes()).is_none() {
return;
}
#[derive(Clone, Copy)]
struct ScanFrame<'a> {
text: &'a str,
index: usize,
position: Position,
}
let mut work = SmallVec::<[ScanFrame<'_>; 2]>::new();
work.push(ScanFrame {
text,
index: 0,
position: base,
});
while let Some(mut frame) = work.pop() {
let bytes = frame.text.as_bytes();
while frame.index < bytes.len() {
let next_special = if matches!(quote_context, BraceQuoteContext::Unquoted) {
memchr2(b'{', b'\\', &bytes[frame.index..])
.map(|relative| frame.index + relative)
} else {
memchr(b'{', &bytes[frame.index..]).map(|relative| frame.index + relative)
};
let Some(next_index) = next_special else {
break;
};
if next_index > frame.index {
frame.position = frame
.position
.advanced_by(&frame.text[frame.index..next_index]);
frame.index = next_index;
}
if matches!(quote_context, BraceQuoteContext::Unquoted)
&& bytes[frame.index] == b'\\'
{
let escaped_start = frame.index;
frame.index += 1;
if let Some(next) = frame.text[frame.index..].chars().next() {
frame.index += next.len_utf8();
}
frame.position = frame
.position
.advanced_by(&frame.text[escaped_start..frame.index]);
continue;
}
let brace_start = frame.position;
if let Some(len) =
Self::template_placeholder_len(frame.text, frame.index, quote_context)
{
let brace_end =
brace_start.advanced_by(&frame.text[frame.index..frame.index + len]);
out.push(BraceSyntax {
kind: BraceSyntaxKind::TemplatePlaceholder,
span: Span::from_positions(brace_start, brace_end),
quote_context,
});
frame.position = brace_end;
frame.index += len;
continue;
}
if let Some((len, kind)) = Self::brace_construct_len(
frame.text,
frame.index,
quote_context,
brace_ccl_enabled,
) {
let brace_end =
brace_start.advanced_by(&frame.text[frame.index..frame.index + len]);
out.push(BraceSyntax {
kind,
span: Span::from_positions(brace_start, brace_end),
quote_context,
});
frame.position = brace_end;
frame.index += len;
if len > 2 {
let inner_start = frame.index - len + '{'.len_utf8();
let inner_end = frame.index - '}'.len_utf8();
if inner_start < inner_end {
let inner_base = brace_start.advanced_by("{");
work.push(frame);
work.push(ScanFrame {
text: &frame.text[inner_start..inner_end],
index: 0,
position: inner_base,
});
break;
}
}
continue;
}
frame.position.advance('{');
frame.index += '{'.len_utf8();
}
}
}
fn text_position(base: Position, text: &str, offset: usize) -> Position {
base.advanced_by(&text[..offset])
}
fn template_placeholder_len(
text: &str,
start: usize,
quote_context: BraceQuoteContext,
) -> Option<usize> {
text.get(start..).filter(|rest| rest.starts_with("{{"))?;
let mut index = start + "{{".len();
let mut depth = 1usize;
while index < text.len() {
if matches!(quote_context, BraceQuoteContext::Unquoted)
&& text[index..].starts_with('\\')
{
index += 1;
if let Some(next) = text[index..].chars().next() {
index += next.len_utf8();
}
continue;
}
if text[index..].starts_with("{{") {
depth += 1;
index += "{{".len();
continue;
}
if text[index..].starts_with("}}") {
depth -= 1;
index += "}}".len();
if depth == 0 {
return Some(index - start);
}
continue;
}
index += text[index..].chars().next()?.len_utf8();
}
None
}
fn brace_construct_len(
text: &str,
start: usize,
quote_context: BraceQuoteContext,
brace_ccl_enabled: bool,
) -> Option<(usize, BraceSyntaxKind)> {
text.get(start..).filter(|rest| rest.starts_with('{'))?;
#[derive(Clone, Copy, PartialEq, Eq)]
enum QuoteState {
Single,
Double,
}
let mut index = start + '{'.len_utf8();
let mut depth = 1usize;
let mut has_comma = false;
let mut has_dot_dot = false;
let mut saw_unquoted_whitespace = false;
let mut has_brace_ccl_content = false;
let mut prev_char = None;
let mut quote_state = None;
while index < text.len() {
if matches!(quote_context, BraceQuoteContext::Unquoted)
&& quote_state.is_none()
&& text[index..].starts_with('\\')
{
index += 1;
if let Some(next) = text[index..].chars().next() {
if depth == 1 {
has_brace_ccl_content = true;
}
index += next.len_utf8();
}
prev_char = None;
continue;
}
let ch = text[index..].chars().next()?;
index += ch.len_utf8();
if matches!(quote_context, BraceQuoteContext::Unquoted) {
match quote_state {
None => match ch {
'\'' => {
quote_state = Some(QuoteState::Single);
prev_char = None;
continue;
}
'"' => {
quote_state = Some(QuoteState::Double);
prev_char = None;
continue;
}
'$' if text[index..].starts_with('\'') => {}
'{' => {
has_brace_ccl_content = true;
depth += 1;
}
'}' => {
depth -= 1;
if depth == 0 {
let kind = Self::classify_brace_construct_kind(
quote_context,
brace_ccl_enabled,
has_comma,
has_dot_dot,
saw_unquoted_whitespace,
has_brace_ccl_content,
);
return Some((index - start, kind));
}
}
',' if depth == 1 => has_comma = true,
'.' if depth == 1 && prev_char == Some('.') => has_dot_dot = true,
c if c.is_whitespace() => saw_unquoted_whitespace = true,
_ => has_brace_ccl_content = true,
},
Some(QuoteState::Single) => {
if ch == '\'' {
quote_state = None;
} else {
has_brace_ccl_content = true;
}
}
Some(QuoteState::Double) => match ch {
'\\' => {
if let Some(next) = text[index..].chars().next() {
has_brace_ccl_content = true;
index += next.len_utf8();
}
prev_char = None;
continue;
}
'"' => quote_state = None,
_ => has_brace_ccl_content = true,
},
}
} else {
match ch {
'{' => {
has_brace_ccl_content = true;
depth += 1;
}
'}' => {
depth -= 1;
if depth == 0 {
let kind = Self::classify_brace_construct_kind(
quote_context,
brace_ccl_enabled,
has_comma,
has_dot_dot,
false,
has_brace_ccl_content,
);
return Some((index - start, kind));
}
}
',' if depth == 1 => has_comma = true,
'.' if depth == 1 && prev_char == Some('.') => has_dot_dot = true,
_ => has_brace_ccl_content = true,
}
}
prev_char = Some(ch);
}
None
}
fn classify_brace_construct_kind(
quote_context: BraceQuoteContext,
brace_ccl_enabled: bool,
has_comma: bool,
has_dot_dot: bool,
saw_unquoted_whitespace: bool,
has_brace_ccl_content: bool,
) -> BraceSyntaxKind {
if matches!(quote_context, BraceQuoteContext::Unquoted) && saw_unquoted_whitespace {
BraceSyntaxKind::Literal
} else if has_comma {
BraceSyntaxKind::Expansion(BraceExpansionKind::CommaList)
} else if has_dot_dot {
BraceSyntaxKind::Expansion(BraceExpansionKind::Sequence)
} else if brace_ccl_enabled && has_brace_ccl_content {
BraceSyntaxKind::Expansion(BraceExpansionKind::CharacterClass)
} else {
BraceSyntaxKind::Literal
}
}
fn maybe_parse_zsh_qualified_glob_word(
&mut self,
text: &str,
span: Span,
source_backed: bool,
) -> Option<Word> {
let features = self.zsh_glob_parse_features_at(span.start.offset);
if !self.zsh_glob_word_parsing_enabled_at(span.start.offset)
|| text.is_empty()
|| text.contains('=')
|| text.contains(['\x00', '\\', '\'', '"', '$', '`'])
|| text.chars().any(char::is_whitespace)
{
return None;
}
let (segments, qualifiers, saw_glob_syntax) =
self.parse_zsh_qualified_glob_segments(text, span, source_backed, features)?;
if !saw_glob_syntax {
return None;
}
Some(self.word_with_parts(
vec![WordPartNode::new(
WordPart::ZshQualifiedGlob(ZshQualifiedGlob {
span,
segments,
qualifiers,
}),
span,
)],
span,
))
}
fn parse_zsh_qualified_glob_segments(
&mut self,
text: &str,
span: Span,
source_backed: bool,
features: ZshGlobParseFeatures,
) -> Option<(Vec<ZshGlobSegment>, Option<ZshGlobQualifierGroup>, bool)> {
let mut segments = Vec::new();
let mut qualifiers = None;
let mut saw_glob_syntax = false;
let mut pattern_start = 0usize;
let mut index = 0usize;
while index < text.len() {
if features.extended_glob && text[index..].starts_with("(#") {
if let Some((len, control)) =
self.parse_zsh_inline_glob_control(text, span.start, index)
{
self.push_zsh_pattern_segment(
&mut segments,
text,
span.start,
(pattern_start, index),
source_backed,
features,
);
segments.push(ZshGlobSegment::InlineControl(control));
saw_glob_syntax = true;
index += len;
pattern_start = index;
continue;
}
let suffix_start = Self::text_position(span.start, text, index);
if let Some(group) = self.parse_zsh_terminal_glob_qualifier_group(
&text[index..],
suffix_start,
source_backed,
) {
self.push_zsh_pattern_segment(
&mut segments,
text,
span.start,
(pattern_start, index),
source_backed,
features,
);
qualifiers = Some(group);
saw_glob_syntax = true;
index = text.len();
pattern_start = index;
break;
}
}
if features.classic_qualifiers && text[index..].starts_with('(') {
let suffix_start = Self::text_position(span.start, text, index);
if let Some(group) = self.parse_zsh_terminal_glob_qualifier_group(
&text[index..],
suffix_start,
source_backed,
) && matches!(group.kind, ZshGlobQualifierKind::Classic)
{
self.push_zsh_pattern_segment(
&mut segments,
text,
span.start,
(pattern_start, index),
source_backed,
features,
);
qualifiers = Some(group);
saw_glob_syntax = true;
index = text.len();
pattern_start = index;
break;
}
}
index += text[index..].chars().next()?.len_utf8();
}
saw_glob_syntax |= self.push_zsh_pattern_segment(
&mut segments,
text,
span.start,
(pattern_start, text.len()),
source_backed,
features,
);
segments
.iter()
.any(|segment| matches!(segment, ZshGlobSegment::Pattern(_)))
.then_some((segments, qualifiers, saw_glob_syntax))
}
fn push_zsh_pattern_segment(
&mut self,
segments: &mut Vec<ZshGlobSegment>,
text: &str,
base: Position,
bounds: (usize, usize),
source_backed: bool,
features: ZshGlobParseFeatures,
) -> bool {
let (start, end) = bounds;
if start >= end {
return false;
}
let span = Span::from_positions(
Self::text_position(base, text, start),
Self::text_position(base, text, end),
);
let pattern_word =
self.decode_word_text(&text[start..end], span, span.start, source_backed);
let pattern = self.pattern_from_word(&pattern_word);
let saw_glob_syntax = self.pattern_has_glob_syntax_with_features(&pattern, features);
segments.push(ZshGlobSegment::Pattern(pattern));
saw_glob_syntax
}
fn parse_zsh_inline_glob_control(
&self,
text: &str,
base: Position,
start: usize,
) -> Option<(usize, ZshInlineGlobControl)> {
let (len, control) = if text[start..].starts_with("(#i)") {
(
"(#i)".len(),
ZshInlineGlobControl::CaseInsensitive {
span: Span::from_positions(
Self::text_position(base, text, start),
Self::text_position(base, text, start + "(#i)".len()),
),
},
)
} else if text[start..].starts_with("(#b)") {
(
"(#b)".len(),
ZshInlineGlobControl::Backreferences {
span: Span::from_positions(
Self::text_position(base, text, start),
Self::text_position(base, text, start + "(#b)".len()),
),
},
)
} else if text[start..].starts_with("(#s)") {
(
"(#s)".len(),
ZshInlineGlobControl::StartAnchor {
span: Span::from_positions(
Self::text_position(base, text, start),
Self::text_position(base, text, start + "(#s)".len()),
),
},
)
} else if text[start..].starts_with("(#e)") {
(
"(#e)".len(),
ZshInlineGlobControl::EndAnchor {
span: Span::from_positions(
Self::text_position(base, text, start),
Self::text_position(base, text, start + "(#e)".len()),
),
},
)
} else {
return None;
};
Some((len, control))
}
fn parse_zsh_terminal_glob_qualifier_group(
&self,
text: &str,
base: Position,
source_backed: bool,
) -> Option<ZshGlobQualifierGroup> {
let (kind, prefix_len, inner) = if let Some(inner) = text
.strip_prefix("(#q")
.and_then(|rest| rest.strip_suffix(')'))
{
(ZshGlobQualifierKind::HashQ, "(#q".len(), inner)
} else {
let inner = text.strip_prefix('(')?.strip_suffix(')')?;
(ZshGlobQualifierKind::Classic, "(".len(), inner)
};
let fragments = self.parse_zsh_glob_qualifier_fragments(
inner,
Self::text_position(base, text, prefix_len),
source_backed,
)?;
Some(ZshGlobQualifierGroup {
span: Span::from_positions(base, Self::text_position(base, text, text.len())),
kind,
fragments,
})
}
fn parse_zsh_glob_qualifier_fragments(
&self,
text: &str,
base: Position,
source_backed: bool,
) -> Option<Vec<ZshGlobQualifier>> {
let mut fragments = Vec::new();
let mut index = 0;
let mut saw_non_letter_fragment = false;
while index < text.len() {
let start = index;
let ch = text[index..].chars().next()?;
match ch {
'^' => {
index += ch.len_utf8();
fragments.push(ZshGlobQualifier::Negation {
span: Span::from_positions(
Self::text_position(base, text, start),
Self::text_position(base, text, index),
),
});
saw_non_letter_fragment = true;
}
'.' | '/' | '-' | 'A'..='Z' => {
index += ch.len_utf8();
fragments.push(ZshGlobQualifier::Flag {
name: ch,
span: Span::from_positions(
Self::text_position(base, text, start),
Self::text_position(base, text, index),
),
});
saw_non_letter_fragment = true;
}
'[' => {
index += ch.len_utf8();
let number_start = index;
while matches!(text[index..].chars().next(), Some('0'..='9')) {
index += 1;
}
if number_start == index {
return None;
}
let start_text = self.zsh_glob_qualifier_source_text(
text,
base,
number_start,
index,
source_backed,
);
let end_text = if text[index..].starts_with(',') {
index += 1;
let range_start = index;
while matches!(text[index..].chars().next(), Some('0'..='9')) {
index += 1;
}
if range_start == index {
return None;
}
Some(self.zsh_glob_qualifier_source_text(
text,
base,
range_start,
index,
source_backed,
))
} else {
None
};
if !text[index..].starts_with(']') {
return None;
}
index += "]".len();
fragments.push(ZshGlobQualifier::NumericArgument {
span: Span::from_positions(
Self::text_position(base, text, start),
Self::text_position(base, text, index),
),
start: start_text,
end: end_text,
});
saw_non_letter_fragment = true;
}
'a'..='z' => {
index += ch.len_utf8();
while matches!(text[index..].chars().next(), Some('a'..='z' | 'A'..='Z')) {
index += 1;
}
if index - start <= 1 {
return None;
}
fragments.push(ZshGlobQualifier::LetterSequence {
text: self.zsh_glob_qualifier_source_text(
text,
base,
start,
index,
source_backed,
),
span: Span::from_positions(
Self::text_position(base, text, start),
Self::text_position(base, text, index),
),
});
}
_ => return None,
}
}
(!fragments.is_empty() && saw_non_letter_fragment).then_some(fragments)
}
fn zsh_glob_qualifier_source_text(
&self,
text: &str,
base: Position,
start: usize,
end: usize,
source_backed: bool,
) -> SourceText {
let span = Span::from_positions(
Self::text_position(base, text, start),
Self::text_position(base, text, end),
);
if source_backed {
SourceText::source(span)
} else {
SourceText::cooked(span, text[start..end].to_string())
}
}
fn pattern_has_glob_syntax_with_features(
&self,
pattern: &Pattern,
features: ZshGlobParseFeatures,
) -> bool {
pattern.parts.iter().any(|part| match &part.kind {
PatternPart::AnyString | PatternPart::AnyChar | PatternPart::CharClass(_) => true,
PatternPart::Group { .. } => true,
PatternPart::Word(word) => Self::pattern_has_glob_word(word),
PatternPart::Literal(text) => {
Self::literal_text_has_zsh_glob_syntax(text.as_str(self.input, part.span), features)
}
})
}
fn pattern_has_glob_word(word: &Word) -> bool {
word.parts
.iter()
.any(|part| !matches!(part.kind, WordPart::Literal(_)))
}
fn literal_text_has_zsh_glob_syntax(text: &str, features: ZshGlobParseFeatures) -> bool {
if text.is_empty() {
return false;
}
if features.extended_glob && text.contains("(#") {
return true;
}
let bytes = text.as_bytes();
let mut escaped = false;
let mut bracket_depth = 0usize;
let mut previous_char = None;
let mut index = 0usize;
while index < bytes.len() {
let Some(ch) = text[index..].chars().next() else {
break;
};
if escaped {
escaped = false;
previous_char = Some(ch);
index += ch.len_utf8();
continue;
}
if ch == '\\' {
escaped = true;
previous_char = Some(ch);
index += ch.len_utf8();
continue;
}
if bracket_depth == 0 {
if features.extended_glob
&& (ch == '~'
|| ch == '#'
|| (ch == '^'
&& previous_char.is_none_or(|prev| prev == '(' || prev == '|')))
{
return true;
}
if features.bare_groups
&& ch == '<'
&& Self::literal_text_has_numeric_range_suffix(&text[index..])
{
return true;
}
}
match ch {
'[' => bracket_depth += 1,
']' if bracket_depth > 0 => bracket_depth -= 1,
_ => {}
}
previous_char = Some(ch);
index += ch.len_utf8();
}
false
}
fn literal_text_has_numeric_range_suffix(text: &str) -> bool {
let Some(rest) = text.strip_prefix('<') else {
return false;
};
let mut saw_body = false;
let mut saw_hyphen = false;
for ch in rest.chars() {
if ch == '>' {
return saw_body && saw_hyphen;
}
if !matches!(ch, '0'..='9' | '-') {
return false;
}
saw_hyphen |= ch == '-';
saw_body = true;
}
false
}
fn simple_word_from_token(&mut self, token: &LexedToken<'_>, span: Span) -> Option<Word> {
let word = token.word()?;
let source_backed = !token.flags.is_synthetic();
if self.zsh_glob_word_parsing_enabled_at(span.start.offset)
&& let Some(segment) = word.single_segment()
&& segment.kind() == LexedWordSegmentKind::Plain
&& let Some(word) = self.maybe_parse_zsh_qualified_glob_word(
segment.as_str(),
span,
segment.span().is_some() && source_backed && segment.text_is_source_backed(),
)
{
return Some(word);
}
let mut parts = Self::word_part_buffer_with_capacity(word.segments().size_hint().0);
for segment in word.segments() {
let source_backed = segment.span().is_some() && !token.flags.is_synthetic();
let content_span = Self::segment_content_span(segment, span);
let raw_text = segment.as_str();
let use_source_slice = source_backed
&& match segment.kind() {
LexedWordSegmentKind::Plain => {
segment.text_is_source_backed()
|| raw_text.contains("${") && raw_text.contains('/')
|| !raw_text.contains("$(")
}
_ => segment.text_is_source_backed(),
};
let text = if use_source_slice {
content_span.slice(self.input)
} else {
raw_text
};
match segment.kind() {
LexedWordSegmentKind::Plain
| LexedWordSegmentKind::DoubleQuoted
| LexedWordSegmentKind::DollarDoubleQuoted
if Self::word_text_needs_parse(text) =>
{
return None;
}
LexedWordSegmentKind::Plain
| LexedWordSegmentKind::SingleQuoted
| LexedWordSegmentKind::DollarSingleQuoted
| LexedWordSegmentKind::DoubleQuoted
| LexedWordSegmentKind::DollarDoubleQuoted => {}
LexedWordSegmentKind::Composite => return None,
}
let wrapper_span = Self::segment_wrapper_span(segment, span);
let part = match segment.kind() {
LexedWordSegmentKind::Plain => {
self.literal_part_from_text(text, content_span, source_backed)
}
LexedWordSegmentKind::SingleQuoted => {
self.single_quoted_part_from_text(text, content_span, wrapper_span, false)
}
LexedWordSegmentKind::DollarSingleQuoted => {
self.single_quoted_part_from_text(text, content_span, wrapper_span, true)
}
LexedWordSegmentKind::DoubleQuoted => self.double_quoted_literal_part_from_text(
text,
content_span,
wrapper_span,
source_backed,
false,
),
LexedWordSegmentKind::DollarDoubleQuoted => self
.double_quoted_literal_part_from_text(
text,
content_span,
wrapper_span,
source_backed,
true,
),
LexedWordSegmentKind::Composite => unreachable!(),
};
Self::push_word_part_node(&mut parts, part);
}
Some(self.word_with_part_buffer(parts, span))
}
fn segment_content_span(segment: &LexedWordSegment<'_>, fallback: Span) -> Span {
segment
.span()
.or_else(|| segment.wrapper_span())
.unwrap_or(fallback)
}
fn segment_wrapper_span(segment: &LexedWordSegment<'_>, fallback: Span) -> Span {
segment
.wrapper_span()
.or_else(|| segment.span())
.unwrap_or(fallback)
}
fn literal_part_from_text(&self, text: &str, span: Span, source_backed: bool) -> WordPartNode {
WordPartNode::new(
WordPart::Literal(self.literal_text_from_str(
text,
span.start,
span.end,
source_backed,
)),
span,
)
}
fn single_quoted_part_from_text(
&self,
text: &str,
content_span: Span,
wrapper_span: Span,
dollar: bool,
) -> WordPartNode {
WordPartNode::new(
WordPart::SingleQuoted {
value: self.source_text_from_str(text, content_span.start, content_span.end),
dollar,
},
wrapper_span,
)
}
fn double_quoted_literal_part_from_text(
&self,
text: &str,
content_span: Span,
wrapper_span: Span,
source_backed: bool,
dollar: bool,
) -> WordPartNode {
WordPartNode::new(
WordPart::DoubleQuoted {
parts: vec![self.literal_part_from_text(text, content_span, source_backed)],
dollar,
},
wrapper_span,
)
}
fn decode_word_from_token(&mut self, token: &LexedToken<'_>, span: Span) -> Option<Word> {
let word = token.word()?;
if word.single_segment().is_none()
&& !token.flags.is_synthetic()
&& let Some(source_text) = token.source_slice(self.input)
{
return Some(self.parse_word_with_context(source_text, span, span.start, true));
}
if let Some(segment) = word.single_segment() {
let content_span = Self::segment_content_span(segment, span);
let wrapper_span = Self::segment_wrapper_span(segment, span);
let source_backed = segment.span().is_some() && !token.flags.is_synthetic();
let raw_text = segment.as_str();
let use_source_slice = source_backed
&& match segment.kind() {
LexedWordSegmentKind::Plain => {
segment.text_is_source_backed()
|| raw_text.contains("${") && raw_text.contains('/')
|| !raw_text.contains("$(")
}
_ => segment.text_is_source_backed(),
};
let text = if use_source_slice {
content_span.slice(self.input)
} else {
raw_text
};
let decode_text = if source_backed
&& !self.source_matches(content_span, text)
&& matches!(
segment.kind(),
LexedWordSegmentKind::DoubleQuoted | LexedWordSegmentKind::DollarDoubleQuoted
)
&& (!text.contains("$(") || text.contains("$(("))
{
content_span.slice(self.input)
} else {
text
};
let preserve_escaped_expansion_literals =
source_backed && self.source_matches(content_span, decode_text);
return match segment.kind() {
LexedWordSegmentKind::SingleQuoted => Some(self.word_with_single_part(
self.single_quoted_part_from_text(text, content_span, wrapper_span, false),
span,
)),
LexedWordSegmentKind::DollarSingleQuoted => Some(self.word_with_single_part(
self.single_quoted_part_from_text(text, content_span, wrapper_span, true),
span,
)),
LexedWordSegmentKind::Plain if Self::word_text_needs_parse(text) => Some(
self.decode_word_text_preserving_quotes_if_needed_with_escape_mode(
text,
span,
content_span.start,
source_backed,
preserve_escaped_expansion_literals,
),
),
LexedWordSegmentKind::DoubleQuoted | LexedWordSegmentKind::DollarDoubleQuoted
if Self::word_text_needs_parse(text) =>
{
let inner = self.decode_quoted_segment_text(
decode_text,
content_span,
content_span.start,
source_backed,
);
Some(self.word_with_single_part(
WordPartNode::new(
WordPart::DoubleQuoted {
parts: inner.parts,
dollar: matches!(
segment.kind(),
LexedWordSegmentKind::DollarDoubleQuoted
),
},
wrapper_span,
),
span,
))
}
LexedWordSegmentKind::Plain => Some(self.word_with_single_part(
self.literal_part_from_text(text, content_span, source_backed),
span,
)),
LexedWordSegmentKind::DoubleQuoted => Some(self.word_with_single_part(
self.double_quoted_literal_part_from_text(
text,
content_span,
wrapper_span,
source_backed,
false,
),
span,
)),
LexedWordSegmentKind::DollarDoubleQuoted => Some(self.word_with_single_part(
self.double_quoted_literal_part_from_text(
text,
content_span,
wrapper_span,
source_backed,
true,
),
span,
)),
LexedWordSegmentKind::Composite => None,
};
}
let mut parts = Self::word_part_buffer_with_capacity(word.segments().size_hint().0);
let mut cursor = span.start;
for segment in word.segments() {
let raw_text = segment.as_str();
let content_span = if let Some(segment_span) = segment.span() {
cursor = segment_span.end;
segment_span
} else {
let start = cursor;
let end = start.advanced_by(raw_text);
cursor = end;
Span::from_positions(start, end)
};
let wrapper_span = segment.wrapper_span().unwrap_or(content_span);
let source_backed = segment.span().is_some() && !token.flags.is_synthetic();
let use_source_slice = source_backed
&& match segment.kind() {
LexedWordSegmentKind::Plain => {
segment.text_is_source_backed()
|| raw_text.contains("${") && raw_text.contains('/')
|| !raw_text.contains("$(")
}
_ => segment.text_is_source_backed(),
};
let text = if use_source_slice {
content_span.slice(self.input)
} else {
raw_text
};
let preserve_escaped_expansion_literals = source_backed;
match segment.kind() {
LexedWordSegmentKind::SingleQuoted => Self::push_word_part_node(
&mut parts,
self.single_quoted_part_from_text(text, content_span, wrapper_span, false),
),
LexedWordSegmentKind::DollarSingleQuoted => Self::push_word_part_node(
&mut parts,
self.single_quoted_part_from_text(text, content_span, wrapper_span, true),
),
LexedWordSegmentKind::Plain => {
if Self::word_text_needs_parse(text) {
parts.extend(
self.decode_word_text_preserving_quotes_if_needed_with_escape_mode(
text,
content_span,
content_span.start,
source_backed,
preserve_escaped_expansion_literals,
)
.parts,
);
} else {
Self::push_word_part_node(
&mut parts,
self.literal_part_from_text(text, content_span, source_backed),
);
}
}
LexedWordSegmentKind::DoubleQuoted | LexedWordSegmentKind::DollarDoubleQuoted => {
if Self::word_text_needs_parse(text) {
let inner = self.decode_quoted_segment_text(
text,
content_span,
content_span.start,
source_backed,
);
Self::push_word_part_node(
&mut parts,
WordPartNode::new(
WordPart::DoubleQuoted {
parts: inner.parts,
dollar: matches!(
segment.kind(),
LexedWordSegmentKind::DollarDoubleQuoted
),
},
wrapper_span,
),
);
} else {
Self::push_word_part_node(
&mut parts,
self.double_quoted_literal_part_from_text(
text,
content_span,
wrapper_span,
source_backed,
matches!(segment.kind(), LexedWordSegmentKind::DollarDoubleQuoted),
),
);
}
}
LexedWordSegmentKind::Composite => return None,
}
}
Some(self.word_with_part_buffer(parts, span))
}
fn current_word_ref(&mut self) -> Option<&Word> {
if self.current_word_cache.is_none() {
self.current_word_cache = self.current_word();
}
self.current_word_cache.as_ref()
}
fn current_word(&mut self) -> Option<Word> {
if let Some(word) = self.current_word_cache.as_ref() {
return Some(word.clone());
}
if let Some(word) = self.current_zsh_glob_word_from_source() {
self.current_word_cache = Some(word.clone());
return Some(word);
}
let span = self.current_span;
if let Some(token) = self.current_token.clone()
&& let Some(word) = self.simple_word_from_token(&token, span)
{
return Some(word);
}
let token = self.current_token.take()?;
let word = self.decode_word_from_token(&token, span);
self.current_token = Some(token);
if let Some(word) = word.as_ref() {
self.current_word_cache = Some(word.clone());
}
word
}
fn take_current_word(&mut self) -> Option<Word> {
if let Some(word) = self.current_word_cache.take() {
return Some(word);
}
if let Some(word) = self.current_zsh_glob_word_from_source() {
return Some(word);
}
let span = self.current_span;
if let Some(token) = self.current_token.clone()
&& let Some(word) = self.simple_word_from_token(&token, span)
{
return Some(word);
}
let token = self.current_token.take()?;
let word = self.decode_word_from_token(&token, span);
self.current_token = Some(token);
word
}
fn take_current_word_and_advance(&mut self) -> Option<Word> {
let word = self.take_current_word()?;
self.advance_past_word(&word);
Some(word)
}
fn current_zsh_glob_word_from_source(&mut self) -> Option<Word> {
let kind = self.current_token_kind?;
if !matches!(kind, TokenKind::LeftParen | TokenKind::Word) {
return None;
}
if kind == TokenKind::Word && !self.dialect.features().zsh_glob_qualifiers {
return None;
}
let start = self.current_span.start;
if !self.source_word_contains_zsh_glob_control(start) {
return None;
}
let (text, end) = self.scan_source_word(start)?;
if !text.contains("(#") {
return None;
}
let span = Span::from_positions(start, end);
if self.zsh_glob_word_parsing_enabled_at(span.start.offset)
&& let Some(word) = self.maybe_parse_zsh_qualified_glob_word(&text, span, true)
{
return Some(word);
}
Some(self.parse_word_with_context(&text, span, start, true))
}
fn source_word_contains_zsh_glob_control(&self, start: Position) -> bool {
if start.offset >= self.input.len() {
return false;
}
let source = &self.input[start.offset..];
let mut chars = source.chars().peekable();
let mut cursor = start;
let mut paren_depth = 0_i32;
let mut brace_depth = 0_i32;
let mut in_single = false;
let mut in_double = false;
let mut in_backtick = false;
let mut escaped = false;
let mut prev_char = None;
while let Some(&ch) = chars.peek() {
if !in_single
&& !in_double
&& !in_backtick
&& paren_depth == 0
&& brace_depth == 0
&& matches!(ch, ' ' | '\t' | '\n' | ';' | '|' | '&' | '>' | '<' | ')')
{
break;
}
let ch = Self::next_word_char_unwrap(&mut chars, &mut cursor);
if !in_single && !in_double && !in_backtick && prev_char == Some('(') && ch == '#' {
return true;
}
if escaped {
escaped = false;
prev_char = Some(ch);
continue;
}
match ch {
'\\' if !in_single => escaped = true,
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
'`' if !in_single => in_backtick = !in_backtick,
'(' if !in_single && !in_double => paren_depth += 1,
')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
'{' if !in_single && !in_double => brace_depth += 1,
'}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
_ => {}
}
prev_char = Some(ch);
}
false
}
fn scan_source_word(&self, start: Position) -> Option<(String, Position)> {
if start.offset >= self.input.len() {
return None;
}
let source = &self.input[start.offset..];
let mut chars = source.chars().peekable();
let mut cursor = start;
let mut text = String::new();
let mut paren_depth = 0_i32;
let mut brace_depth = 0_i32;
let mut in_single = false;
let mut in_double = false;
let mut in_backtick = false;
let mut escaped = false;
while let Some(&ch) = chars.peek() {
if !in_single
&& !in_double
&& !in_backtick
&& paren_depth == 0
&& brace_depth == 0
&& matches!(ch, ' ' | '\t' | '\n' | ';' | '|' | '&' | '>' | '<' | ')')
{
break;
}
let ch = Self::next_word_char_unwrap(&mut chars, &mut cursor);
text.push(ch);
if escaped {
escaped = false;
continue;
}
match ch {
'\\' if !in_single => escaped = true,
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
'`' if !in_single => in_backtick = !in_backtick,
'(' if !in_single && !in_double => paren_depth += 1,
')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
'{' if !in_single && !in_double => brace_depth += 1,
'}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
_ => {}
}
}
(!text.is_empty()).then_some((text, cursor))
}
fn advance_past_word(&mut self, word: &Word) {
let stop_after_synthetic = self
.current_token
.as_ref()
.is_some_and(|token| token.flags.is_synthetic());
while self.current_token.is_some() && self.current_span.start.offset < word.span.end.offset
{
self.advance();
if stop_after_synthetic
&& self
.current_token
.as_ref()
.is_none_or(|token| !token.flags.is_synthetic())
{
break;
}
}
}
fn token_source_like_word_text(&self, token: &LexedToken<'a>) -> Option<Cow<'a, str>> {
token
.source_slice(self.input)
.map(Cow::Borrowed)
.or_else(|| {
(token.span.start.offset <= token.span.end.offset
&& token.span.end.offset <= self.input.len())
.then(|| Cow::Borrowed(&self.input[token.span.start.offset..token.span.end.offset]))
})
.or_else(|| token.word_string().map(Cow::Owned))
}
fn current_source_like_word_text(&self) -> Option<Cow<'a, str>> {
self.current_token_kind
.filter(|kind| kind.is_word_like())
.and(self.current_token.as_ref())
.and_then(|token| self.token_source_like_word_text(token))
}
fn current_source_like_word_text_or_error(
&self,
context: &'static str,
) -> Result<Cow<'a, str>> {
self.current_source_like_word_text().ok_or_else(|| {
self.error(format!(
"internal parser error: missing source text for {context}"
))
})
}
fn keyword_from_token(token: &LexedToken<'_>) -> Option<Keyword> {
(token.kind == TokenKind::Word)
.then(|| token.word_text())
.flatten()
.and_then(Self::classify_keyword)
}
fn current_conditional_literal_word(&self) -> Option<Word> {
match self.current_token_kind? {
TokenKind::LeftBrace | TokenKind::RightBrace => Some(Word::literal_with_span(
self.input[self.current_span.start.offset..self.current_span.end.offset]
.to_string(),
self.current_span,
)),
_ => None,
}
}
fn current_name_token(&self) -> Option<(Name, Span)> {
self.current_token_kind
.filter(|kind| kind.is_word_like())
.and_then(|_| self.current_word_str())
.map(|word| (Name::from(word), self.current_span))
}
fn current_static_token_text(&self) -> Option<(String, bool)> {
let token = self.current_token.as_ref()?;
let raw_text = token.word_string()?;
let text_had_escape_markers = raw_text.contains('\x00');
let text = if text_had_escape_markers {
raw_text.replace('\x00', "")
} else {
raw_text
};
match token.kind {
TokenKind::LiteralWord => Some((text, true)),
TokenKind::QuotedWord if !Self::word_text_needs_parse(&text) => Some((text, true)),
TokenKind::Word if !Self::word_text_needs_parse(&text) => Some((
text,
token.flags.has_cooked_text() || text_had_escape_markers,
)),
_ => None,
}
}
fn nested_stmt_seq_from_source(&mut self, source: &str, base: Position) -> StmtSeq {
let remaining_depth = self.max_depth.saturating_sub(self.current_depth);
let nested_profile = self
.current_zsh_options()
.cloned()
.map(|options| ShellProfile::with_zsh_options(self.dialect, options))
.unwrap_or_else(|| self.shell_profile.clone());
let inner_parser =
Parser::with_limits_and_profile(source, remaining_depth, self.fuel, nested_profile);
let mut output = inner_parser.parse();
if output.is_ok() {
Self::materialize_stmt_seq_source_backing(&mut output.file.body, source);
Self::rebase_file(&mut output.file, base);
output.file.body
} else {
StmtSeq {
leading_comments: Vec::new(),
stmts: Vec::new(),
trailing_comments: Vec::new(),
span: Span::from_positions(base, base),
}
}
}
fn nested_stmt_seq_from_current_input(&mut self, start: Position, end: Position) -> StmtSeq {
if start.offset > end.offset || end.offset > self.input.len() {
return StmtSeq {
leading_comments: Vec::new(),
stmts: Vec::new(),
trailing_comments: Vec::new(),
span: Span::from_positions(start, start),
};
}
let source = &self.input[start.offset..end.offset];
self.nested_stmt_seq_from_source(source, start)
}
fn merge_optional_span(primary: Span, other: Span) -> Span {
if other == Span::new() {
primary
} else {
primary.merge(other)
}
}
fn redirect_span(operator_span: Span, target: &Word) -> Span {
Self::merge_optional_span(operator_span, target.span)
}
fn optional_span(start: Position, end: Position) -> Option<Span> {
(start.offset < end.offset).then(|| Span::from_positions(start, end))
}
fn split_nested_arithmetic_close(&mut self, context: &'static str) -> Result<Span> {
let right_paren_start = self.current_span.start.advanced_by(")");
self.advance();
if self.at(TokenKind::RightParen) {
let right_paren_span = Span::from_positions(right_paren_start, self.current_span.end);
self.advance();
Ok(right_paren_span)
} else {
Err(Error::parse(format!(
"expected ')' after '))' in {context}"
)))
}
}
fn split_double_semicolon(span: Span) -> (Span, Span) {
let middle = span.start.advanced_by(";");
(
Span::from_positions(span.start, middle),
Span::from_positions(middle, span.end),
)
}
fn split_double_left_paren(span: Span) -> (Span, Span) {
let middle = span.start.advanced_by("(");
(
Span::from_positions(span.start, middle),
Span::from_positions(middle, span.end),
)
}
fn split_double_right_paren(span: Span) -> (Span, Span) {
let middle = span.start.advanced_by(")");
(
Span::from_positions(span.start, middle),
Span::from_positions(middle, span.end),
)
}
fn record_arithmetic_for_separator(
semicolon_span: Span,
segment_start: &mut Position,
init_span: &mut Option<Span>,
first_semicolon_span: &mut Option<Span>,
condition_span: &mut Option<Span>,
second_semicolon_span: &mut Option<Span>,
) -> Result<()> {
if first_semicolon_span.is_none() {
*init_span = Self::optional_span(*segment_start, semicolon_span.start);
*first_semicolon_span = Some(semicolon_span);
*segment_start = semicolon_span.end;
return Ok(());
}
if second_semicolon_span.is_none() {
*condition_span = Self::optional_span(*segment_start, semicolon_span.start);
*second_semicolon_span = Some(semicolon_span);
*segment_start = semicolon_span.end;
return Ok(());
}
Err(Error::parse(
"unexpected ';' in arithmetic for header".to_string(),
))
}
fn rebase_file(file: &mut File, base: Position) {
file.span = file.span.rebased(base);
Self::rebase_stmt_seq(&mut file.body, base);
}
fn rebase_comments(comments: &mut [Comment], base: Position) {
let base_offset = TextSize::new(base.offset as u32);
for comment in comments {
comment.range = comment.range.offset_by(base_offset);
}
}
fn rebase_stmt_seq(sequence: &mut StmtSeq, base: Position) {
sequence.span = sequence.span.rebased(base);
Self::rebase_comments(&mut sequence.leading_comments, base);
for stmt in &mut sequence.stmts {
Self::rebase_stmt(stmt, base);
}
Self::rebase_comments(&mut sequence.trailing_comments, base);
}
fn rebase_stmt(stmt: &mut Stmt, base: Position) {
stmt.span = stmt.span.rebased(base);
Self::rebase_comments(&mut stmt.leading_comments, base);
stmt.terminator_span = stmt.terminator_span.map(|span| span.rebased(base));
if let Some(comment) = &mut stmt.inline_comment {
let base_offset = TextSize::new(base.offset as u32);
comment.range = comment.range.offset_by(base_offset);
}
Self::rebase_redirects(&mut stmt.redirects, base);
Self::rebase_ast_command(&mut stmt.command, base);
}
fn rebase_ast_command(command: &mut AstCommand, base: Position) {
match command {
AstCommand::Simple(simple) => {
simple.span = simple.span.rebased(base);
Self::rebase_word(&mut simple.name, base);
Self::rebase_words(&mut simple.args, base);
Self::rebase_assignments(&mut simple.assignments, base);
}
AstCommand::Builtin(builtin) => match builtin {
AstBuiltinCommand::Break(command) => {
command.span = command.span.rebased(base);
if let Some(depth) = &mut command.depth {
Self::rebase_word(depth, base);
}
Self::rebase_words(&mut command.extra_args, base);
Self::rebase_assignments(&mut command.assignments, base);
}
AstBuiltinCommand::Continue(command) => {
command.span = command.span.rebased(base);
if let Some(depth) = &mut command.depth {
Self::rebase_word(depth, base);
}
Self::rebase_words(&mut command.extra_args, base);
Self::rebase_assignments(&mut command.assignments, base);
}
AstBuiltinCommand::Return(command) => {
command.span = command.span.rebased(base);
if let Some(code) = &mut command.code {
Self::rebase_word(code, base);
}
Self::rebase_words(&mut command.extra_args, base);
Self::rebase_assignments(&mut command.assignments, base);
}
AstBuiltinCommand::Exit(command) => {
command.span = command.span.rebased(base);
if let Some(code) = &mut command.code {
Self::rebase_word(code, base);
}
Self::rebase_words(&mut command.extra_args, base);
Self::rebase_assignments(&mut command.assignments, base);
}
},
AstCommand::Decl(decl) => {
decl.span = decl.span.rebased(base);
decl.variant_span = decl.variant_span.rebased(base);
Self::rebase_assignments(&mut decl.assignments, base);
for operand in &mut decl.operands {
match operand {
DeclOperand::Flag(word) | DeclOperand::Dynamic(word) => {
Self::rebase_word(word, base);
}
DeclOperand::Name(name) => Self::rebase_var_ref(name, base),
DeclOperand::Assignment(assignment) => {
Self::rebase_assignments(std::slice::from_mut(assignment), base);
}
}
}
}
AstCommand::Binary(binary) => {
binary.span = binary.span.rebased(base);
binary.op_span = binary.op_span.rebased(base);
Self::rebase_stmt(binary.left.as_mut(), base);
Self::rebase_stmt(binary.right.as_mut(), base);
}
AstCommand::Compound(compound) => Self::rebase_compound(compound, base),
AstCommand::Function(function) => {
function.span = function.span.rebased(base);
if let Some(span) = &mut function.header.function_keyword_span {
*span = span.rebased(base);
}
if let Some(span) = &mut function.header.trailing_parens_span {
*span = span.rebased(base);
}
for entry in &mut function.header.entries {
Self::rebase_word(&mut entry.word, base);
}
Self::rebase_stmt(function.body.as_mut(), base);
}
AstCommand::AnonymousFunction(function) => {
function.span = function.span.rebased(base);
function.surface = match function.surface {
AnonymousFunctionSurface::FunctionKeyword {
function_keyword_span,
} => AnonymousFunctionSurface::FunctionKeyword {
function_keyword_span: function_keyword_span.rebased(base),
},
AnonymousFunctionSurface::Parens { parens_span } => {
AnonymousFunctionSurface::Parens {
parens_span: parens_span.rebased(base),
}
}
};
Self::rebase_stmt(function.body.as_mut(), base);
Self::rebase_words(&mut function.args, base);
}
}
}
fn rebase_subscript(subscript: &mut Subscript, base: Position) {
subscript.text.rebased(base);
if let Some(raw) = &mut subscript.raw {
raw.rebased(base);
}
if let Some(word) = &mut subscript.word_ast {
Self::rebase_word(word, base);
}
if let Some(expr) = &mut subscript.arithmetic_ast {
Self::rebase_arithmetic_expr(expr, base);
}
}
fn rebase_var_ref(reference: &mut VarRef, base: Position) {
reference.span = reference.span.rebased(base);
reference.name_span = reference.name_span.rebased(base);
if let Some(subscript) = &mut reference.subscript {
Self::rebase_subscript(subscript, base);
}
}
fn rebase_array_expr(array: &mut ArrayExpr, base: Position) {
array.span = array.span.rebased(base);
for element in &mut array.elements {
match element {
ArrayElem::Sequential(word) => Self::rebase_word(word, base),
ArrayElem::Keyed { key, value } | ArrayElem::KeyedAppend { key, value } => {
Self::rebase_subscript(key, base);
Self::rebase_word(value, base);
}
}
}
}
fn rebase_compound(compound: &mut CompoundCommand, base: Position) {
match compound {
CompoundCommand::If(command) => {
command.span = command.span.rebased(base);
command.syntax = match command.syntax {
IfSyntax::ThenFi { then_span, fi_span } => IfSyntax::ThenFi {
then_span: then_span.rebased(base),
fi_span: fi_span.rebased(base),
},
IfSyntax::Brace {
left_brace_span,
right_brace_span,
} => IfSyntax::Brace {
left_brace_span: left_brace_span.rebased(base),
right_brace_span: right_brace_span.rebased(base),
},
};
Self::rebase_stmt_seq(&mut command.condition, base);
Self::rebase_stmt_seq(&mut command.then_branch, base);
for (condition, body) in &mut command.elif_branches {
Self::rebase_stmt_seq(condition, base);
Self::rebase_stmt_seq(body, base);
}
if let Some(else_branch) = &mut command.else_branch {
Self::rebase_stmt_seq(else_branch, base);
}
}
CompoundCommand::For(command) => {
command.span = command.span.rebased(base);
for target in &mut command.targets {
target.span = target.span.rebased(base);
}
if let Some(words) = &mut command.words {
Self::rebase_words(words, base);
}
command.syntax = match command.syntax {
ForSyntax::InDoDone {
in_span,
do_span,
done_span,
} => ForSyntax::InDoDone {
in_span: in_span.map(|span| span.rebased(base)),
do_span: do_span.rebased(base),
done_span: done_span.rebased(base),
},
ForSyntax::InDirect { in_span } => ForSyntax::InDirect {
in_span: in_span.map(|span| span.rebased(base)),
},
ForSyntax::InBrace {
in_span,
left_brace_span,
right_brace_span,
} => ForSyntax::InBrace {
in_span: in_span.map(|span| span.rebased(base)),
left_brace_span: left_brace_span.rebased(base),
right_brace_span: right_brace_span.rebased(base),
},
ForSyntax::ParenDoDone {
left_paren_span,
right_paren_span,
do_span,
done_span,
} => ForSyntax::ParenDoDone {
left_paren_span: left_paren_span.rebased(base),
right_paren_span: right_paren_span.rebased(base),
do_span: do_span.rebased(base),
done_span: done_span.rebased(base),
},
ForSyntax::ParenDirect {
left_paren_span,
right_paren_span,
} => ForSyntax::ParenDirect {
left_paren_span: left_paren_span.rebased(base),
right_paren_span: right_paren_span.rebased(base),
},
ForSyntax::ParenBrace {
left_paren_span,
right_paren_span,
left_brace_span,
right_brace_span,
} => ForSyntax::ParenBrace {
left_paren_span: left_paren_span.rebased(base),
right_paren_span: right_paren_span.rebased(base),
left_brace_span: left_brace_span.rebased(base),
right_brace_span: right_brace_span.rebased(base),
},
};
Self::rebase_stmt_seq(&mut command.body, base);
}
CompoundCommand::Repeat(command) => {
command.span = command.span.rebased(base);
Self::rebase_word(&mut command.count, base);
command.syntax = match command.syntax {
RepeatSyntax::DoDone { do_span, done_span } => RepeatSyntax::DoDone {
do_span: do_span.rebased(base),
done_span: done_span.rebased(base),
},
RepeatSyntax::Direct => RepeatSyntax::Direct,
RepeatSyntax::Brace {
left_brace_span,
right_brace_span,
} => RepeatSyntax::Brace {
left_brace_span: left_brace_span.rebased(base),
right_brace_span: right_brace_span.rebased(base),
},
};
Self::rebase_stmt_seq(&mut command.body, base);
}
CompoundCommand::Foreach(command) => {
command.span = command.span.rebased(base);
command.variable_span = command.variable_span.rebased(base);
Self::rebase_words(&mut command.words, base);
command.syntax = match command.syntax {
ForeachSyntax::ParenBrace {
left_paren_span,
right_paren_span,
left_brace_span,
right_brace_span,
} => ForeachSyntax::ParenBrace {
left_paren_span: left_paren_span.rebased(base),
right_paren_span: right_paren_span.rebased(base),
left_brace_span: left_brace_span.rebased(base),
right_brace_span: right_brace_span.rebased(base),
},
ForeachSyntax::InDoDone {
in_span,
do_span,
done_span,
} => ForeachSyntax::InDoDone {
in_span: in_span.rebased(base),
do_span: do_span.rebased(base),
done_span: done_span.rebased(base),
},
};
Self::rebase_stmt_seq(&mut command.body, base);
}
CompoundCommand::ArithmeticFor(command) => {
command.span = command.span.rebased(base);
command.left_paren_span = command.left_paren_span.rebased(base);
command.init_span = command.init_span.map(|span| span.rebased(base));
if let Some(expr) = &mut command.init_ast {
Self::rebase_arithmetic_expr(expr, base);
}
command.first_semicolon_span = command.first_semicolon_span.rebased(base);
command.condition_span = command.condition_span.map(|span| span.rebased(base));
if let Some(expr) = &mut command.condition_ast {
Self::rebase_arithmetic_expr(expr, base);
}
command.second_semicolon_span = command.second_semicolon_span.rebased(base);
command.step_span = command.step_span.map(|span| span.rebased(base));
if let Some(expr) = &mut command.step_ast {
Self::rebase_arithmetic_expr(expr, base);
}
command.right_paren_span = command.right_paren_span.rebased(base);
Self::rebase_stmt_seq(&mut command.body, base);
}
CompoundCommand::While(command) => {
command.span = command.span.rebased(base);
Self::rebase_stmt_seq(&mut command.condition, base);
Self::rebase_stmt_seq(&mut command.body, base);
}
CompoundCommand::Until(command) => {
command.span = command.span.rebased(base);
Self::rebase_stmt_seq(&mut command.condition, base);
Self::rebase_stmt_seq(&mut command.body, base);
}
CompoundCommand::Case(command) => {
command.span = command.span.rebased(base);
Self::rebase_word(&mut command.word, base);
for case in &mut command.cases {
Self::rebase_patterns(&mut case.patterns, base);
Self::rebase_stmt_seq(&mut case.body, base);
}
}
CompoundCommand::Select(command) => {
command.span = command.span.rebased(base);
command.variable_span = command.variable_span.rebased(base);
Self::rebase_words(&mut command.words, base);
Self::rebase_stmt_seq(&mut command.body, base);
}
CompoundCommand::Subshell(commands) | CompoundCommand::BraceGroup(commands) => {
Self::rebase_stmt_seq(commands, base);
}
CompoundCommand::Arithmetic(command) => {
command.span = command.span.rebased(base);
command.left_paren_span = command.left_paren_span.rebased(base);
command.expr_span = command.expr_span.map(|span| span.rebased(base));
if let Some(expr) = &mut command.expr_ast {
Self::rebase_arithmetic_expr(expr, base);
}
command.right_paren_span = command.right_paren_span.rebased(base);
}
CompoundCommand::Time(command) => {
command.span = command.span.rebased(base);
if let Some(inner) = &mut command.command {
Self::rebase_stmt(inner.as_mut(), base);
}
}
CompoundCommand::Conditional(command) => {
command.span = command.span.rebased(base);
command.left_bracket_span = command.left_bracket_span.rebased(base);
command.right_bracket_span = command.right_bracket_span.rebased(base);
Self::rebase_conditional_expr(&mut command.expression, base);
}
CompoundCommand::Coproc(command) => {
command.span = command.span.rebased(base);
command.name_span = command.name_span.map(|span| span.rebased(base));
Self::rebase_stmt(command.body.as_mut(), base);
}
CompoundCommand::Always(command) => {
command.span = command.span.rebased(base);
Self::rebase_stmt_seq(&mut command.body, base);
Self::rebase_stmt_seq(&mut command.always_body, base);
}
}
}
fn materialize_stmt_seq_source_backing(sequence: &mut StmtSeq, source: &str) {
for stmt in &mut sequence.stmts {
Self::materialize_stmt_source_backing(stmt, source);
}
}
fn materialize_stmt_source_backing(stmt: &mut Stmt, source: &str) {
Self::materialize_ast_command_source_backing(&mut stmt.command, source);
}
fn materialize_ast_command_source_backing(command: &mut AstCommand, source: &str) {
match command {
AstCommand::Simple(simple) => {
Self::materialize_word_source_backing(&mut simple.name, source);
}
AstCommand::Builtin(_) | AstCommand::Decl(_) => {}
AstCommand::Binary(binary) => {
Self::materialize_stmt_source_backing(binary.left.as_mut(), source);
Self::materialize_stmt_source_backing(binary.right.as_mut(), source);
}
AstCommand::Compound(compound) => {
Self::materialize_compound_source_backing(compound, source);
}
AstCommand::Function(function) => {
Self::materialize_stmt_source_backing(function.body.as_mut(), source);
}
AstCommand::AnonymousFunction(function) => {
Self::materialize_stmt_source_backing(function.body.as_mut(), source);
}
}
}
fn materialize_compound_source_backing(compound: &mut CompoundCommand, source: &str) {
match compound {
CompoundCommand::If(command) => {
Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
Self::materialize_stmt_seq_source_backing(&mut command.then_branch, source);
for (condition, body) in &mut command.elif_branches {
Self::materialize_stmt_seq_source_backing(condition, source);
Self::materialize_stmt_seq_source_backing(body, source);
}
if let Some(else_branch) = &mut command.else_branch {
Self::materialize_stmt_seq_source_backing(else_branch, source);
}
}
CompoundCommand::For(command) => {
Self::materialize_stmt_seq_source_backing(&mut command.body, source)
}
CompoundCommand::Repeat(command) => {
Self::materialize_stmt_seq_source_backing(&mut command.body, source)
}
CompoundCommand::Foreach(command) => {
Self::materialize_stmt_seq_source_backing(&mut command.body, source)
}
CompoundCommand::ArithmeticFor(command) => {
Self::materialize_stmt_seq_source_backing(&mut command.body, source)
}
CompoundCommand::While(command) => {
Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
Self::materialize_stmt_seq_source_backing(&mut command.body, source);
}
CompoundCommand::Until(command) => {
Self::materialize_stmt_seq_source_backing(&mut command.condition, source);
Self::materialize_stmt_seq_source_backing(&mut command.body, source);
}
CompoundCommand::Case(command) => {
for case in &mut command.cases {
Self::materialize_stmt_seq_source_backing(&mut case.body, source);
}
}
CompoundCommand::Select(command) => {
Self::materialize_stmt_seq_source_backing(&mut command.body, source)
}
CompoundCommand::Subshell(commands) | CompoundCommand::BraceGroup(commands) => {
Self::materialize_stmt_seq_source_backing(commands, source);
}
CompoundCommand::Arithmetic(_) => {}
CompoundCommand::Time(command) => {
if let Some(inner) = &mut command.command {
Self::materialize_stmt_source_backing(inner.as_mut(), source);
}
}
CompoundCommand::Conditional(_) => {}
CompoundCommand::Coproc(command) => {
Self::materialize_stmt_source_backing(command.body.as_mut(), source);
}
CompoundCommand::Always(command) => {
Self::materialize_stmt_seq_source_backing(&mut command.body, source);
Self::materialize_stmt_seq_source_backing(&mut command.always_body, source);
}
}
}
fn rebase_words(words: &mut [Word], base: Position) {
for word in words {
Self::rebase_word(word, base);
}
}
fn rebase_patterns(patterns: &mut [Pattern], base: Position) {
for pattern in patterns {
Self::rebase_pattern(pattern, base);
}
}
fn materialize_literal_text_source_backing(text: &mut LiteralText, span: Span, source: &str) {
match text {
LiteralText::Source => {
*text = LiteralText::owned(span.slice(source).to_string());
}
LiteralText::CookedSource(cooked) => {
*text = LiteralText::owned(cooked.to_string());
}
LiteralText::Owned(_) => {}
}
}
fn materialize_source_text_source_backing(text: &mut SourceText, source: &str) {
if text.is_source_backed() {
let span = text.span();
let cooked = text.slice(source).to_string();
*text = SourceText::cooked(span, cooked);
}
}
fn materialize_word_source_backing(word: &mut Word, source: &str) {
for part in &mut word.parts {
Self::materialize_word_part_source_backing(part, source);
}
}
fn materialize_pattern_source_backing(pattern: &mut Pattern, source: &str) {
for part in &mut pattern.parts {
Self::materialize_pattern_part_source_backing(part, source);
}
}
fn materialize_pattern_part_source_backing(part: &mut PatternPartNode, source: &str) {
match &mut part.kind {
PatternPart::Literal(text) => {
Self::materialize_literal_text_source_backing(text, part.span, source);
}
PatternPart::CharClass(text) => {
Self::materialize_source_text_source_backing(text, source);
}
PatternPart::Group { patterns, .. } => {
for pattern in patterns {
Self::materialize_pattern_source_backing(pattern, source);
}
}
PatternPart::Word(word) => Self::materialize_word_source_backing(word, source),
PatternPart::AnyString | PatternPart::AnyChar => {}
}
}
fn materialize_word_part_source_backing(part: &mut WordPartNode, source: &str) {
match &mut part.kind {
WordPart::Literal(text) => {
Self::materialize_literal_text_source_backing(text, part.span, source);
}
WordPart::ZshQualifiedGlob(glob) => {
Self::materialize_zsh_qualified_glob_source_backing(glob, source);
}
WordPart::SingleQuoted { value, .. } => {
Self::materialize_source_text_source_backing(value, source);
}
WordPart::DoubleQuoted { parts, .. } => {
for part in parts {
Self::materialize_word_part_source_backing(part, source);
}
}
WordPart::Parameter(parameter) => {
Self::materialize_source_text_source_backing(&mut parameter.raw_body, source);
Self::materialize_parameter_expansion_syntax_source_backing(
&mut parameter.syntax,
source,
);
}
WordPart::ParameterExpansion {
reference,
operator,
operand,
operand_word_ast,
..
} => {
Self::materialize_var_ref_source_backing(reference, source);
Self::materialize_parameter_operator_source_backing(operator, source);
if let Some(operand) = operand {
Self::materialize_source_text_source_backing(operand, source);
}
if let Some(word_ast) = operand_word_ast {
Self::materialize_word_source_backing(word_ast, source);
}
}
WordPart::ArrayAccess(reference)
| WordPart::Length(reference)
| WordPart::ArrayLength(reference)
| WordPart::ArrayIndices(reference)
| WordPart::Transformation { reference, .. } => {
Self::materialize_var_ref_source_backing(reference, source);
}
WordPart::Substring {
reference,
offset,
offset_ast,
offset_word_ast,
length,
length_ast,
length_word_ast,
..
}
| WordPart::ArraySlice {
reference,
offset,
offset_ast,
offset_word_ast,
length,
length_ast,
length_word_ast,
..
} => {
Self::materialize_var_ref_source_backing(reference, source);
Self::materialize_source_text_source_backing(offset, source);
Self::materialize_word_source_backing(offset_word_ast, source);
if let Some(expr) = offset_ast {
Self::materialize_arithmetic_expr_source_backing(expr, source);
}
if let Some(length) = length {
Self::materialize_source_text_source_backing(length, source);
}
if let Some(word_ast) = length_word_ast {
Self::materialize_word_source_backing(word_ast, source);
}
if let Some(expr) = length_ast {
Self::materialize_arithmetic_expr_source_backing(expr, source);
}
}
WordPart::IndirectExpansion {
reference,
operator,
operand,
operand_word_ast,
..
} => {
Self::materialize_var_ref_source_backing(reference, source);
if let Some(operator) = operator {
Self::materialize_parameter_operator_source_backing(operator, source);
}
if let Some(operand) = operand {
Self::materialize_source_text_source_backing(operand, source);
}
if let Some(word_ast) = operand_word_ast {
Self::materialize_word_source_backing(word_ast, source);
}
}
WordPart::ArithmeticExpansion {
expression,
expression_ast,
expression_word_ast,
..
} => {
Self::materialize_source_text_source_backing(expression, source);
Self::materialize_word_source_backing(expression_word_ast, source);
if let Some(expr) = expression_ast {
Self::materialize_arithmetic_expr_source_backing(expr, source);
}
}
WordPart::CommandSubstitution { .. }
| WordPart::ProcessSubstitution { .. }
| WordPart::Variable(_)
| WordPart::PrefixMatch { .. } => {}
}
}
fn materialize_var_ref_source_backing(reference: &mut VarRef, source: &str) {
if let Some(subscript) = &mut reference.subscript {
Self::materialize_subscript_source_backing(subscript, source);
}
}
fn materialize_subscript_source_backing(subscript: &mut Subscript, source: &str) {
Self::materialize_source_text_source_backing(&mut subscript.text, source);
if let Some(raw) = &mut subscript.raw {
Self::materialize_source_text_source_backing(raw, source);
}
if let Some(word_ast) = &mut subscript.word_ast {
Self::materialize_word_source_backing(word_ast, source);
}
if let Some(expr) = &mut subscript.arithmetic_ast {
Self::materialize_arithmetic_expr_source_backing(expr, source);
}
}
fn materialize_zsh_qualified_glob_source_backing(glob: &mut ZshQualifiedGlob, source: &str) {
for segment in &mut glob.segments {
match segment {
ZshGlobSegment::Pattern(pattern) => {
Self::materialize_pattern_source_backing(pattern, source);
}
ZshGlobSegment::InlineControl(_) => {}
}
}
if let Some(qualifiers) = &mut glob.qualifiers {
for fragment in &mut qualifiers.fragments {
match fragment {
ZshGlobQualifier::LetterSequence { text, .. } => {
Self::materialize_source_text_source_backing(text, source);
}
ZshGlobQualifier::NumericArgument { start, end, .. } => {
Self::materialize_source_text_source_backing(start, source);
if let Some(end) = end {
Self::materialize_source_text_source_backing(end, source);
}
}
ZshGlobQualifier::Negation { .. } | ZshGlobQualifier::Flag { .. } => {}
}
}
}
}
fn materialize_parameter_expansion_syntax_source_backing(
syntax: &mut ParameterExpansionSyntax,
source: &str,
) {
match syntax {
ParameterExpansionSyntax::Bourne(syntax) => match syntax {
BourneParameterExpansion::Access { reference }
| BourneParameterExpansion::Length { reference }
| BourneParameterExpansion::Indices { reference }
| BourneParameterExpansion::Transformation { reference, .. } => {
Self::materialize_var_ref_source_backing(reference, source);
}
BourneParameterExpansion::Indirect {
reference,
operator,
operand,
operand_word_ast,
..
} => {
Self::materialize_var_ref_source_backing(reference, source);
if let Some(operator) = operator {
Self::materialize_parameter_operator_source_backing(operator, source);
}
if let Some(operand) = operand {
Self::materialize_source_text_source_backing(operand, source);
}
if let Some(word_ast) = operand_word_ast {
Self::materialize_word_source_backing(word_ast, source);
}
}
BourneParameterExpansion::PrefixMatch { .. } => {}
BourneParameterExpansion::Slice {
reference,
offset,
offset_ast,
offset_word_ast,
length,
length_ast,
length_word_ast,
} => {
Self::materialize_var_ref_source_backing(reference, source);
Self::materialize_source_text_source_backing(offset, source);
Self::materialize_word_source_backing(offset_word_ast, source);
if let Some(expr) = offset_ast {
Self::materialize_arithmetic_expr_source_backing(expr, source);
}
if let Some(length) = length {
Self::materialize_source_text_source_backing(length, source);
}
if let Some(word_ast) = length_word_ast {
Self::materialize_word_source_backing(word_ast, source);
}
if let Some(expr) = length_ast {
Self::materialize_arithmetic_expr_source_backing(expr, source);
}
}
BourneParameterExpansion::Operation {
reference,
operator,
operand,
operand_word_ast,
..
} => {
Self::materialize_var_ref_source_backing(reference, source);
Self::materialize_parameter_operator_source_backing(operator, source);
if let Some(operand) = operand {
Self::materialize_source_text_source_backing(operand, source);
}
if let Some(word_ast) = operand_word_ast {
Self::materialize_word_source_backing(word_ast, source);
}
}
},
ParameterExpansionSyntax::Zsh(syntax) => {
match &mut syntax.target {
ZshExpansionTarget::Reference(reference) => {
Self::materialize_var_ref_source_backing(reference, source);
}
ZshExpansionTarget::Word(word) => {
Self::materialize_word_source_backing(word, source);
}
ZshExpansionTarget::Nested(parameter) => {
Self::materialize_source_text_source_backing(
&mut parameter.raw_body,
source,
);
Self::materialize_parameter_expansion_syntax_source_backing(
&mut parameter.syntax,
source,
);
}
ZshExpansionTarget::Empty => {}
}
for modifier in &mut syntax.modifiers {
if let Some(argument) = &mut modifier.argument {
Self::materialize_source_text_source_backing(argument, source);
}
if let Some(argument_word_ast) = &mut modifier.argument_word_ast {
Self::materialize_word_source_backing(argument_word_ast, source);
}
}
if let Some(operation) = &mut syntax.operation {
match operation {
ZshExpansionOperation::PatternOperation {
operand,
operand_word_ast,
..
}
| ZshExpansionOperation::Defaulting {
operand,
operand_word_ast,
..
}
| ZshExpansionOperation::TrimOperation {
operand,
operand_word_ast,
..
} => {
Self::materialize_source_text_source_backing(operand, source);
Self::materialize_word_source_backing(operand_word_ast, source);
}
ZshExpansionOperation::Unknown { text, word_ast } => {
Self::materialize_source_text_source_backing(text, source);
Self::materialize_word_source_backing(word_ast, source);
}
ZshExpansionOperation::ReplacementOperation {
pattern,
pattern_word_ast,
replacement,
replacement_word_ast,
..
} => {
Self::materialize_source_text_source_backing(pattern, source);
Self::materialize_word_source_backing(pattern_word_ast, source);
if let Some(replacement) = replacement {
Self::materialize_source_text_source_backing(replacement, source);
}
if let Some(replacement_word_ast) = replacement_word_ast {
Self::materialize_word_source_backing(replacement_word_ast, source);
}
}
ZshExpansionOperation::Slice {
offset,
offset_word_ast,
length,
length_word_ast,
} => {
Self::materialize_source_text_source_backing(offset, source);
Self::materialize_word_source_backing(offset_word_ast, source);
if let Some(length) = length {
Self::materialize_source_text_source_backing(length, source);
}
if let Some(length_word_ast) = length_word_ast {
Self::materialize_word_source_backing(length_word_ast, source);
}
}
}
}
}
}
}
fn materialize_parameter_operator_source_backing(operator: &mut ParameterOp, source: &str) {
match operator {
ParameterOp::RemovePrefixShort { pattern }
| ParameterOp::RemovePrefixLong { pattern }
| ParameterOp::RemoveSuffixShort { pattern }
| ParameterOp::RemoveSuffixLong { pattern } => {
Self::materialize_pattern_source_backing(pattern, source);
}
ParameterOp::ReplaceFirst {
pattern,
replacement,
replacement_word_ast,
}
| ParameterOp::ReplaceAll {
pattern,
replacement,
replacement_word_ast,
} => {
Self::materialize_pattern_source_backing(pattern, source);
Self::materialize_source_text_source_backing(replacement, source);
Self::materialize_word_source_backing(replacement_word_ast, source);
}
ParameterOp::UseDefault
| ParameterOp::AssignDefault
| ParameterOp::UseReplacement
| ParameterOp::Error
| ParameterOp::UpperFirst
| ParameterOp::UpperAll
| ParameterOp::LowerFirst
| ParameterOp::LowerAll => {}
}
}
fn materialize_arithmetic_expr_source_backing(expr: &mut ArithmeticExprNode, source: &str) {
match &mut expr.kind {
ArithmeticExpr::Number(text) => {
Self::materialize_source_text_source_backing(text, source);
}
ArithmeticExpr::Variable(_) => {}
ArithmeticExpr::Indexed { index, .. } => {
Self::materialize_arithmetic_expr_source_backing(index, source);
}
ArithmeticExpr::ShellWord(word) => {
Self::materialize_word_source_backing(word, source);
}
ArithmeticExpr::Parenthesized { expression } => {
Self::materialize_arithmetic_expr_source_backing(expression, source);
}
ArithmeticExpr::Unary { expr, .. } | ArithmeticExpr::Postfix { expr, .. } => {
Self::materialize_arithmetic_expr_source_backing(expr, source);
}
ArithmeticExpr::Binary { left, right, .. } => {
Self::materialize_arithmetic_expr_source_backing(left, source);
Self::materialize_arithmetic_expr_source_backing(right, source);
}
ArithmeticExpr::Conditional {
condition,
then_expr,
else_expr,
} => {
Self::materialize_arithmetic_expr_source_backing(condition, source);
Self::materialize_arithmetic_expr_source_backing(then_expr, source);
Self::materialize_arithmetic_expr_source_backing(else_expr, source);
}
ArithmeticExpr::Assignment { target, value, .. } => {
Self::materialize_arithmetic_lvalue_source_backing(target, source);
Self::materialize_arithmetic_expr_source_backing(value, source);
}
}
}
fn materialize_arithmetic_lvalue_source_backing(target: &mut ArithmeticLvalue, source: &str) {
match target {
ArithmeticLvalue::Variable(_) => {}
ArithmeticLvalue::Indexed { index, .. } => {
Self::materialize_arithmetic_expr_source_backing(index, source);
}
}
}
fn rebase_word(word: &mut Word, base: Position) {
word.span = word.span.rebased(base);
for brace in &mut word.brace_syntax {
brace.span = brace.span.rebased(base);
}
Self::rebase_word_parts(&mut word.parts, base);
}
fn rebase_heredoc_body(body: &mut HeredocBody, base: Position) {
body.span = body.span.rebased(base);
for part in &mut body.parts {
Self::rebase_heredoc_body_part(part, base);
}
}
fn rebase_pattern(pattern: &mut Pattern, base: Position) {
pattern.span = pattern.span.rebased(base);
Self::rebase_pattern_parts(&mut pattern.parts, base);
}
fn rebase_word_parts(parts: &mut [WordPartNode], base: Position) {
for part in parts {
Self::rebase_word_part(part, base);
}
}
fn rebase_pattern_parts(parts: &mut [PatternPartNode], base: Position) {
for part in parts {
part.span = part.span.rebased(base);
match &mut part.kind {
PatternPart::CharClass(text) => text.rebased(base),
PatternPart::Group { patterns, .. } => Self::rebase_patterns(patterns, base),
PatternPart::Word(word) => Self::rebase_word(word, base),
PatternPart::Literal(_) | PatternPart::AnyString | PatternPart::AnyChar => {}
}
}
}
fn rebase_heredoc_body_part(part: &mut HeredocBodyPartNode, base: Position) {
part.span = part.span.rebased(base);
match &mut part.kind {
HeredocBodyPart::Literal(_) | HeredocBodyPart::Variable(_) => {}
HeredocBodyPart::CommandSubstitution { body, .. } => Self::rebase_stmt_seq(body, base),
HeredocBodyPart::ArithmeticExpansion {
expression,
expression_ast,
expression_word_ast,
..
} => {
expression.rebased(base);
Self::rebase_word(expression_word_ast, base);
if let Some(expr) = expression_ast {
Self::rebase_arithmetic_expr(expr, base);
}
}
HeredocBodyPart::Parameter(parameter) => {
parameter.span = parameter.span.rebased(base);
parameter.raw_body.rebased(base);
Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
}
}
}
fn rebase_word_part(part: &mut WordPartNode, base: Position) {
part.span = part.span.rebased(base);
match &mut part.kind {
WordPart::ZshQualifiedGlob(glob) => Self::rebase_zsh_qualified_glob(glob, base),
WordPart::SingleQuoted { value, .. } => value.rebased(base),
WordPart::DoubleQuoted { parts, .. } => Self::rebase_word_parts(parts, base),
WordPart::Parameter(parameter) => {
parameter.span = parameter.span.rebased(base);
parameter.raw_body.rebased(base);
Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
}
WordPart::ParameterExpansion {
reference,
operator,
operand,
operand_word_ast,
..
} => {
Self::rebase_var_ref(reference, base);
match operator.as_mut() {
ParameterOp::RemovePrefixShort { pattern }
| ParameterOp::RemovePrefixLong { pattern }
| ParameterOp::RemoveSuffixShort { pattern }
| ParameterOp::RemoveSuffixLong { pattern } => {
Self::rebase_pattern(pattern, base);
}
ParameterOp::ReplaceFirst {
pattern,
replacement,
..
}
| ParameterOp::ReplaceAll {
pattern,
replacement,
..
} => {
Self::rebase_pattern(pattern, base);
replacement.rebased(base);
}
ParameterOp::UseDefault
| ParameterOp::AssignDefault
| ParameterOp::UseReplacement
| ParameterOp::Error
| ParameterOp::UpperFirst
| ParameterOp::UpperAll
| ParameterOp::LowerFirst
| ParameterOp::LowerAll => {}
}
if let Some(operand) = operand {
operand.rebased(base);
}
if let Some(word_ast) = operand_word_ast {
Self::rebase_word(word_ast, base);
}
}
WordPart::ArrayAccess(reference)
| WordPart::Length(reference)
| WordPart::ArrayLength(reference)
| WordPart::ArrayIndices(reference)
| WordPart::Transformation { reference, .. } => Self::rebase_var_ref(reference, base),
WordPart::Substring {
reference,
offset,
offset_ast,
offset_word_ast,
length,
length_ast,
length_word_ast,
..
}
| WordPart::ArraySlice {
reference,
offset,
offset_ast,
offset_word_ast,
length,
length_ast,
length_word_ast,
..
} => {
Self::rebase_var_ref(reference, base);
offset.rebased(base);
Self::rebase_word(offset_word_ast, base);
if let Some(expr) = offset_ast {
Self::rebase_arithmetic_expr(expr, base);
}
if let Some(length) = length {
length.rebased(base);
}
if let Some(word_ast) = length_word_ast {
Self::rebase_word(word_ast, base);
}
if let Some(expr) = length_ast {
Self::rebase_arithmetic_expr(expr, base);
}
}
WordPart::IndirectExpansion {
reference,
operator,
operand,
operand_word_ast,
..
} => {
Self::rebase_var_ref(reference, base);
if let Some(operator) = operator {
Self::rebase_parameter_operator(operator, base);
}
if let Some(operand) = operand {
operand.rebased(base);
}
if let Some(word_ast) = operand_word_ast {
Self::rebase_word(word_ast, base);
}
}
WordPart::ArithmeticExpansion {
expression,
expression_ast,
expression_word_ast,
..
} => {
expression.rebased(base);
Self::rebase_word(expression_word_ast, base);
if let Some(expr) = expression_ast {
Self::rebase_arithmetic_expr(expr, base);
}
}
WordPart::CommandSubstitution { body, .. }
| WordPart::ProcessSubstitution { body, .. } => Self::rebase_stmt_seq(body, base),
WordPart::Literal(_) | WordPart::Variable(_) | WordPart::PrefixMatch { .. } => {}
}
}
fn rebase_zsh_qualified_glob(glob: &mut ZshQualifiedGlob, base: Position) {
glob.span = glob.span.rebased(base);
for segment in &mut glob.segments {
Self::rebase_zsh_glob_segment(segment, base);
}
if let Some(qualifiers) = &mut glob.qualifiers {
Self::rebase_zsh_glob_qualifier_group(qualifiers, base);
}
}
fn rebase_zsh_glob_segment(segment: &mut ZshGlobSegment, base: Position) {
match segment {
ZshGlobSegment::Pattern(pattern) => Self::rebase_pattern(pattern, base),
ZshGlobSegment::InlineControl(control) => {
Self::rebase_zsh_inline_glob_control(control, base)
}
}
}
fn rebase_zsh_inline_glob_control(control: &mut ZshInlineGlobControl, base: Position) {
match control {
ZshInlineGlobControl::CaseInsensitive { span }
| ZshInlineGlobControl::Backreferences { span }
| ZshInlineGlobControl::StartAnchor { span }
| ZshInlineGlobControl::EndAnchor { span } => {
*span = span.rebased(base);
}
}
}
fn rebase_zsh_glob_qualifier_group(group: &mut ZshGlobQualifierGroup, base: Position) {
group.span = group.span.rebased(base);
for fragment in &mut group.fragments {
match fragment {
ZshGlobQualifier::Negation { span } | ZshGlobQualifier::Flag { span, .. } => {
*span = span.rebased(base);
}
ZshGlobQualifier::LetterSequence { text, span } => {
*span = span.rebased(base);
text.rebased(base);
}
ZshGlobQualifier::NumericArgument { span, start, end } => {
*span = span.rebased(base);
start.rebased(base);
if let Some(end) = end {
end.rebased(base);
}
}
}
}
}
fn rebase_parameter_expansion_syntax(syntax: &mut ParameterExpansionSyntax, base: Position) {
match syntax {
ParameterExpansionSyntax::Bourne(syntax) => match syntax {
BourneParameterExpansion::Access { reference }
| BourneParameterExpansion::Length { reference }
| BourneParameterExpansion::Indices { reference }
| BourneParameterExpansion::Transformation { reference, .. } => {
Self::rebase_var_ref(reference, base);
}
BourneParameterExpansion::Indirect {
reference,
operand,
operator,
operand_word_ast,
..
} => {
Self::rebase_var_ref(reference, base);
if let Some(operator) = operator {
Self::rebase_parameter_operator(operator, base);
}
if let Some(operand) = operand {
operand.rebased(base);
}
if let Some(word_ast) = operand_word_ast {
Self::rebase_word(word_ast, base);
}
}
BourneParameterExpansion::PrefixMatch { .. } => {}
BourneParameterExpansion::Slice {
reference,
offset,
offset_ast,
offset_word_ast,
length,
length_ast,
length_word_ast,
} => {
Self::rebase_var_ref(reference, base);
offset.rebased(base);
Self::rebase_word(offset_word_ast, base);
if let Some(expr) = offset_ast {
Self::rebase_arithmetic_expr(expr, base);
}
if let Some(length) = length {
length.rebased(base);
}
if let Some(word_ast) = length_word_ast {
Self::rebase_word(word_ast, base);
}
if let Some(expr) = length_ast {
Self::rebase_arithmetic_expr(expr, base);
}
}
BourneParameterExpansion::Operation {
reference,
operator,
operand,
operand_word_ast,
..
} => {
Self::rebase_var_ref(reference, base);
Self::rebase_parameter_operator(operator, base);
if let Some(operand) = operand {
operand.rebased(base);
}
if let Some(word_ast) = operand_word_ast {
Self::rebase_word(word_ast, base);
}
}
},
ParameterExpansionSyntax::Zsh(syntax) => {
match &mut syntax.target {
ZshExpansionTarget::Reference(reference) => {
Self::rebase_var_ref(reference, base)
}
ZshExpansionTarget::Word(word) => Self::rebase_word(word, base),
ZshExpansionTarget::Nested(parameter) => {
parameter.span = parameter.span.rebased(base);
parameter.raw_body.rebased(base);
Self::rebase_parameter_expansion_syntax(&mut parameter.syntax, base);
}
ZshExpansionTarget::Empty => {}
}
for modifier in &mut syntax.modifiers {
modifier.span = modifier.span.rebased(base);
if let Some(argument) = &mut modifier.argument {
argument.rebased(base);
}
if let Some(argument_word_ast) = &mut modifier.argument_word_ast {
Self::rebase_word(argument_word_ast, base);
}
}
if let Some(length_prefix) = &mut syntax.length_prefix {
*length_prefix = length_prefix.rebased(base);
}
if let Some(operation) = &mut syntax.operation {
match operation {
ZshExpansionOperation::PatternOperation {
operand,
operand_word_ast,
..
}
| ZshExpansionOperation::Defaulting {
operand,
operand_word_ast,
..
}
| ZshExpansionOperation::TrimOperation {
operand,
operand_word_ast,
..
} => {
operand.rebased(base);
Self::rebase_word(operand_word_ast, base);
}
ZshExpansionOperation::Unknown { text, word_ast } => {
text.rebased(base);
Self::rebase_word(word_ast, base);
}
ZshExpansionOperation::ReplacementOperation {
pattern,
pattern_word_ast,
replacement,
replacement_word_ast,
..
} => {
pattern.rebased(base);
Self::rebase_word(pattern_word_ast, base);
if let Some(replacement) = replacement {
replacement.rebased(base);
}
if let Some(replacement_word_ast) = replacement_word_ast {
Self::rebase_word(replacement_word_ast, base);
}
}
ZshExpansionOperation::Slice {
offset,
offset_word_ast,
length,
length_word_ast,
} => {
offset.rebased(base);
Self::rebase_word(offset_word_ast, base);
if let Some(length) = length {
length.rebased(base);
}
if let Some(length_word_ast) = length_word_ast {
Self::rebase_word(length_word_ast, base);
}
}
}
}
}
}
}
fn rebase_parameter_operator(operator: &mut ParameterOp, base: Position) {
match operator {
ParameterOp::RemovePrefixShort { pattern }
| ParameterOp::RemovePrefixLong { pattern }
| ParameterOp::RemoveSuffixShort { pattern }
| ParameterOp::RemoveSuffixLong { pattern } => {
Self::rebase_pattern(pattern, base);
}
ParameterOp::ReplaceFirst {
pattern,
replacement,
replacement_word_ast,
}
| ParameterOp::ReplaceAll {
pattern,
replacement,
replacement_word_ast,
} => {
Self::rebase_pattern(pattern, base);
replacement.rebased(base);
Self::rebase_word(replacement_word_ast, base);
}
ParameterOp::UseDefault
| ParameterOp::AssignDefault
| ParameterOp::UseReplacement
| ParameterOp::Error
| ParameterOp::UpperFirst
| ParameterOp::UpperAll
| ParameterOp::LowerFirst
| ParameterOp::LowerAll => {}
}
}
fn rebase_conditional_expr(expr: &mut ConditionalExpr, base: Position) {
match expr {
ConditionalExpr::Binary(binary) => {
binary.op_span = binary.op_span.rebased(base);
Self::rebase_conditional_expr(&mut binary.left, base);
Self::rebase_conditional_expr(&mut binary.right, base);
}
ConditionalExpr::Unary(unary) => {
unary.op_span = unary.op_span.rebased(base);
Self::rebase_conditional_expr(&mut unary.expr, base);
}
ConditionalExpr::Parenthesized(paren) => {
paren.left_paren_span = paren.left_paren_span.rebased(base);
paren.right_paren_span = paren.right_paren_span.rebased(base);
Self::rebase_conditional_expr(&mut paren.expr, base);
}
ConditionalExpr::Word(word) | ConditionalExpr::Regex(word) => {
Self::rebase_word(word, base);
}
ConditionalExpr::Pattern(pattern) => Self::rebase_pattern(pattern, base),
ConditionalExpr::VarRef(var_ref) => Self::rebase_var_ref(var_ref, base),
}
}
fn rebase_arithmetic_expr(expr: &mut ArithmeticExprNode, base: Position) {
expr.span = expr.span.rebased(base);
match &mut expr.kind {
ArithmeticExpr::Number(text) => text.rebased(base),
ArithmeticExpr::Variable(_) => {}
ArithmeticExpr::Indexed { index, .. } => Self::rebase_arithmetic_expr(index, base),
ArithmeticExpr::ShellWord(word) => Self::rebase_word(word, base),
ArithmeticExpr::Parenthesized { expression } => {
Self::rebase_arithmetic_expr(expression, base)
}
ArithmeticExpr::Unary { expr, .. } | ArithmeticExpr::Postfix { expr, .. } => {
Self::rebase_arithmetic_expr(expr, base)
}
ArithmeticExpr::Binary { left, right, .. } => {
Self::rebase_arithmetic_expr(left, base);
Self::rebase_arithmetic_expr(right, base);
}
ArithmeticExpr::Conditional {
condition,
then_expr,
else_expr,
} => {
Self::rebase_arithmetic_expr(condition, base);
Self::rebase_arithmetic_expr(then_expr, base);
Self::rebase_arithmetic_expr(else_expr, base);
}
ArithmeticExpr::Assignment { target, value, .. } => {
Self::rebase_arithmetic_lvalue(target, base);
Self::rebase_arithmetic_expr(value, base);
}
}
}
fn rebase_arithmetic_lvalue(target: &mut ArithmeticLvalue, base: Position) {
match target {
ArithmeticLvalue::Variable(_) => {}
ArithmeticLvalue::Indexed { index, .. } => Self::rebase_arithmetic_expr(index, base),
}
}
fn push_word_part(parts: &mut WordPartBuffer, part: WordPart, start: Position, end: Position) {
Self::push_word_part_node(
parts,
WordPartNode::new(part, Span::from_positions(start, end)),
);
}
fn push_word_part_node(parts: &mut WordPartBuffer, part: WordPartNode) {
parts.push(part);
}
fn flush_literal_part(
&self,
parts: &mut WordPartBuffer,
current: &mut String,
current_start: Position,
end: Position,
source_backed: bool,
) {
if !current.is_empty() {
Self::push_word_part(
parts,
WordPart::Literal(self.literal_text(
std::mem::take(current),
current_start,
end,
source_backed,
)),
current_start,
end,
);
}
}
fn word_part_buffer_with_capacity(capacity: usize) -> WordPartBuffer {
if capacity <= 2 {
WordPartBuffer::new()
} else {
WordPartBuffer::with_capacity(capacity)
}
}
fn literal_text(
&self,
text: String,
start: Position,
end: Position,
source_backed: bool,
) -> LiteralText {
let span = Span::from_positions(start, end);
if self.source_matches(span, &text) {
LiteralText::source()
} else if source_backed {
LiteralText::cooked_source(text)
} else {
LiteralText::owned(text)
}
}
fn literal_text_from_str(
&self,
text: &str,
start: Position,
end: Position,
source_backed: bool,
) -> LiteralText {
self.literal_text_impl(text, None, start, end, source_backed)
}
fn literal_text_impl(
&self,
text: &str,
owned: Option<String>,
start: Position,
end: Position,
source_backed: bool,
) -> LiteralText {
let span = Span::from_positions(start, end);
if self.source_matches(span, text) {
LiteralText::source()
} else if source_backed {
LiteralText::cooked_source(owned.unwrap_or_else(|| text.to_owned()))
} else {
LiteralText::owned(owned.unwrap_or_else(|| text.to_owned()))
}
}
fn source_text(&self, text: String, start: Position, end: Position) -> SourceText {
let span = Span::from_positions(start, end);
if self.source_matches(span, &text) {
SourceText::source(span)
} else {
SourceText::cooked(span, text)
}
}
fn source_text_from_str(&self, text: &str, start: Position, end: Position) -> SourceText {
self.source_text_impl(text, None, start, end)
}
fn source_text_impl(
&self,
text: &str,
owned: Option<String>,
start: Position,
end: Position,
) -> SourceText {
let span = Span::from_positions(start, end);
if self.source_matches(span, text) {
SourceText::source(span)
} else {
SourceText::cooked(span, owned.unwrap_or_else(|| text.to_owned()))
}
}
fn empty_source_text(&self, pos: Position) -> SourceText {
SourceText::source(Span::from_positions(pos, pos))
}
fn input_prefix_ends_with(&self, end_offset: usize, ch: char) -> bool {
self.input
.get(..end_offset)
.is_some_and(|prefix| prefix.ends_with(ch))
}
fn input_span_ends_with(&self, start: Position, end: Position, ch: char) -> bool {
self.input
.get(start.offset..end.offset)
.is_some_and(|slice| slice.ends_with(ch))
}
fn input_suffix_starts_with(&self, start_offset: usize, ch: char) -> bool {
self.input
.get(start_offset..)
.is_some_and(|suffix| suffix.starts_with(ch))
}
fn subscript_source_text(&self, raw: &str, span: Span) -> (SourceText, Option<SourceText>) {
if raw.len() >= 2
&& ((raw.starts_with('"') && raw.ends_with('"'))
|| (raw.starts_with('\'') && raw.ends_with('\'')))
{
let raw_text = raw.to_string();
let raw = if self.source_matches(span, raw) {
SourceText::source(span)
} else {
SourceText::cooked(span, raw_text.clone())
};
let cooked = raw_text[1..raw_text.len() - 1].to_string();
return (self.source_text(cooked, span.start, span.end), Some(raw));
}
let text = if self.source_matches(span, raw) {
SourceText::source(span)
} else {
SourceText::cooked(span, raw.to_string())
};
(text, None)
}
fn subscript_from_source_text(
&self,
text: SourceText,
raw: Option<SourceText>,
interpretation: SubscriptInterpretation,
) -> Subscript {
let kind = match text.slice(self.input).trim() {
"@" => SubscriptKind::Selector(SubscriptSelector::At),
"*" => SubscriptKind::Selector(SubscriptSelector::Star),
_ => SubscriptKind::Ordinary,
};
let word_ast = if matches!(kind, SubscriptKind::Ordinary) {
Some(self.parse_source_text_as_word(raw.as_ref().unwrap_or(&text)))
} else {
None
};
let arithmetic_ast = if matches!(kind, SubscriptKind::Ordinary) {
self.simple_subscript_arithmetic_ast(&text)
.or_else(|| self.maybe_parse_source_text_as_arithmetic(&text))
} else {
None
};
Subscript {
text,
raw,
kind,
interpretation,
word_ast,
arithmetic_ast,
}
}
fn simple_subscript_arithmetic_ast(&self, text: &SourceText) -> Option<ArithmeticExprNode> {
if !text.is_source_backed() {
return None;
}
let raw = text.slice(self.input);
if raw.is_empty() || raw.trim() != raw {
return None;
}
let span = text.span();
if raw.bytes().all(|byte| byte.is_ascii_digit()) {
return Some(ArithmeticExprNode::new(
ArithmeticExpr::Number(SourceText::source(span)),
span,
));
}
if Self::is_valid_identifier(raw) {
return Some(ArithmeticExprNode::new(
ArithmeticExpr::Variable(Name::from(raw)),
span,
));
}
None
}
fn subscript_from_text(
&self,
raw: &str,
span: Span,
interpretation: SubscriptInterpretation,
) -> Subscript {
let (text, raw) = self.subscript_source_text(raw, span);
self.subscript_from_source_text(text, raw, interpretation)
}
fn var_ref(
&self,
name: impl Into<Name>,
name_span: Span,
subscript: Option<Subscript>,
span: Span,
) -> VarRef {
VarRef {
name: name.into(),
name_span,
subscript: subscript.map(Box::new),
span,
}
}
fn parameter_var_ref(
&self,
part_start: Position,
prefix: &str,
name: &str,
subscript: Option<Subscript>,
part_end: Position,
) -> VarRef {
let name_start = part_start.advanced_by(prefix);
let name_span = Span::from_positions(name_start, name_start.advanced_by(name));
self.var_ref(
Name::from(name),
name_span,
subscript,
Span::from_positions(part_start, part_end),
)
}
fn parameter_word_part_from_legacy(
&self,
part: WordPart,
part_start: Position,
part_end: Position,
source_backed: bool,
) -> WordPart {
let span = Span::from_positions(part_start, part_end);
let raw_body = self.parameter_raw_body_from_legacy(&part, span, source_backed);
let raw_body_text = raw_body.slice(self.input).to_string();
let syntax = match part {
WordPart::ParameterExpansion {
reference,
operator,
operand,
operand_word_ast,
colon_variant,
} => Some(BourneParameterExpansion::Operation {
reference,
operator: self.enrich_parameter_operator(operator),
operand,
operand_word_ast,
colon_variant,
}),
WordPart::Length(reference) | WordPart::ArrayLength(reference) => {
Some(BourneParameterExpansion::Length { reference })
}
WordPart::ArrayAccess(reference) => {
Some(BourneParameterExpansion::Access { reference })
}
WordPart::ArrayIndices(reference) => {
Some(BourneParameterExpansion::Indices { reference })
}
WordPart::Substring {
reference,
offset,
offset_ast,
offset_word_ast,
length,
length_ast,
length_word_ast,
}
| WordPart::ArraySlice {
reference,
offset,
offset_ast,
offset_word_ast,
length,
length_ast,
length_word_ast,
} => Some(BourneParameterExpansion::Slice {
reference,
offset,
offset_ast,
offset_word_ast,
length,
length_ast,
length_word_ast,
}),
WordPart::IndirectExpansion {
reference,
operator,
operand,
operand_word_ast,
colon_variant,
} => Some(BourneParameterExpansion::Indirect {
reference,
operator: operator.map(|operator| self.enrich_parameter_operator(operator)),
operand,
operand_word_ast,
colon_variant,
}),
WordPart::PrefixMatch { prefix, kind } => {
Some(BourneParameterExpansion::PrefixMatch { prefix, kind })
}
WordPart::Transformation {
reference,
operator,
} => Some(BourneParameterExpansion::Transformation {
reference,
operator,
}),
WordPart::Variable(name) if raw_body_text == name.as_str() => {
Some(BourneParameterExpansion::Access {
reference: self.parameter_var_ref(
part_start,
"${",
name.as_str(),
None,
part_end,
),
})
}
other => return other,
};
let Some(syntax) = syntax else {
unreachable!("matched Some above");
};
WordPart::Parameter(Box::new(ParameterExpansion {
syntax: ParameterExpansionSyntax::Bourne(syntax),
span,
raw_body,
}))
}
fn enrich_parameter_operator(&self, operator: Box<ParameterOp>) -> Box<ParameterOp> {
match *operator {
ParameterOp::ReplaceFirst {
pattern,
replacement,
..
} => Box::new(ParameterOp::ReplaceFirst {
pattern,
replacement_word_ast: Box::new(self.parse_source_text_as_word(&replacement)),
replacement,
}),
ParameterOp::ReplaceAll {
pattern,
replacement,
..
} => Box::new(ParameterOp::ReplaceAll {
pattern,
replacement_word_ast: Box::new(self.parse_source_text_as_word(&replacement)),
replacement,
}),
ParameterOp::UseDefault
| ParameterOp::AssignDefault
| ParameterOp::UseReplacement
| ParameterOp::Error
| ParameterOp::RemovePrefixShort { .. }
| ParameterOp::RemovePrefixLong { .. }
| ParameterOp::RemoveSuffixShort { .. }
| ParameterOp::RemoveSuffixLong { .. }
| ParameterOp::UpperFirst
| ParameterOp::UpperAll
| ParameterOp::LowerFirst
| ParameterOp::LowerAll => operator,
}
}
fn parameter_raw_body_from_legacy(
&self,
part: &WordPart,
span: Span,
source_backed: bool,
) -> SourceText {
if source_backed && span.end.offset <= self.input.len() {
let syntax = span.slice(self.input);
if let Some(body) = syntax
.strip_prefix("${")
.and_then(|syntax| syntax.strip_suffix('}'))
{
let start = span.start.advanced_by("${");
let end = start.advanced_by(body);
return SourceText::source(Span::from_positions(start, end));
}
}
let mut syntax = String::new();
self.push_word_part_syntax(&mut syntax, part, span);
let body = syntax
.strip_prefix("${")
.and_then(|syntax| syntax.strip_suffix('}'))
.unwrap_or(syntax.as_str())
.to_string();
SourceText::from(body)
}
fn zsh_parameter_word_part(
&mut self,
raw_body: SourceText,
part_start: Position,
part_end: Position,
) -> WordPart {
let syntax = self.parse_zsh_parameter_syntax(&raw_body, raw_body.span().start);
WordPart::Parameter(Box::new(ParameterExpansion {
syntax: ParameterExpansionSyntax::Zsh(syntax),
span: Span::from_positions(part_start, part_end),
raw_body,
}))
}
fn parse_zsh_modifier_group(
&self,
text: &str,
base: Position,
start: usize,
) -> Option<(usize, Vec<ZshModifier>)> {
let rest = text.get(start..)?;
if !rest.starts_with('(') {
return None;
}
let close_rel = rest[1..].find(')')?;
let close = start + 1 + close_rel;
let group_text = &text[start..=close];
let inner = &text[start + 1..close];
let group_start = base.advanced_by(&text[..start]);
let group_span = Span::from_positions(group_start, group_start.advanced_by(group_text));
let mut modifiers = Vec::new();
let mut index = 0usize;
while index < inner.len() {
let name = inner[index..].chars().next()?;
index += name.len_utf8();
let mut argument_delimiter = None;
let mut argument = None;
if matches!(name, 's' | 'j')
&& let Some(delimiter) = inner[index..].chars().next()
{
index += delimiter.len_utf8();
let argument_start = index;
while index < inner.len() {
let ch = inner[index..].chars().next()?;
if ch == delimiter {
let argument_text = &inner[argument_start..index];
let argument_base =
group_start.advanced_by(&group_text[..1 + argument_start]);
let argument_end = argument_base.advanced_by(argument_text);
argument_delimiter = Some(delimiter);
argument = Some(self.source_text(
argument_text.to_string(),
argument_base,
argument_end,
));
index += delimiter.len_utf8();
break;
}
index += ch.len_utf8();
}
}
let argument_word_ast = argument
.as_ref()
.map(|argument| Box::new(self.parse_source_text_as_word(argument)));
modifiers.push(ZshModifier {
name,
argument,
argument_word_ast,
argument_delimiter,
span: group_span,
});
}
Some((close + 1, modifiers))
}
fn parse_zsh_parameter_syntax(
&mut self,
raw_body: &SourceText,
base: Position,
) -> ZshParameterExpansion {
let text = raw_body.slice(self.input);
let mut index = 0;
let mut modifiers = Vec::new();
let mut length_prefix = None;
let source_backed = raw_body.is_source_backed();
while text[index..].starts_with('(')
&& let Some((next_index, group_modifiers)) =
self.parse_zsh_modifier_group(text, base, index)
{
modifiers.extend(group_modifiers);
index = next_index;
}
while index < text.len() {
let Some(flag) = text[index..].chars().next() else {
break;
};
match flag {
'=' | '~' | '^' => {
let modifier_start = base.advanced_by(&text[..index]);
let modifier_end =
modifier_start.advanced_by(&text[index..index + flag.len_utf8()]);
modifiers.push(ZshModifier {
name: flag,
argument: None,
argument_word_ast: None,
argument_delimiter: None,
span: Span::from_positions(modifier_start, modifier_end),
});
index += flag.len_utf8();
}
'#' if length_prefix.is_none() => {
let prefix_start = base.advanced_by(&text[..index]);
let prefix_end = prefix_start.advanced_by("#");
length_prefix = Some(Span::from_positions(prefix_start, prefix_end));
index += '#'.len_utf8();
}
_ => break,
}
}
let (target, operation_index) = if text[index..].starts_with("${") {
let end = self
.find_matching_parameter_end(&text[index..])
.unwrap_or(text.len() - index);
let nested_text = &text[index..index + end];
let target =
self.parse_nested_parameter_target(nested_text, base.advanced_by(&text[..index]));
(target, index + end)
} else if text[index..].starts_with(':') || text[index..].is_empty() {
(ZshExpansionTarget::Empty, index)
} else {
let end = self
.find_zsh_operation_start(&text[index..])
.map(|offset| index + offset)
.unwrap_or(text.len());
let raw_target = &text[index..end];
let trimmed = raw_target.trim();
let target = if trimmed.is_empty() {
ZshExpansionTarget::Empty
} else {
let leading = raw_target
.len()
.saturating_sub(raw_target.trim_start().len());
let target_base = base.advanced_by(&text[..index + leading]);
self.parse_zsh_target_from_text(
trimmed,
target_base,
source_backed && leading == 0 && trimmed.len() == raw_target.len(),
)
};
(target, end)
};
let operation = (operation_index < text.len()).then(|| {
self.parse_zsh_parameter_operation(
&text[operation_index..],
base.advanced_by(&text[..operation_index]),
)
});
ZshParameterExpansion {
target,
modifiers,
length_prefix,
operation,
}
}
fn parse_zsh_target_from_text(
&mut self,
text: &str,
base: Position,
source_backed: bool,
) -> ZshExpansionTarget {
let trimmed = text.trim();
if trimmed.is_empty() {
return ZshExpansionTarget::Empty;
}
if trimmed.starts_with("${") && trimmed.ends_with('}') {
return self.parse_nested_parameter_target(trimmed, base);
}
if let Some(reference) = self.maybe_parse_loose_var_ref_target(trimmed) {
return ZshExpansionTarget::Reference(reference);
}
let span = Span::from_positions(base, base.advanced_by(trimmed));
let word = self.parse_word_with_context(trimmed, span, base, source_backed);
if let Some(reference) =
self.parse_var_ref_from_word(&word, SubscriptInterpretation::Contextual)
{
ZshExpansionTarget::Reference(reference)
} else {
ZshExpansionTarget::Word(Box::new(word))
}
}
fn maybe_parse_loose_var_ref_target(&self, text: &str) -> Option<VarRef> {
let trimmed = text.trim();
Self::looks_like_plain_parameter_access(trimmed).then(|| self.parse_loose_var_ref(trimmed))
}
fn is_plain_special_parameter_name(name: &str) -> bool {
matches!(name, "#" | "$" | "!" | "*" | "@" | "?" | "-") || name == "0"
}
fn is_plain_parameter_access_name(name: &str) -> bool {
Self::is_valid_identifier(name)
|| name.bytes().all(|byte| byte.is_ascii_digit())
|| Self::is_plain_special_parameter_name(name)
}
fn looks_like_plain_parameter_access(text: &str) -> bool {
let trimmed = text.trim();
if trimmed.is_empty() {
return false;
}
let name = if let Some(open) = trimmed.find('[') {
if !trimmed.ends_with(']') {
return false;
}
&trimmed[..open]
} else {
trimmed
};
Self::is_plain_parameter_access_name(name)
|| name
.strip_prefix('+')
.is_some_and(Self::is_plain_parameter_access_name)
}
fn parse_nested_parameter_target(&mut self, text: &str, base: Position) -> ZshExpansionTarget {
if !(text.starts_with("${") && text.ends_with('}')) {
return self.parse_zsh_target_from_text(text, base, false);
}
let raw_body_start = base.advanced_by("${");
let raw_body = self.source_text(
text[2..text.len() - 1].to_string(),
raw_body_start,
base.advanced_by(&text[..text.len() - 1]),
);
let raw_body_text = raw_body.slice(self.input);
let has_operation = self.find_zsh_operation_start(raw_body_text).is_some();
let syntax = if Self::looks_like_plain_parameter_access(raw_body_text) && !has_operation {
ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access {
reference: self.parse_loose_var_ref(raw_body_text),
})
} else if raw_body_text.starts_with('(')
|| raw_body_text.starts_with(':')
|| raw_body_text.starts_with('=')
|| raw_body_text.starts_with('^')
|| raw_body_text.starts_with('~')
|| raw_body_text.starts_with('.')
|| raw_body_text.starts_with('#')
|| raw_body_text.starts_with('"')
|| raw_body_text.starts_with('\'')
|| raw_body_text.starts_with('$')
|| has_operation
{
ParameterExpansionSyntax::Zsh(
self.parse_zsh_parameter_syntax(&raw_body, raw_body_start),
)
} else {
ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access {
reference: self.parse_loose_var_ref(raw_body_text),
})
};
ZshExpansionTarget::Nested(Box::new(ParameterExpansion {
syntax,
span: Span::from_positions(base, base.advanced_by(text)),
raw_body,
}))
}
fn parse_loose_var_ref(&self, text: &str) -> VarRef {
let trimmed = text.trim();
if let Some(open) = trimmed.find('[')
&& trimmed.ends_with(']')
{
let name = &trimmed[..open];
let subscript_text = &trimmed[open + 1..trimmed.len() - 1];
let subscript = self.subscript_from_source_text(
SourceText::from(subscript_text.to_string()),
None,
SubscriptInterpretation::Contextual,
);
return VarRef {
name: Name::from(name),
name_span: Span::new(),
subscript: Some(Box::new(subscript)),
span: Span::new(),
};
}
VarRef {
name: Name::from(trimmed),
name_span: Span::new(),
subscript: None,
span: Span::new(),
}
}
fn find_matching_parameter_end(&self, text: &str) -> Option<usize> {
let mut depth = 0_i32;
let mut chars = text.char_indices().peekable();
while let Some((index, ch)) = chars.next() {
match ch {
'$' if chars.peek().is_some_and(|(_, next)| *next == '{') => {
depth += 1;
}
'}' => {
depth -= 1;
if depth == 0 {
return Some(index + ch.len_utf8());
}
}
_ => {}
}
}
None
}
fn find_zsh_operation_start(&self, text: &str) -> Option<usize> {
let mut bracket_depth = 0_usize;
let mut in_single = false;
let mut in_double = false;
let mut escaped = false;
for (index, ch) in text.char_indices() {
if escaped {
escaped = false;
continue;
}
match ch {
'\\' if !in_single => escaped = true,
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
'[' if !in_single && !in_double => bracket_depth += 1,
']' if !in_single && !in_double && bracket_depth > 0 => bracket_depth -= 1,
':' if !in_single && !in_double && bracket_depth == 0 => return Some(index),
'#' | '%' | '/' | '^' | ',' | '~'
if !in_single && !in_double && bracket_depth == 0 && index > 0 =>
{
return Some(index);
}
_ => {}
}
}
None
}
fn zsh_operation_source_text(
&self,
text: &str,
base: Position,
start: usize,
end: usize,
) -> SourceText {
self.source_text(
text[start..end].to_string(),
base.advanced_by(&text[..start]),
base.advanced_by(&text[..end]),
)
}
fn find_zsh_top_level_delimiter(&self, text: &str, delimiter: char) -> Option<usize> {
let mut chars = text.char_indices().peekable();
let mut in_single = false;
let mut in_double = false;
let mut escaped = false;
let mut brace_depth = 0_usize;
let mut paren_depth = 0_usize;
while let Some((index, ch)) = chars.next() {
if escaped {
escaped = false;
continue;
}
match ch {
'\\' if !in_single => escaped = true,
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
'$' if !in_single => {
if let Some((_, next)) = chars.peek() {
if *next == '{' {
brace_depth += 1;
chars.next();
} else if *next == '(' {
paren_depth += 1;
chars.next();
if let Some((_, after)) = chars.peek()
&& *after == '('
{
paren_depth += 1;
chars.next();
}
}
}
}
'}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
_ if ch == delimiter
&& !in_single
&& !in_double
&& brace_depth == 0
&& paren_depth == 0 =>
{
return Some(index);
}
_ => {}
}
}
None
}
fn zsh_simple_modifier_suffix_segment(segment: &str) -> bool {
let mut chars = segment.chars();
let Some(first) = chars.next() else {
return false;
};
match first {
'a' | 'A' | 'c' | 'e' | 'l' | 'P' | 'q' | 'Q' | 'r' | 'u' => chars.next().is_none(),
'h' | 't' => chars.all(|ch| ch.is_ascii_digit()),
_ => false,
}
}
fn zsh_modifier_suffix_candidate(rest: &str) -> bool {
if rest.is_empty() {
return false;
}
let Some(first) = rest.chars().next() else {
return false;
};
if first.is_ascii_digit()
|| first.is_ascii_whitespace()
|| matches!(first, '$' | '\'' | '"' | '(' | '{')
{
return false;
}
rest.split(':')
.all(Self::zsh_simple_modifier_suffix_segment)
}
fn zsh_slice_candidate(rest: &str) -> bool {
let Some(first) = rest.chars().next() else {
return false;
};
!Self::zsh_modifier_suffix_candidate(rest)
&& (first.is_ascii_alphanumeric()
|| first == '_'
|| first.is_ascii_whitespace()
|| matches!(first, '$' | '\'' | '"' | '(' | '{'))
}
fn parse_zsh_parameter_operation(&self, text: &str, base: Position) -> ZshExpansionOperation {
if let Some(operand) = text.strip_prefix(":#") {
let operand = self.source_text(
operand.to_string(),
base.advanced_by(":#"),
base.advanced_by(text),
);
return ZshExpansionOperation::PatternOperation {
kind: ZshPatternOp::Filter,
operand_word_ast: Box::new(self.parse_source_text_as_word(&operand)),
operand,
};
}
if let Some((kind, operand)) = text
.strip_prefix(":-")
.map(|operand| (ZshDefaultingOp::UseDefault, operand))
.or_else(|| {
text.strip_prefix(":=")
.map(|operand| (ZshDefaultingOp::AssignDefault, operand))
})
.or_else(|| {
text.strip_prefix(":+")
.map(|operand| (ZshDefaultingOp::UseReplacement, operand))
})
.or_else(|| {
text.strip_prefix(":?")
.map(|operand| (ZshDefaultingOp::Error, operand))
})
{
let operand = self.source_text(
operand.to_string(),
base.advanced_by(&text[..2]),
base.advanced_by(text),
);
return ZshExpansionOperation::Defaulting {
kind,
operand_word_ast: Box::new(self.parse_source_text_as_word(&operand)),
operand,
colon_variant: true,
};
}
if let Some((kind, prefix_len)) = [
("##", ZshTrimOp::RemovePrefixLong),
("#", ZshTrimOp::RemovePrefixShort),
("%%", ZshTrimOp::RemoveSuffixLong),
("%", ZshTrimOp::RemoveSuffixShort),
]
.into_iter()
.find_map(|(prefix, kind)| text.starts_with(prefix).then_some((kind, prefix.len())))
{
let operand = self.zsh_operation_source_text(text, base, prefix_len, text.len());
return ZshExpansionOperation::TrimOperation {
kind,
operand_word_ast: Box::new(self.parse_source_text_as_word(&operand)),
operand,
};
}
if let Some((kind, prefix_len)) = [
("//", ZshReplacementOp::ReplaceAll),
("/#", ZshReplacementOp::ReplacePrefix),
("/%", ZshReplacementOp::ReplaceSuffix),
("/", ZshReplacementOp::ReplaceFirst),
]
.into_iter()
.find_map(|(prefix, kind)| text.starts_with(prefix).then_some((kind, prefix.len())))
{
let rest = &text[prefix_len..];
let separator = self.find_zsh_top_level_delimiter(rest, '/');
let pattern_end = separator.unwrap_or(rest.len());
let pattern =
self.zsh_operation_source_text(text, base, prefix_len, prefix_len + pattern_end);
let replacement = separator.map(|separator| {
self.zsh_operation_source_text(text, base, prefix_len + separator + 1, text.len())
});
return ZshExpansionOperation::ReplacementOperation {
kind,
pattern_word_ast: Box::new(self.parse_source_text_as_word(&pattern)),
replacement_word_ast: self
.parse_optional_source_text_as_word(replacement.as_ref())
.map(Box::new),
pattern,
replacement,
};
}
if let Some(rest) = text.strip_prefix(':') {
if Self::zsh_modifier_suffix_candidate(rest) {
let text = self.source_text(text.to_string(), base, base.advanced_by(text));
return ZshExpansionOperation::Unknown {
word_ast: Box::new(self.parse_source_text_as_word(&text)),
text,
};
}
if Self::zsh_slice_candidate(rest) {
let separator = self.find_zsh_top_level_delimiter(rest, ':');
let offset_end = separator.unwrap_or(rest.len());
let offset = self.zsh_operation_source_text(text, base, 1, 1 + offset_end);
let length = separator.map(|separator| {
self.zsh_operation_source_text(text, base, 1 + separator + 1, text.len())
});
return ZshExpansionOperation::Slice {
offset_word_ast: Box::new(self.parse_source_text_as_word(&offset)),
length_word_ast: self
.parse_optional_source_text_as_word(length.as_ref())
.map(Box::new),
offset,
length,
};
}
}
let text = self.source_text(text.to_string(), base, base.advanced_by(text));
ZshExpansionOperation::Unknown {
word_ast: Box::new(self.parse_source_text_as_word(&text)),
text,
}
}
fn parse_explicit_arithmetic_span(
&self,
span: Option<Span>,
context: &'static str,
) -> Result<Option<ArithmeticExprNode>> {
let Some(span) = span else {
return Ok(None);
};
if span.slice(self.input).trim().is_empty() {
return Ok(None);
}
arithmetic::parse_expression(
span.slice(self.input),
span,
self.dialect,
self.max_depth.saturating_sub(self.current_depth),
self.fuel,
)
.map(Some)
.map_err(|error| match error {
Error::Parse { message, .. } => self.error(format!("{context}: {message}")),
})
}
fn parse_source_text_as_arithmetic(&self, text: &SourceText) -> Result<ArithmeticExprNode> {
arithmetic::parse_expression(
text.slice(self.input),
text.span(),
self.dialect,
self.max_depth.saturating_sub(self.current_depth),
self.fuel,
)
}
fn maybe_parse_source_text_as_arithmetic(
&self,
text: &SourceText,
) -> Option<ArithmeticExprNode> {
if !text.is_source_backed() {
return None;
}
self.parse_source_text_as_arithmetic(text).ok()
}
fn parse_source_text_as_word(&self, text: &SourceText) -> Word {
if let Some(word) = self.simple_source_text_as_word(text) {
return word;
}
let span = text.span();
let nested_profile = self
.zsh_options_at_offset(span.start.offset)
.cloned()
.map(|options| ShellProfile::with_zsh_options(self.dialect, options))
.unwrap_or_else(|| self.shell_profile.clone());
let remaining_depth = self.max_depth.saturating_sub(self.current_depth);
if remaining_depth == 0 {
return self.word_with_single_part(
self.literal_part_from_text(text.slice(self.input), span, text.is_source_backed()),
span,
);
}
let reparse_depth = (remaining_depth - 1).min(SOURCE_TEXT_WORD_REPARSE_MAX_DEPTH);
if !text.is_source_backed()
&& span.start.offset <= span.end.offset
&& span.end.offset <= self.input.len()
{
let raw = span.slice(self.input);
if raw.contains("\\\"") {
return Self::parse_word_fragment_with_limits(
self.input,
raw,
span,
reparse_depth,
self.fuel,
nested_profile.clone(),
);
}
}
Self::parse_word_fragment_with_limits(
self.input,
text.slice(self.input),
text.span(),
reparse_depth,
self.fuel,
nested_profile,
)
}
fn simple_source_text_as_word(&self, text: &SourceText) -> Option<Word> {
if !text.is_source_backed() {
return None;
}
let span = text.span();
let raw = text.slice(self.input);
if raw.is_empty() {
return Some(Word::literal_with_span("", span));
}
if let Some(word) = self.simple_quoted_source_text_as_word(raw, span) {
return Some(word);
}
if Self::word_text_needs_parse(raw)
|| raw.contains(['\'', '"', '\\'])
|| self.zsh_glob_word_parsing_enabled_at(span.start.offset)
{
return None;
}
Some(self.word_with_single_part(self.literal_part_from_text(raw, span, true), span))
}
fn simple_quoted_source_text_as_word(&self, raw: &str, span: Span) -> Option<Word> {
if raw.len() < 2 {
return None;
}
let quote = raw.as_bytes()[0];
if quote != b'\'' && quote != b'"' || raw.as_bytes().last().copied() != Some(quote) {
return None;
}
let inner = &raw[1..raw.len() - 1];
if quote == b'\'' && inner.contains('\'') {
return None;
}
let inner_start = span.start.advanced_by(&raw[..1]);
let inner_end = inner_start.advanced_by(inner);
let inner_span = Span::from_positions(inner_start, inner_end);
let part = match quote {
b'\'' => self.single_quoted_part_from_text(inner, inner_span, span, false),
b'"' => {
if Self::word_text_needs_parse(inner) || inner.contains(['\\', '"']) {
return None;
}
self.double_quoted_literal_part_from_text(inner, inner_span, span, true, false)
}
_ => unreachable!("quote is checked above"),
};
Some(self.word_with_single_part(part, span))
}
fn parse_optional_source_text_as_word(&self, text: Option<&SourceText>) -> Option<Word> {
text.map(|text| self.parse_source_text_as_word(text))
}
fn source_matches(&self, span: Span, text: &str) -> bool {
span.start.offset <= span.end.offset
&& self
.input
.get(span.start.offset..span.end.offset)
.is_some_and(|slice| slice == text)
}
fn checkpoint(&self) -> ParserCheckpoint<'a> {
ParserCheckpoint {
lexer: self.lexer.clone(),
synthetic_tokens: self.synthetic_tokens.clone(),
alias_replays: self.alias_replays.clone(),
current_token: self.current_token.clone(),
current_token_kind: self.current_token_kind,
current_keyword: self.current_keyword,
current_span: self.current_span,
peeked_token: self.peeked_token.clone(),
current_depth: self.current_depth,
fuel: self.fuel,
source_text_pattern_depth: self.source_text_pattern_depth,
comments_len: self.comments.len(),
expand_next_word: self.expand_next_word,
brace_group_depth: self.brace_group_depth,
brace_body_stack_len: self.brace_body_stack.len(),
syntax_facts_zsh_brace_if_spans_len: self.syntax_facts.zsh_brace_if_spans.len(),
syntax_facts_zsh_always_spans_len: self.syntax_facts.zsh_always_spans.len(),
syntax_facts_zsh_case_group_parts_len: self.syntax_facts.zsh_case_group_parts.len(),
#[cfg(feature = "benchmarking")]
benchmark_counters: self.benchmark_counters,
}
}
fn restore(&mut self, checkpoint: ParserCheckpoint<'a>) {
self.lexer = checkpoint.lexer;
self.synthetic_tokens = checkpoint.synthetic_tokens;
self.alias_replays = checkpoint.alias_replays;
self.current_token = checkpoint.current_token;
self.current_word_cache = None;
self.current_token_kind = checkpoint.current_token_kind;
self.current_keyword = checkpoint.current_keyword;
self.current_span = checkpoint.current_span;
self.peeked_token = checkpoint.peeked_token;
self.current_depth = checkpoint.current_depth;
self.fuel = checkpoint.fuel;
self.source_text_pattern_depth = checkpoint.source_text_pattern_depth;
self.comments.truncate(checkpoint.comments_len);
self.expand_next_word = checkpoint.expand_next_word;
self.brace_group_depth = checkpoint.brace_group_depth;
self.brace_body_stack
.truncate(checkpoint.brace_body_stack_len);
self.syntax_facts
.zsh_brace_if_spans
.truncate(checkpoint.syntax_facts_zsh_brace_if_spans_len);
self.syntax_facts
.zsh_always_spans
.truncate(checkpoint.syntax_facts_zsh_always_spans_len);
self.syntax_facts
.zsh_case_group_parts
.truncate(checkpoint.syntax_facts_zsh_case_group_parts_len);
#[cfg(feature = "benchmarking")]
{
self.benchmark_counters = checkpoint.benchmark_counters;
}
}
fn set_current_spanned(&mut self, token: LexedToken<'a>) {
#[cfg(feature = "benchmarking")]
self.maybe_record_set_current_spanned_call();
let span = token.span;
self.current_token_kind = Some(token.kind);
self.current_keyword = Self::keyword_from_token(&token);
self.current_token = Some(token);
self.current_word_cache = None;
self.current_span = span;
}
fn set_current_kind(&mut self, kind: TokenKind, span: Span) {
self.current_token_kind = Some(kind);
self.current_keyword = None;
self.current_token = Some(LexedToken::punctuation(kind).with_span(span));
self.current_word_cache = None;
self.current_span = span;
}
fn clear_current_token(&mut self) {
self.current_token = None;
self.current_word_cache = None;
self.current_token_kind = None;
self.current_keyword = None;
}
fn next_pending_token(&mut self) -> Option<LexedToken<'a>> {
if let Some(token) = self.synthetic_tokens.pop_front() {
return Some(token.materialize());
}
loop {
let replay = self.alias_replays.last_mut()?;
if let Some(token) = replay.next_token() {
return Some(token);
}
self.alias_replays.pop();
}
}
fn next_spanned_token_with_comments(&mut self) -> Option<LexedToken<'a>> {
self.next_pending_token()
.or_else(|| self.lexer.next_lexed_token_with_comments())
}
fn compile_alias_definition(&self, value: &str) -> AliasDefinition {
let source = Arc::<str>::from(value.to_string());
let mut lexer = Lexer::with_max_subst_depth(source.as_ref(), self.max_depth);
let mut tokens = Vec::new();
while let Some(token) = lexer.next_lexed_token_with_comments() {
tokens.push(token.into_shared(&source));
}
AliasDefinition {
tokens: tokens.into(),
expands_next_word: value.chars().last().is_some_and(char::is_whitespace),
}
}
fn maybe_expand_current_alias_chain(&mut self) {
if !self.expand_aliases {
self.expand_next_word = false;
return;
}
let mut seen = HashSet::new();
let mut expands_next_word = false;
loop {
if self.current_token_kind != Some(TokenKind::Word) {
break;
}
let Some(name) = self.current_token.as_ref().and_then(LexedToken::word_text) else {
break;
};
if self.current_source_word_starts_posix_function_header(name) {
break;
}
let Some(alias) = self.aliases.get(name).cloned() else {
break;
};
if !seen.insert(name.to_string()) {
break;
}
expands_next_word = alias.expands_next_word;
self.peeked_token = None;
self.alias_replays
.push(AliasReplay::new(&alias, self.current_span.start));
self.advance_raw();
}
self.expand_next_word = expands_next_word;
}
fn current_source_word_starts_posix_function_header(&self, name: &str) -> bool {
if name.contains('=') || name.contains('[') {
return false;
}
if self
.current_token
.as_ref()
.is_some_and(|token| token.flags.is_synthetic())
{
return false;
}
let Some(tail) = self.input.get(self.current_span.end.offset..) else {
return false;
};
let tail = tail.trim_start_matches([' ', '\t']);
let Some(after_left) = tail.strip_prefix('(') else {
return false;
};
let after_left = after_left.trim_start_matches([' ', '\t']);
after_left.starts_with(')')
}
fn next_word_char(
chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
cursor: &mut Position,
) -> Option<char> {
let ch = chars.next()?;
cursor.advance(ch);
Some(ch)
}
fn next_word_char_unwrap(
chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
cursor: &mut Position,
) -> char {
let Some(ch) = Self::next_word_char(chars, cursor) else {
unreachable!("word parser should only consume characters that were already peeked");
};
ch
}
fn consume_word_char_if(
chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
cursor: &mut Position,
expected: char,
) -> bool {
if chars.peek() == Some(&expected) {
Self::next_word_char_unwrap(chars, cursor);
true
} else {
false
}
}
fn read_word_while<F>(
chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
cursor: &mut Position,
mut predicate: F,
) -> String
where
F: FnMut(char) -> bool,
{
let mut text = String::new();
while let Some(&ch) = chars.peek() {
if !predicate(ch) {
break;
}
text.push(Self::next_word_char_unwrap(chars, cursor));
}
text
}
fn rebase_redirects(redirects: &mut [Redirect], base: Position) {
for redirect in redirects {
redirect.span = redirect.span.rebased(base);
redirect.fd_var_span = redirect.fd_var_span.map(|span| span.rebased(base));
match &mut redirect.target {
RedirectTarget::Word(word) => Self::rebase_word(word, base),
RedirectTarget::Heredoc(heredoc) => {
heredoc.delimiter.span = heredoc.delimiter.span.rebased(base);
Self::rebase_word(&mut heredoc.delimiter.raw, base);
Self::rebase_heredoc_body(&mut heredoc.body, base);
}
}
}
}
fn rebase_assignments(assignments: &mut [Assignment], base: Position) {
for assignment in assignments {
assignment.span = assignment.span.rebased(base);
Self::rebase_var_ref(&mut assignment.target, base);
match &mut assignment.value {
AssignmentValue::Scalar(word) => Self::rebase_word(word, base),
AssignmentValue::Compound(array) => Self::rebase_array_expr(array, base),
}
}
}
fn error(&self, message: impl Into<String>) -> Error {
Error::parse_at(
message,
self.current_span.start.line,
self.current_span.start.column,
)
}
fn ensure_feature(
&self,
enabled: bool,
feature: &str,
unsupported_message: &str,
) -> Result<()> {
if enabled {
Ok(())
} else {
Err(self.error(format!("{feature} {unsupported_message}")))
}
}
fn ensure_double_bracket(&self) -> Result<()> {
self.ensure_feature(
self.dialect.features().double_bracket,
"[[ ]] conditionals",
"are not available in this shell mode",
)
}
fn ensure_arithmetic_for(&self) -> Result<()> {
self.ensure_feature(
self.dialect.features().arithmetic_for,
"c-style for loops",
"are not available in this shell mode",
)
}
fn ensure_coproc(&self) -> Result<()> {
self.ensure_feature(
self.dialect.features().coproc_keyword,
"coprocess commands",
"are not available in this shell mode",
)
}
fn ensure_arithmetic_command(&self) -> Result<()> {
self.ensure_feature(
self.dialect.features().arithmetic_command,
"arithmetic commands",
"are not available in this shell mode",
)
}
fn ensure_select_loop(&self) -> Result<()> {
self.ensure_feature(
self.dialect.features().select_loop,
"select loops",
"are not available in this shell mode",
)
}
fn ensure_repeat_loop(&self) -> Result<()> {
self.ensure_feature(
self.zsh_short_repeat_enabled(),
"repeat loops",
"are not available in this shell mode",
)
}
fn ensure_foreach_loop(&self) -> Result<()> {
self.ensure_feature(
self.zsh_short_loops_enabled(),
"foreach loops",
"are not available in this shell mode",
)
}
fn ensure_function_keyword(&self) -> Result<()> {
self.ensure_feature(
self.dialect.features().function_keyword,
"function keyword definitions",
"are not available in this shell mode",
)
}
fn tick(&mut self) -> Result<()> {
if self.fuel == 0 {
let used = self.max_fuel - self.fuel;
return Err(Error::parse(format!(
"parser fuel exhausted ({} operations, max {})",
used, self.max_fuel
)));
}
self.fuel -= 1;
Ok(())
}
fn push_depth(&mut self) -> Result<()> {
self.current_depth += 1;
if self.current_depth > self.max_depth {
return Err(Error::parse(format!(
"AST nesting too deep ({} levels, max {})",
self.current_depth, self.max_depth
)));
}
Ok(())
}
fn pop_depth(&mut self) {
if self.current_depth > 0 {
self.current_depth -= 1;
}
}
fn check_error_token(&self) -> Result<()> {
if self.current_token_kind == Some(TokenKind::Error) {
let msg = self
.current_token
.as_ref()
.and_then(LexedToken::error_kind)
.map(|kind| kind.message())
.unwrap_or("unknown lexer error");
return Err(self.error(format!("syntax error: {}", msg)));
}
Ok(())
}
fn parse_diagnostic_from_error(&self, error: Error) -> ParseDiagnostic {
let Error::Parse { message, .. } = error;
ParseDiagnostic {
message,
span: self.current_span,
}
}
fn compound_span(compound: &CompoundCommand) -> Span {
match compound {
CompoundCommand::If(command) => command.span,
CompoundCommand::For(command) => command.span,
CompoundCommand::Repeat(command) => command.span,
CompoundCommand::Foreach(command) => command.span,
CompoundCommand::ArithmeticFor(command) => command.span,
CompoundCommand::While(command) => command.span,
CompoundCommand::Until(command) => command.span,
CompoundCommand::Case(command) => command.span,
CompoundCommand::Select(command) => command.span,
CompoundCommand::Subshell(body) | CompoundCommand::BraceGroup(body) => body.span,
CompoundCommand::Arithmetic(command) => command.span,
CompoundCommand::Time(command) => command.span,
CompoundCommand::Conditional(command) => command.span,
CompoundCommand::Coproc(command) => command.span,
CompoundCommand::Always(command) => command.span,
}
}
fn stmt_seq_with_span(span: Span, stmts: Vec<Stmt>) -> StmtSeq {
StmtSeq {
leading_comments: Vec::new(),
stmts,
trailing_comments: Vec::new(),
span,
}
}
fn binary_stmt(left: Stmt, op: BinaryOp, op_span: Span, right: Stmt) -> Stmt {
let span = left.span.merge(right.span);
Stmt {
leading_comments: Vec::new(),
command: AstCommand::Binary(BinaryCommand {
left: Box::new(left),
op,
op_span,
right: Box::new(right),
span,
}),
negated: false,
redirects: Box::default(),
terminator: None,
terminator_span: None,
inline_comment: None,
span,
}
}
fn lower_builtin_command(
builtin: BuiltinCommand,
) -> (AstBuiltinCommand, SmallVec<[Redirect; 1]>, Span) {
match builtin {
BuiltinCommand::Break(command) => {
let span = command.span;
let redirects = command.redirects;
(
AstBuiltinCommand::Break(AstBreakCommand {
depth: command.depth,
extra_args: command.extra_args.into_vec(),
assignments: command.assignments.into_boxed_slice(),
span,
}),
redirects,
span,
)
}
BuiltinCommand::Continue(command) => {
let span = command.span;
let redirects = command.redirects;
(
AstBuiltinCommand::Continue(AstContinueCommand {
depth: command.depth,
extra_args: command.extra_args.into_vec(),
assignments: command.assignments.into_boxed_slice(),
span,
}),
redirects,
span,
)
}
BuiltinCommand::Return(command) => {
let span = command.span;
let redirects = command.redirects;
(
AstBuiltinCommand::Return(AstReturnCommand {
code: command.code,
extra_args: command.extra_args.into_vec(),
assignments: command.assignments.into_boxed_slice(),
span,
}),
redirects,
span,
)
}
BuiltinCommand::Exit(command) => {
let span = command.span;
let redirects = command.redirects;
(
AstBuiltinCommand::Exit(AstExitCommand {
code: command.code,
extra_args: command.extra_args.into_vec(),
assignments: command.assignments.into_boxed_slice(),
span,
}),
redirects,
span,
)
}
}
}
fn lower_non_sequence_command_to_stmt(command: Command) -> Stmt {
match command {
Command::Simple(command) => Stmt {
leading_comments: Vec::new(),
command: AstCommand::Simple(AstSimpleCommand {
name: command.name,
args: command.args.into_vec(),
assignments: command.assignments.into_boxed_slice(),
span: command.span,
}),
negated: false,
redirects: command.redirects.into_boxed_slice(),
terminator: None,
terminator_span: None,
inline_comment: None,
span: command.span,
},
Command::Builtin(command) => {
let (command, redirects, span) = Self::lower_builtin_command(command);
Stmt {
leading_comments: Vec::new(),
command: AstCommand::Builtin(command),
negated: false,
redirects: redirects.into_boxed_slice(),
terminator: None,
terminator_span: None,
inline_comment: None,
span,
}
}
Command::Decl(command) => {
let command = *command;
Stmt {
leading_comments: Vec::new(),
command: AstCommand::Decl(AstDeclClause {
variant: command.variant,
variant_span: command.variant_span,
operands: command.operands.into_vec(),
assignments: command.assignments.into_boxed_slice(),
span: command.span,
}),
negated: false,
redirects: command.redirects.into_boxed_slice(),
terminator: None,
terminator_span: None,
inline_comment: None,
span: command.span,
}
}
Command::Compound(compound, redirects) => {
let span = Self::compound_span(&compound);
Stmt {
leading_comments: Vec::new(),
command: AstCommand::Compound(*compound),
negated: false,
redirects: redirects.into_boxed_slice(),
terminator: None,
terminator_span: None,
inline_comment: None,
span,
}
}
Command::Function(function) => Stmt {
leading_comments: Vec::new(),
span: function.span,
command: AstCommand::Function(function),
negated: false,
redirects: Box::default(),
terminator: None,
terminator_span: None,
inline_comment: None,
},
Command::AnonymousFunction(function, redirects) => Stmt {
leading_comments: Vec::new(),
span: function.span,
command: AstCommand::AnonymousFunction(function),
negated: false,
redirects: redirects.into_boxed_slice(),
terminator: None,
terminator_span: None,
inline_comment: None,
},
}
}
fn comment_start(comment: Comment) -> usize {
usize::from(comment.range.start())
}
fn is_inline_comment(source: &str, stmt: &Stmt, comment: Comment) -> bool {
let comment_start = Self::comment_start(comment);
if comment_start < stmt.span.end.offset {
return false;
}
source
.get(stmt.span.end.offset..comment_start)
.is_some_and(|gap| !gap.contains('\n'))
}
fn take_comments_before(
comments: &mut VecDeque<Comment>,
end_offset: usize,
) -> VecDeque<Comment> {
let mut taken = VecDeque::new();
while comments
.front()
.is_some_and(|comment| Self::comment_start(*comment) < end_offset)
{
let Some(comment) = comments.pop_front() else {
unreachable!("front comment should exist while draining");
};
taken.push_back(comment);
}
taken
}
fn attach_comments_to_file(&self, file: &mut File) {
let mut comments = self.comments.iter().copied().collect::<VecDeque<_>>();
Self::attach_comments_to_stmt_seq_with_source(self.input, &mut file.body, &mut comments);
file.body.trailing_comments.extend(comments);
}
fn attach_comments_to_stmt_seq_with_source(
source: &str,
sequence: &mut StmtSeq,
comments: &mut VecDeque<Comment>,
) {
if sequence.stmts.is_empty() {
sequence
.trailing_comments
.extend(Self::take_comments_before(
comments,
sequence.span.end.offset,
));
return;
}
for (index, stmt) in sequence.stmts.iter_mut().enumerate() {
let leading = Self::take_comments_before(comments, stmt.span.start.offset);
if index == 0 {
sequence.leading_comments.extend(leading);
} else {
stmt.leading_comments.extend(leading);
}
let mut nested = Self::take_comments_before(comments, stmt.span.end.offset);
Self::attach_comments_to_stmt_with_source(source, stmt, &mut nested);
if !nested.is_empty() {
stmt.leading_comments.extend(nested);
}
if stmt.inline_comment.is_none()
&& comments
.front()
.is_some_and(|comment| Self::is_inline_comment(source, stmt, *comment))
{
stmt.inline_comment = comments.pop_front();
}
}
sequence
.trailing_comments
.extend(Self::take_comments_before(
comments,
sequence.span.end.offset,
));
}
fn attach_comments_to_stmt_with_source(
source: &str,
stmt: &mut Stmt,
comments: &mut VecDeque<Comment>,
) {
match &mut stmt.command {
AstCommand::Binary(binary) => {
let mut left_comments =
Self::take_comments_before(comments, binary.left.span.end.offset);
Self::attach_comments_to_stmt_with_source(
source,
binary.left.as_mut(),
&mut left_comments,
);
if !left_comments.is_empty() {
binary.left.leading_comments.extend(left_comments);
}
let mut right_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_with_source(
source,
binary.right.as_mut(),
&mut right_comments,
);
if !right_comments.is_empty() {
binary.right.leading_comments.extend(right_comments);
}
}
AstCommand::Compound(compound) => {
Self::attach_comments_to_compound_with_source(source, compound, comments);
}
AstCommand::Function(function) => {
let mut body_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_with_source(
source,
function.body.as_mut(),
&mut body_comments,
);
if !body_comments.is_empty() {
function.body.leading_comments.extend(body_comments);
}
}
AstCommand::AnonymousFunction(function) => {
let mut body_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_with_source(
source,
function.body.as_mut(),
&mut body_comments,
);
if !body_comments.is_empty() {
function.body.leading_comments.extend(body_comments);
}
}
AstCommand::Simple(_) | AstCommand::Builtin(_) | AstCommand::Decl(_) => {}
}
}
fn attach_comments_to_compound_with_source(
source: &str,
command: &mut CompoundCommand,
comments: &mut VecDeque<Comment>,
) {
match command {
CompoundCommand::If(command) => {
let mut condition =
Self::take_comments_before(comments, command.condition.span.end.offset);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut command.condition,
&mut condition,
);
command.condition.trailing_comments.extend(condition);
let mut then_branch =
Self::take_comments_before(comments, command.then_branch.span.end.offset);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut command.then_branch,
&mut then_branch,
);
command.then_branch.trailing_comments.extend(then_branch);
for (condition_seq, body_seq) in &mut command.elif_branches {
let mut elif_condition =
Self::take_comments_before(comments, condition_seq.span.end.offset);
Self::attach_comments_to_stmt_seq_with_source(
source,
condition_seq,
&mut elif_condition,
);
condition_seq.trailing_comments.extend(elif_condition);
let mut elif_body =
Self::take_comments_before(comments, body_seq.span.end.offset);
Self::attach_comments_to_stmt_seq_with_source(source, body_seq, &mut elif_body);
body_seq.trailing_comments.extend(elif_body);
}
if let Some(else_branch) = &mut command.else_branch {
let mut else_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_seq_with_source(
source,
else_branch,
&mut else_comments,
);
else_branch.trailing_comments.extend(else_comments);
}
}
CompoundCommand::For(command) => {
let mut body_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut command.body,
&mut body_comments,
);
command.body.trailing_comments.extend(body_comments);
}
CompoundCommand::Repeat(command) => {
let mut body_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut command.body,
&mut body_comments,
);
command.body.trailing_comments.extend(body_comments);
}
CompoundCommand::Foreach(command) => {
let mut body_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut command.body,
&mut body_comments,
);
command.body.trailing_comments.extend(body_comments);
}
CompoundCommand::ArithmeticFor(command) => {
let mut body_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut command.body,
&mut body_comments,
);
command.body.trailing_comments.extend(body_comments);
}
CompoundCommand::While(command) => {
let mut condition =
Self::take_comments_before(comments, command.condition.span.end.offset);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut command.condition,
&mut condition,
);
command.condition.trailing_comments.extend(condition);
let mut body_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut command.body,
&mut body_comments,
);
command.body.trailing_comments.extend(body_comments);
}
CompoundCommand::Until(command) => {
let mut condition =
Self::take_comments_before(comments, command.condition.span.end.offset);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut command.condition,
&mut condition,
);
command.condition.trailing_comments.extend(condition);
let mut body_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut command.body,
&mut body_comments,
);
command.body.trailing_comments.extend(body_comments);
}
CompoundCommand::Case(command) => {
for case in &mut command.cases {
let mut body_comments =
Self::take_comments_before(comments, case.body.span.end.offset);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut case.body,
&mut body_comments,
);
case.body.trailing_comments.extend(body_comments);
}
}
CompoundCommand::Select(command) => {
let mut body_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut command.body,
&mut body_comments,
);
command.body.trailing_comments.extend(body_comments);
}
CompoundCommand::Subshell(body) | CompoundCommand::BraceGroup(body) => {
let mut body_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_seq_with_source(source, body, &mut body_comments);
body.trailing_comments.extend(body_comments);
}
CompoundCommand::Always(command) => {
let mut body_comments =
Self::take_comments_before(comments, command.body.span.end.offset);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut command.body,
&mut body_comments,
);
command.body.trailing_comments.extend(body_comments);
let mut always_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_seq_with_source(
source,
&mut command.always_body,
&mut always_comments,
);
command
.always_body
.trailing_comments
.extend(always_comments);
}
CompoundCommand::Time(command) => {
if let Some(inner) = &mut command.command {
let mut inner_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_with_source(
source,
inner.as_mut(),
&mut inner_comments,
);
if !inner_comments.is_empty() {
inner.leading_comments.extend(inner_comments);
}
}
}
CompoundCommand::Coproc(command) => {
let mut body_comments = std::mem::take(comments);
Self::attach_comments_to_stmt_with_source(
source,
command.body.as_mut(),
&mut body_comments,
);
if !body_comments.is_empty() {
command.body.leading_comments.extend(body_comments);
}
}
CompoundCommand::Arithmetic(_) | CompoundCommand::Conditional(_) => {}
}
}
fn advance_raw(&mut self) {
#[cfg(feature = "benchmarking")]
self.maybe_record_advance_raw_call();
if let Some(peeked) = self.peeked_token.take() {
self.set_current_spanned(peeked);
} else {
loop {
match self.next_spanned_token_with_comments() {
Some(st) if st.kind == TokenKind::Comment => {
self.maybe_record_comment(&st);
}
Some(st) => {
self.set_current_spanned(st);
break;
}
None => {
self.clear_current_token();
break;
}
}
}
}
}
#[cfg(feature = "benchmarking")]
fn maybe_record_set_current_spanned_call(&mut self) {
if let Some(counters) = &mut self.benchmark_counters {
counters.parser_set_current_spanned_calls += 1;
}
}
#[cfg(feature = "benchmarking")]
fn maybe_record_advance_raw_call(&mut self) {
if let Some(counters) = &mut self.benchmark_counters {
counters.parser_advance_raw_calls += 1;
}
}
#[cfg(feature = "benchmarking")]
fn finish_benchmark_counters(&self) -> ParserBenchmarkCounters {
let mut counters = self.benchmark_counters.unwrap_or_default();
counters.lexer_current_position_calls =
self.lexer.benchmark_counters().current_position_calls;
counters
}
fn advance(&mut self) {
let should_expand = std::mem::take(&mut self.expand_next_word);
self.advance_raw();
if should_expand {
if self
.current_token
.as_ref()
.is_some_and(|token| token.flags.is_synthetic())
{
self.expand_next_word = true;
} else {
self.maybe_expand_current_alias_chain();
}
}
}
fn peek_next(&mut self) -> Option<&LexedToken<'a>> {
if self.peeked_token.is_none() {
loop {
match self.next_spanned_token_with_comments() {
Some(st) if st.kind == TokenKind::Comment => {
self.maybe_record_comment(&st);
}
other => {
self.peeked_token = other;
break;
}
}
}
}
self.peeked_token.as_ref()
}
fn peek_next_kind(&mut self) -> Option<TokenKind> {
self.peek_next()?;
self.peeked_token.as_ref().map(|st| st.kind)
}
fn peek_next_is(&mut self, kind: TokenKind) -> bool {
self.peek_next_kind() == Some(kind)
}
fn at(&self, kind: TokenKind) -> bool {
self.current_token_kind == Some(kind)
}
fn current_token_has_leading_whitespace(&self) -> bool {
self.current_span.start.offset > 0
&& self.input[..self.current_span.start.offset]
.chars()
.next_back()
.is_some_and(|ch| matches!(ch, ' ' | '\t' | '\n'))
}
fn current_token_is_tight_to_next_token(&mut self) -> bool {
let current_end = self.current_span.end.offset;
self.peek_next()
.is_some_and(|token| token.span.start.offset == current_end)
}
fn at_in_set(&self, set: TokenSet) -> bool {
self.current_token_kind
.is_some_and(|kind| set.contains(kind))
}
fn at_word_like(&self) -> bool {
self.current_token_kind.is_some_and(TokenKind::is_word_like)
}
fn current_word_str(&self) -> Option<&str> {
self.current_token_kind
.filter(|kind| kind.is_word_like())
.and(self.current_token.as_ref())
.and_then(LexedToken::word_text)
}
fn classify_keyword(word: &str) -> Option<Keyword> {
match word.as_bytes() {
b"if" => Some(Keyword::If),
b"for" => Some(Keyword::For),
b"repeat" => Some(Keyword::Repeat),
b"foreach" => Some(Keyword::Foreach),
b"while" => Some(Keyword::While),
b"until" => Some(Keyword::Until),
b"case" => Some(Keyword::Case),
b"select" => Some(Keyword::Select),
b"time" => Some(Keyword::Time),
b"coproc" => Some(Keyword::Coproc),
b"function" => Some(Keyword::Function),
b"always" => Some(Keyword::Always),
b"then" => Some(Keyword::Then),
b"else" => Some(Keyword::Else),
b"elif" => Some(Keyword::Elif),
b"fi" => Some(Keyword::Fi),
b"do" => Some(Keyword::Do),
b"done" => Some(Keyword::Done),
b"esac" => Some(Keyword::Esac),
b"in" => Some(Keyword::In),
_ => None,
}
}
fn current_keyword(&self) -> Option<Keyword> {
self.current_keyword
}
fn looks_like_disabled_repeat_loop(&mut self) -> Result<bool> {
if self.current_keyword() != Some(Keyword::Repeat) {
return Ok(false);
}
let checkpoint = self.checkpoint();
self.advance();
if !self.at_word_like() {
self.restore(checkpoint);
return Ok(false);
}
self.advance();
let result = match self.current_token_kind {
Some(TokenKind::LeftBrace) => Ok(true),
Some(TokenKind::Semicolon) => {
self.advance();
if let Err(error) = self.skip_newlines() {
self.restore(checkpoint);
return Err(error);
}
Ok(self.current_keyword() == Some(Keyword::Do))
}
Some(TokenKind::Newline) => {
if let Err(error) = self.skip_newlines() {
self.restore(checkpoint);
return Err(error);
}
Ok(self.current_keyword() == Some(Keyword::Do))
}
_ => Ok(false),
};
self.restore(checkpoint);
result
}
fn looks_like_disabled_foreach_loop(&mut self) -> Result<bool> {
if self.current_keyword() != Some(Keyword::Foreach) {
return Ok(false);
}
let checkpoint = self.checkpoint();
self.advance();
if self.current_name_token().is_none() {
self.restore(checkpoint);
return Ok(false);
}
self.advance();
let result = if self.at(TokenKind::LeftParen) {
self.advance();
let mut saw_word = false;
while !self.at(TokenKind::RightParen) {
if !self.at_word_like() {
self.restore(checkpoint);
return Ok(false);
}
saw_word = true;
self.advance();
}
if !saw_word {
self.restore(checkpoint);
return Ok(false);
}
self.advance();
Ok(self.at(TokenKind::LeftBrace))
} else {
if self.current_keyword() != Some(Keyword::In) {
self.restore(checkpoint);
return Ok(false);
}
self.advance();
let mut saw_word = false;
let saw_separator = loop {
if self.current_keyword() == Some(Keyword::Do) {
break false;
}
match self.current_token_kind {
Some(kind) if kind.is_word_like() => {
saw_word = true;
self.advance();
}
Some(TokenKind::Semicolon) => {
self.advance();
break true;
}
Some(TokenKind::Newline) => {
if let Err(error) = self.skip_newlines() {
self.restore(checkpoint);
return Err(error);
}
break true;
}
_ => break false,
}
};
Ok(saw_word && saw_separator && self.current_keyword() == Some(Keyword::Do))
};
self.restore(checkpoint);
result
}
fn skip_newlines_with_flag(&mut self) -> Result<bool> {
let mut skipped = false;
while self.at(TokenKind::Newline) {
self.tick()?;
self.advance();
skipped = true;
}
Ok(skipped)
}
fn skip_newlines(&mut self) -> Result<()> {
self.skip_newlines_with_flag().map(|_| ())
}
}
#[cfg(test)]
mod tests;