use crate::prelude::*;
use fish_fallback::{fish_wcswidth, fish_wcwidth};
pub type SourceOffset = u32;
pub const SOURCE_OFFSET_INVALID: usize = SourceOffset::MAX as _;
pub const SOURCE_LOCATION_UNKNOWN: usize = usize::MAX;
#[derive(Copy, Clone, Default)]
pub struct ParseTreeFlags {
pub continue_after_error: bool,
pub include_comments: bool,
pub accept_incomplete_tokens: bool,
pub leave_unterminated: bool,
pub show_blank_lines: bool,
pub show_extra_semis: bool,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct ParseIssue {
pub error: bool, pub incomplete: bool, }
impl ParseIssue {
pub const ERROR: Result<(), Self> = Err(Self {
error: true,
incomplete: false,
});
pub const INCOMPLETE: Result<(), Self> = Err(Self {
error: false,
incomplete: true,
});
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)]
pub struct SourceRange {
pub start: u32,
pub length: u32,
}
impl SourceRange {
pub fn as_usize(self) -> std::ops::Range<usize> {
self.into()
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ParseTokenType {
#[default]
Invalid = 1,
String,
Pipe,
LeftBrace,
RightBrace,
Redirection,
Background,
AndAnd,
OrOr,
End,
Terminate,
Error,
TokenizerError,
Comment,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ParseKeyword {
#[default]
None,
And,
Begin,
Builtin,
Case,
Command,
Else,
End,
Exclam,
Exec,
For,
Function,
If,
In,
Not,
Or,
Switch,
Time,
While,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StatementDecoration {
None,
Command,
Builtin,
Exec,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ParseErrorCode {
#[default]
None,
Syntax,
CmdSubst,
Generic,
TokenizerUnterminatedQuote,
TokenizerUnterminatedSubshell,
TokenizerUnterminatedSlice,
TokenizerUnterminatedEscape,
TokenizerOther,
UnbalancingEnd, UnbalancingElse, UnbalancingCase, UnbalancingBrace, BareVariableAssignment, AndOrInPipeline, }
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum PipelinePosition {
None, First, Subsequent, }
impl SourceRange {
pub fn new(start: usize, length: usize) -> Self {
SourceRange {
start: start.try_into().unwrap(),
length: length.try_into().unwrap(),
}
}
pub fn start(self) -> usize {
self.start.try_into().unwrap()
}
pub fn length(self) -> usize {
self.length.try_into().unwrap()
}
pub fn end(self) -> usize {
self.start
.checked_add(self.length)
.expect("Overflow")
.try_into()
.unwrap()
}
pub fn combine(self, other: Self) -> Self {
let start = std::cmp::min(self.start, other.start);
SourceRange {
start,
length: std::cmp::max(self.end(), other.end())
.checked_sub(start.try_into().unwrap())
.expect("Overflow")
.try_into()
.unwrap(),
}
}
pub fn contains_inclusive(self, loc: usize) -> bool {
self.start() <= loc && loc - self.start() <= self.length()
}
}
impl From<SourceRange> for std::ops::Range<usize> {
fn from(value: SourceRange) -> Self {
value.start()..value.end()
}
}
impl ParseTokenType {
pub fn to_wstr(self) -> &'static wstr {
match self {
ParseTokenType::Comment => L!("ParseTokenType::comment"),
ParseTokenType::Error => L!("ParseTokenType::error"),
ParseTokenType::TokenizerError => L!("ParseTokenType::tokenizer_error"),
ParseTokenType::Background => L!("ParseTokenType::background"),
ParseTokenType::End => L!("ParseTokenType::end"),
ParseTokenType::Pipe => L!("ParseTokenType::pipe"),
ParseTokenType::LeftBrace => L!("ParseTokenType::lbrace"),
ParseTokenType::RightBrace => L!("ParseTokenType::rbrace"),
ParseTokenType::Redirection => L!("ParseTokenType::redirection"),
ParseTokenType::String => L!("ParseTokenType::string"),
ParseTokenType::AndAnd => L!("ParseTokenType::andand"),
ParseTokenType::OrOr => L!("ParseTokenType::oror"),
ParseTokenType::Terminate => L!("ParseTokenType::terminate"),
ParseTokenType::Invalid => L!("ParseTokenType::invalid"),
}
}
}
impl ParseKeyword {
pub fn to_wstr(self) -> &'static wstr {
match self {
ParseKeyword::And => L!("and"),
ParseKeyword::Begin => L!("begin"),
ParseKeyword::Builtin => L!("builtin"),
ParseKeyword::Case => L!("case"),
ParseKeyword::Command => L!("command"),
ParseKeyword::Else => L!("else"),
ParseKeyword::End => L!("end"),
ParseKeyword::Exclam => L!("!"),
ParseKeyword::Exec => L!("exec"),
ParseKeyword::For => L!("for"),
ParseKeyword::Function => L!("function"),
ParseKeyword::If => L!("if"),
ParseKeyword::In => L!("in"),
ParseKeyword::Not => L!("not"),
ParseKeyword::Or => L!("or"),
ParseKeyword::Switch => L!("switch"),
ParseKeyword::Time => L!("time"),
ParseKeyword::While => L!("while"),
_ => L!("unknown_keyword"),
}
}
}
impl fish_printf::ToArg<'static> for ParseKeyword {
fn to_arg(self) -> fish_printf::Arg<'static> {
fish_printf::Arg::WStr(self.to_wstr())
}
}
impl From<&wstr> for ParseKeyword {
fn from(s: &wstr) -> Self {
let c0 = s.as_char_slice().first().copied().unwrap_or('\0');
match c0 {
'!' if s == L!("!") => ParseKeyword::Exclam,
'a' if s == L!("and") => ParseKeyword::And,
'b' if s == L!("begin") => ParseKeyword::Begin,
'b' if s == L!("builtin") => ParseKeyword::Builtin,
'c' if s == L!("case") => ParseKeyword::Case,
'c' if s == L!("command") => ParseKeyword::Command,
'e' if s == L!("else") => ParseKeyword::Else,
'e' if s == L!("end") => ParseKeyword::End,
'e' if s == L!("exec") => ParseKeyword::Exec,
'f' if s == L!("for") => ParseKeyword::For,
'f' if s == L!("function") => ParseKeyword::Function,
'i' if s == L!("if") => ParseKeyword::If,
'i' if s == L!("in") => ParseKeyword::In,
'n' if s == L!("not") => ParseKeyword::Not,
'o' if s == L!("or") => ParseKeyword::Or,
's' if s == L!("switch") => ParseKeyword::Switch,
't' if s == L!("time") => ParseKeyword::Time,
'w' if s == L!("while") => ParseKeyword::While,
_ => ParseKeyword::None,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ParseError {
pub text: WString,
pub code: ParseErrorCode,
pub source_start: usize,
pub source_length: usize,
}
impl ParseError {
pub fn describe(self: &ParseError, src: &wstr, is_interactive: bool) -> WString {
self.describe_with_prefix(src, L!(""), is_interactive, false)
}
pub fn describe_with_prefix(
self: &ParseError,
src: &wstr,
prefix: &wstr,
is_interactive: bool,
skip_caret: bool,
) -> WString {
if skip_caret && self.text.is_empty() {
return L!("").to_owned();
}
let mut result = if prefix.is_empty() {
self.text.clone()
} else {
wgettext_fmt!("%s: %s", prefix, &self.text)
};
if skip_caret {
return result;
}
let mut start = self.source_start;
let mut len = self.source_length;
if start >= src.len() {
start = src.len() - 1;
len = 0;
}
if start + len > src.len() {
len = src.len() - self.source_start;
}
let mut line_start = 0;
if start > 0 {
let prefix = &src.as_char_slice()[..start];
let newline_left_of_start = prefix.iter().rev().position(|c| *c == '\n');
if let Some(left_of_start) = newline_left_of_start {
line_start = start - left_of_start;
}
}
let last_char_in_range = if len == 0 { start } else { start + len - 1 };
let line_end = src.as_char_slice()[last_char_in_range..]
.iter()
.position(|c| *c == '\n')
.map_or(src.len(), |pos| pos + last_char_in_range);
if start + len > line_end {
len = line_end - start;
}
assert!(line_end >= line_start);
assert!(start >= line_start);
let interactive_skip_caret = is_interactive && start == 0;
if interactive_skip_caret {
return result;
}
if !result.is_empty() {
result += "\n";
}
result += wstr::from_char_slice(&src.as_char_slice()[line_start..line_end]);
let mut caret_space_line = WString::new();
caret_space_line.reserve(start - line_start);
for i in line_start..start {
let wc = src.as_char_slice()[i];
if wc == '\t' {
caret_space_line += "\t";
} else if wc == '\n' {
caret_space_line += " ";
} else if let Some(width) = fish_wcwidth(wc) {
caret_space_line += " ".repeat(width).as_str();
}
}
result += "\n";
result.push_utfstr(&caret_space_line);
result += "^";
if len > 1 {
let width = fish_wcswidth(&src[start..start + len]).unwrap_or_default();
if width >= 2 {
result += "~".repeat(width - 2).as_str();
result += "^";
}
}
result
}
}
pub fn token_type_user_presentable_description(
type_: ParseTokenType,
keyword: ParseKeyword,
) -> WString {
if keyword != ParseKeyword::None {
return sprintf!("keyword: '%s'", keyword.to_wstr());
}
match type_ {
ParseTokenType::String => L!("a string").to_owned(),
ParseTokenType::Pipe => L!("a pipe").to_owned(),
ParseTokenType::Redirection => L!("a redirection").to_owned(),
ParseTokenType::Background => L!("a '&'").to_owned(),
ParseTokenType::LeftBrace => L!("a '{'").to_owned(),
ParseTokenType::RightBrace => L!("a '}'").to_owned(),
ParseTokenType::AndAnd => L!("'&&'").to_owned(),
ParseTokenType::OrOr => L!("'||'").to_owned(),
ParseTokenType::End => L!("end of the statement").to_owned(),
ParseTokenType::Terminate => L!("end of the input").to_owned(),
ParseTokenType::Error => L!("a parse error").to_owned(),
ParseTokenType::TokenizerError => L!("an incomplete token").to_owned(),
ParseTokenType::Comment => L!("a comment").to_owned(),
_ => sprintf!("a %s", type_.to_wstr()),
}
}
pub type ParseErrorList = Vec<ParseError>;
pub fn parse_error_offset_source_start(errors: &mut ParseErrorList, amt: usize) {
if amt > 0 {
for ref mut error in errors.iter_mut() {
if error.source_start != SOURCE_LOCATION_UNKNOWN {
error.source_start += amt;
}
}
}
}
pub const FISH_MAX_STACK_DEPTH: isize = 128;
#[cfg(feature = "tsan")]
pub const FISH_MAX_EVAL_DEPTH: isize = 250;
#[cfg(not(feature = "tsan"))]
pub const FISH_MAX_EVAL_DEPTH: isize = 500;
localizable_consts!(
pub INFINITE_FUNC_RECURSION_ERR_MSG
"The function '%s' calls itself immediately, which would result in an infinite loop."
pub CALL_STACK_LIMIT_EXCEEDED_ERR_MSG
"The call stack limit has been exceeded. Do you have an accidental infinite loop?"
pub UNKNOWN_BUILTIN_ERR_MSG
"Unknown builtin '%s'"
pub FAILED_EXPANSION_VARIABLE_NAME_ERR_MSG
"Unable to expand variable name '%s'"
pub ILLEGAL_FD_ERR_MSG
"Illegal file descriptor in redirection '%s'"
pub WILDCARD_ERR_MSG
"No matches for wildcard '%s'. See `help %s`."
pub INVALID_BREAK_ERR_MSG
"'break' while not inside of loop"
pub INVALID_CONTINUE_ERR_MSG
"'continue' while not inside of loop"
pub INVALID_PIPELINE_CMD_ERR_MSG
"The '%s' command can not be used in a pipeline"
pub ERROR_BAD_VAR_CHAR1
"$%c is not a valid variable in fish."
pub ERROR_BRACKETED_VARIABLE1
"Variables cannot be bracketed. In fish, please use {$%s}."
pub ERROR_BRACKETED_VARIABLE_QUOTED1
"Variables cannot be bracketed. In fish, please use \"$%s\"."
pub ERROR_NOT_STATUS
"$? is not the exit status. In fish, please use $status."
pub ERROR_NOT_PID
"$$ is not the pid. In fish, please use $fish_pid."
pub ERROR_NOT_ARGV_COUNT
"$# is not supported. In fish, please use 'count $argv'."
pub ERROR_NOT_ARGV_AT
"$@ is not supported. In fish, please use $argv."
pub ERROR_NOT_ARGV_STAR
"$* is not supported. In fish, please use $argv."
pub ERROR_NO_VAR_NAME
"Expected a variable name after this $."
pub ERROR_BAD_COMMAND_ASSIGN_ERR_MSG
"Unsupported use of '='. In fish, please use 'set %s %s'."
pub ERROR_TIME_BACKGROUND
"'time' is not supported for background jobs. Consider using 'command time'."
);