Unify string lexing (#790)
Unify lexing of backticks, cooked strings, and raw strings. Also allow newlines in backticks and cooked strings, since I can't think of a reason not to.
This commit is contained in:
parent
78b67f6cae
commit
dd578d141c
@ -9,7 +9,7 @@ tokens
|
|||||||
------
|
------
|
||||||
|
|
||||||
```
|
```
|
||||||
BACKTICK = `[^`\n\r]*`
|
BACKTICK = `[^`]*`
|
||||||
COMMENT = #([^!].*)?$
|
COMMENT = #([^!].*)?$
|
||||||
DEDENT = emitted when indentation decreases
|
DEDENT = emitted when indentation decreases
|
||||||
EOF = emitted at the end of the file
|
EOF = emitted at the end of the file
|
||||||
@ -17,7 +17,7 @@ INDENT = emitted when indentation increases
|
|||||||
LINE = emitted before a recipe line
|
LINE = emitted before a recipe line
|
||||||
NAME = [a-zA-Z_][a-zA-Z0-9_-]*
|
NAME = [a-zA-Z_][a-zA-Z0-9_-]*
|
||||||
NEWLINE = \n|\r\n
|
NEWLINE = \n|\r\n
|
||||||
RAW_STRING = '[^'\r\n]*'
|
RAW_STRING = '[^']*'
|
||||||
STRING = "[^"]*" # also processes \n \r \t \" \\ escapes
|
STRING = "[^"]*" # also processes \n \r \t \" \\ escapes
|
||||||
TEXT = recipe text, only matches in a recipe body
|
TEXT = recipe text, only matches in a recipe body
|
||||||
```
|
```
|
||||||
|
30
README.adoc
30
README.adoc
@ -557,31 +557,27 @@ string-with-slash := "\"
|
|||||||
string-with-tab := " "
|
string-with-tab := " "
|
||||||
```
|
```
|
||||||
|
|
||||||
Single-quoted strings do not recognize escape sequences and may contain line breaks:
|
Strings may contain line breaks:
|
||||||
|
|
||||||
|
```make
|
||||||
|
single := '
|
||||||
|
hello
|
||||||
|
'
|
||||||
|
|
||||||
|
double := "
|
||||||
|
goodbye
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Single-quoted strings do not recognize escape sequences:
|
||||||
|
|
||||||
```make
|
```make
|
||||||
escapes := '\t\n\r\"\\'
|
escapes := '\t\n\r\"\\'
|
||||||
|
|
||||||
line-breaks := 'hello
|
|
||||||
this
|
|
||||||
is
|
|
||||||
a
|
|
||||||
raw
|
|
||||||
string!
|
|
||||||
'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ just --evaluate
|
$ just --evaluate
|
||||||
escapes := "\t\n\r\"\\"
|
escapes := "\t\n\r\"\\"
|
||||||
|
|
||||||
line-breaks := "hello
|
|
||||||
this
|
|
||||||
is
|
|
||||||
a
|
|
||||||
raw
|
|
||||||
string!
|
|
||||||
"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
=== Ignoring Errors
|
=== Ignoring Errors
|
||||||
|
@ -55,10 +55,10 @@ pub(crate) use crate::{
|
|||||||
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
|
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
|
||||||
scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set,
|
scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set,
|
||||||
setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
|
setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
|
||||||
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
|
string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand,
|
||||||
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
|
suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind,
|
||||||
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
|
unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe,
|
||||||
verbosity::Verbosity, warning::Warning,
|
use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning,
|
||||||
};
|
};
|
||||||
|
|
||||||
// type aliases
|
// type aliases
|
||||||
|
@ -242,10 +242,10 @@ impl Display for CompilationError<'_> {
|
|||||||
UnterminatedInterpolation => {
|
UnterminatedInterpolation => {
|
||||||
writeln!(f, "Unterminated interpolation")?;
|
writeln!(f, "Unterminated interpolation")?;
|
||||||
},
|
},
|
||||||
UnterminatedString => {
|
UnterminatedString(StringKind::Cooked) | UnterminatedString(StringKind::Raw) => {
|
||||||
writeln!(f, "Unterminated string")?;
|
writeln!(f, "Unterminated string")?;
|
||||||
},
|
},
|
||||||
UnterminatedBacktick => {
|
UnterminatedString(StringKind::Backtick) => {
|
||||||
writeln!(f, "Unterminated backtick")?;
|
writeln!(f, "Unterminated backtick")?;
|
||||||
},
|
},
|
||||||
Internal { ref message } => {
|
Internal { ref message } => {
|
||||||
|
@ -107,6 +107,5 @@ pub(crate) enum CompilationErrorKind<'src> {
|
|||||||
open_line: usize,
|
open_line: usize,
|
||||||
},
|
},
|
||||||
UnterminatedInterpolation,
|
UnterminatedInterpolation,
|
||||||
UnterminatedString,
|
UnterminatedString(StringKind),
|
||||||
UnterminatedBacktick,
|
|
||||||
}
|
}
|
||||||
|
176
src/lexer.rs
176
src/lexer.rs
@ -30,8 +30,8 @@ pub(crate) struct Lexer<'src> {
|
|||||||
recipe_body: bool,
|
recipe_body: bool,
|
||||||
/// Indentation stack
|
/// Indentation stack
|
||||||
indentation: Vec<&'src str>,
|
indentation: Vec<&'src str>,
|
||||||
/// Current interpolation start token
|
/// Interpolation token start stack
|
||||||
interpolation_start: Option<Token<'src>>,
|
interpolation_stack: Vec<Token<'src>>,
|
||||||
/// Current open delimiters
|
/// Current open delimiters
|
||||||
open_delimiters: Vec<(Delimiter, usize)>,
|
open_delimiters: Vec<(Delimiter, usize)>,
|
||||||
}
|
}
|
||||||
@ -60,7 +60,7 @@ impl<'src> Lexer<'src> {
|
|||||||
token_end: start,
|
token_end: start,
|
||||||
recipe_body_pending: false,
|
recipe_body_pending: false,
|
||||||
recipe_body: false,
|
recipe_body: false,
|
||||||
interpolation_start: None,
|
interpolation_stack: Vec::new(),
|
||||||
open_delimiters: Vec::new(),
|
open_delimiters: Vec::new(),
|
||||||
chars,
|
chars,
|
||||||
next,
|
next,
|
||||||
@ -210,10 +210,8 @@ impl<'src> Lexer<'src> {
|
|||||||
|
|
||||||
// The width of the error site to highlight depends on the kind of error:
|
// The width of the error site to highlight depends on the kind of error:
|
||||||
let length = match kind {
|
let length = match kind {
|
||||||
// highlight ' or "
|
// highlight ', ", or `
|
||||||
UnterminatedString => 1,
|
UnterminatedString(_) => 1,
|
||||||
// highlight `
|
|
||||||
UnterminatedBacktick => 1,
|
|
||||||
// highlight the full token
|
// highlight the full token
|
||||||
_ => self.lexeme().len(),
|
_ => self.lexeme().len(),
|
||||||
};
|
};
|
||||||
@ -280,7 +278,7 @@ impl<'src> Lexer<'src> {
|
|||||||
|
|
||||||
match self.next {
|
match self.next {
|
||||||
Some(first) => {
|
Some(first) => {
|
||||||
if let Some(interpolation_start) = self.interpolation_start {
|
if let Some(&interpolation_start) = self.interpolation_stack.last() {
|
||||||
self.lex_interpolation(interpolation_start, first)?
|
self.lex_interpolation(interpolation_start, first)?
|
||||||
} else if self.recipe_body {
|
} else if self.recipe_body {
|
||||||
self.lex_body()?
|
self.lex_body()?
|
||||||
@ -292,7 +290,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(interpolation_start) = self.interpolation_start {
|
if let Some(&interpolation_start) = self.interpolation_stack.last() {
|
||||||
return Err(Self::unterminated_interpolation_error(interpolation_start));
|
return Err(Self::unterminated_interpolation_error(interpolation_start));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,10 +475,10 @@ impl<'src> Lexer<'src> {
|
|||||||
'}' => self.lex_delimiter(BraceR),
|
'}' => self.lex_delimiter(BraceR),
|
||||||
'+' => self.lex_single(Plus),
|
'+' => self.lex_single(Plus),
|
||||||
'#' => self.lex_comment(),
|
'#' => self.lex_comment(),
|
||||||
'`' => self.lex_backtick(),
|
|
||||||
' ' => self.lex_whitespace(),
|
' ' => self.lex_whitespace(),
|
||||||
'"' => self.lex_cooked_string(),
|
'`' => self.lex_string(StringKind::Backtick),
|
||||||
'\'' => self.lex_raw_string(),
|
'"' => self.lex_string(StringKind::Cooked),
|
||||||
|
'\'' => self.lex_string(StringKind::Raw),
|
||||||
'\n' => self.lex_eol(),
|
'\n' => self.lex_eol(),
|
||||||
'\r' => self.lex_eol(),
|
'\r' => self.lex_eol(),
|
||||||
'\t' => self.lex_whitespace(),
|
'\t' => self.lex_whitespace(),
|
||||||
@ -500,7 +498,13 @@ impl<'src> Lexer<'src> {
|
|||||||
) -> CompilationResult<'src, ()> {
|
) -> CompilationResult<'src, ()> {
|
||||||
if self.rest_starts_with("}}") {
|
if self.rest_starts_with("}}") {
|
||||||
// end current interpolation
|
// end current interpolation
|
||||||
self.interpolation_start = None;
|
if self.interpolation_stack.pop().is_none() {
|
||||||
|
self.advance()?;
|
||||||
|
self.advance()?;
|
||||||
|
return Err(self.internal_error(
|
||||||
|
"Lexer::lex_interpolation found `}}` but was called with empty interpolation stack.",
|
||||||
|
));
|
||||||
|
}
|
||||||
// Emit interpolation end token
|
// Emit interpolation end token
|
||||||
self.lex_double(InterpolationEnd)
|
self.lex_double(InterpolationEnd)
|
||||||
} else if self.at_eol_or_eof() {
|
} else if self.at_eol_or_eof() {
|
||||||
@ -530,7 +534,7 @@ impl<'src> Lexer<'src> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some('\n') = self.next {
|
if self.rest_starts_with("\n") {
|
||||||
break Newline;
|
break Newline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -542,7 +546,7 @@ impl<'src> Lexer<'src> {
|
|||||||
break Interpolation;
|
break Interpolation;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.next.is_none() {
|
if self.rest().is_empty() {
|
||||||
break EndOfFile;
|
break EndOfFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -559,7 +563,9 @@ impl<'src> Lexer<'src> {
|
|||||||
NewlineCarriageReturn => self.lex_double(Eol),
|
NewlineCarriageReturn => self.lex_double(Eol),
|
||||||
Interpolation => {
|
Interpolation => {
|
||||||
self.lex_double(InterpolationStart)?;
|
self.lex_double(InterpolationStart)?;
|
||||||
self.interpolation_start = Some(self.tokens[self.tokens.len() - 1]);
|
self
|
||||||
|
.interpolation_stack
|
||||||
|
.push(self.tokens[self.tokens.len() - 1]);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
EndOfFile => Ok(()),
|
EndOfFile => Ok(()),
|
||||||
@ -738,25 +744,6 @@ impl<'src> Lexer<'src> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lex backtick: `[^\r\n]*`
|
|
||||||
fn lex_backtick(&mut self) -> CompilationResult<'src, ()> {
|
|
||||||
// advance over initial `
|
|
||||||
self.advance()?;
|
|
||||||
|
|
||||||
while !self.next_is('`') {
|
|
||||||
if self.at_eol_or_eof() {
|
|
||||||
return Err(self.error(UnterminatedBacktick));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.advance()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.advance()?;
|
|
||||||
self.token(Backtick);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lex whitespace: [ \t]+
|
/// Lex whitespace: [ \t]+
|
||||||
fn lex_whitespace(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_whitespace(&mut self) -> CompilationResult<'src, ()> {
|
||||||
while self.next_is_whitespace() {
|
while self.next_is_whitespace() {
|
||||||
@ -768,48 +755,29 @@ impl<'src> Lexer<'src> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lex raw string: '[^']*'
|
/// Lex a backtick, cooked string, or raw string.
|
||||||
fn lex_raw_string(&mut self) -> CompilationResult<'src, ()> {
|
///
|
||||||
self.presume('\'')?;
|
/// Backtick: `[^`]*`
|
||||||
|
/// Cooked string: "[^"]*" # also processes escape sequences
|
||||||
loop {
|
/// Raw string: '[^']*'
|
||||||
match self.next {
|
fn lex_string(&mut self, kind: StringKind) -> CompilationResult<'src, ()> {
|
||||||
Some('\'') => break,
|
self.presume(kind.delimiter())?;
|
||||||
None => return Err(self.error(UnterminatedString)),
|
|
||||||
Some(_) => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
self.advance()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.presume('\'')?;
|
|
||||||
|
|
||||||
self.token(StringRaw);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lex cooked string: "[^"\n\r]*" (also processes escape sequences)
|
|
||||||
fn lex_cooked_string(&mut self) -> CompilationResult<'src, ()> {
|
|
||||||
self.presume('"')?;
|
|
||||||
|
|
||||||
let mut escape = false;
|
let mut escape = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match self.next {
|
match self.next {
|
||||||
Some('\r') | Some('\n') | None => return Err(self.error(UnterminatedString)),
|
Some(c) if c == kind.delimiter() && !escape => break,
|
||||||
Some('"') if !escape => break,
|
Some('\\') if kind.processes_escape_sequences() && !escape => escape = true,
|
||||||
Some('\\') if !escape => escape = true,
|
Some(_) => escape = false,
|
||||||
_ => escape = false,
|
None => return Err(self.error(kind.unterminated_error_kind())),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// advance over closing "
|
self.presume(kind.delimiter())?;
|
||||||
self.advance()?;
|
self.token(kind.token_kind());
|
||||||
|
|
||||||
self.token(StringCooked);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -821,6 +789,10 @@ mod tests {
|
|||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
const STRING_BACKTICK: TokenKind = StringToken(StringKind::Backtick);
|
||||||
|
const STRING_RAW: TokenKind = StringToken(StringKind::Raw);
|
||||||
|
const STRING_COOKED: TokenKind = StringToken(StringKind::Cooked);
|
||||||
|
|
||||||
macro_rules! test {
|
macro_rules! test {
|
||||||
{
|
{
|
||||||
name: $name:ident,
|
name: $name:ident,
|
||||||
@ -929,7 +901,7 @@ mod tests {
|
|||||||
Dedent | Eof => "",
|
Dedent | Eof => "",
|
||||||
|
|
||||||
// Variable lexemes
|
// Variable lexemes
|
||||||
Text | StringCooked | StringRaw | Identifier | Comment | Backtick | Unspecified =>
|
Text | StringToken(_) | Identifier | Comment | Unspecified =>
|
||||||
panic!("Token {:?} has no default lexeme", kind),
|
panic!("Token {:?} has no default lexeme", kind),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -993,19 +965,37 @@ mod tests {
|
|||||||
test! {
|
test! {
|
||||||
name: backtick,
|
name: backtick,
|
||||||
text: "`echo`",
|
text: "`echo`",
|
||||||
tokens: (Backtick:"`echo`"),
|
tokens: (STRING_BACKTICK:"`echo`"),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: backtick_multi_line,
|
||||||
|
text: "`echo\necho`",
|
||||||
|
tokens: (STRING_BACKTICK:"`echo\necho`"),
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: raw_string,
|
name: raw_string,
|
||||||
text: "'hello'",
|
text: "'hello'",
|
||||||
tokens: (StringRaw:"'hello'"),
|
tokens: (STRING_RAW:"'hello'"),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: raw_string_multi_line,
|
||||||
|
text: "'hello\ngoodbye'",
|
||||||
|
tokens: (STRING_RAW:"'hello\ngoodbye'"),
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: cooked_string,
|
name: cooked_string,
|
||||||
text: "\"hello\"",
|
text: "\"hello\"",
|
||||||
tokens: (StringCooked:"\"hello\""),
|
tokens: (STRING_COOKED:"\"hello\""),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: cooked_string_multi_line,
|
||||||
|
text: "\"hello\ngoodbye\"",
|
||||||
|
tokens: (STRING_COOKED:"\"hello\ngoodbye\""),
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
@ -1066,11 +1056,11 @@ mod tests {
|
|||||||
Whitespace,
|
Whitespace,
|
||||||
Equals,
|
Equals,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
StringRaw:"'foo'",
|
STRING_RAW:"'foo'",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
StringRaw:"'bar'",
|
STRING_RAW:"'bar'",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1085,16 +1075,16 @@ mod tests {
|
|||||||
Equals,
|
Equals,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
ParenL,
|
ParenL,
|
||||||
StringRaw:"'foo'",
|
STRING_RAW:"'foo'",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
StringRaw:"'bar'",
|
STRING_RAW:"'bar'",
|
||||||
ParenR,
|
ParenR,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Backtick:"`baz`",
|
STRING_BACKTICK:"`baz`",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1421,11 +1411,11 @@ mod tests {
|
|||||||
Indent:" ",
|
Indent:" ",
|
||||||
Text:"echo ",
|
Text:"echo ",
|
||||||
InterpolationStart,
|
InterpolationStart,
|
||||||
Backtick:"`echo hello`",
|
STRING_BACKTICK:"`echo hello`",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Backtick:"`echo goodbye`",
|
STRING_BACKTICK:"`echo goodbye`",
|
||||||
InterpolationEnd,
|
InterpolationEnd,
|
||||||
Dedent,
|
Dedent,
|
||||||
),
|
),
|
||||||
@ -1441,7 +1431,7 @@ mod tests {
|
|||||||
Indent:" ",
|
Indent:" ",
|
||||||
Text:"echo ",
|
Text:"echo ",
|
||||||
InterpolationStart,
|
InterpolationStart,
|
||||||
StringRaw:"'\n'",
|
STRING_RAW:"'\n'",
|
||||||
InterpolationEnd,
|
InterpolationEnd,
|
||||||
Dedent,
|
Dedent,
|
||||||
),
|
),
|
||||||
@ -1513,19 +1503,19 @@ mod tests {
|
|||||||
Whitespace,
|
Whitespace,
|
||||||
Equals,
|
Equals,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
StringCooked:"\"'a'\"",
|
STRING_COOKED:"\"'a'\"",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
StringRaw:"'\"b\"'",
|
STRING_RAW:"'\"b\"'",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
StringCooked:"\"'c'\"",
|
STRING_COOKED:"\"'c'\"",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
StringRaw:"'\"d\"'",
|
STRING_RAW:"'\"d\"'",
|
||||||
Comment:"#echo hello",
|
Comment:"#echo hello",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1593,7 +1583,7 @@ mod tests {
|
|||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
StringCooked:"\"z\"",
|
STRING_COOKED:"\"z\"",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
@ -1717,7 +1707,7 @@ mod tests {
|
|||||||
Eol,
|
Eol,
|
||||||
Identifier:"A",
|
Identifier:"A",
|
||||||
Equals,
|
Equals,
|
||||||
StringRaw:"'1'",
|
STRING_RAW:"'1'",
|
||||||
Eol,
|
Eol,
|
||||||
Identifier:"echo",
|
Identifier:"echo",
|
||||||
Colon,
|
Colon,
|
||||||
@ -1742,11 +1732,11 @@ mod tests {
|
|||||||
Indent:" ",
|
Indent:" ",
|
||||||
Text:"echo ",
|
Text:"echo ",
|
||||||
InterpolationStart,
|
InterpolationStart,
|
||||||
Backtick:"`echo hello`",
|
STRING_BACKTICK:"`echo hello`",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Backtick:"`echo goodbye`",
|
STRING_BACKTICK:"`echo goodbye`",
|
||||||
InterpolationEnd,
|
InterpolationEnd,
|
||||||
Dedent
|
Dedent
|
||||||
),
|
),
|
||||||
@ -1775,11 +1765,11 @@ mod tests {
|
|||||||
Whitespace,
|
Whitespace,
|
||||||
Equals,
|
Equals,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Backtick:"`echo hello`",
|
STRING_BACKTICK:"`echo hello`",
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Plus,
|
Plus,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
Backtick:"`echo goodbye`",
|
STRING_BACKTICK:"`echo goodbye`",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2021,7 +2011,7 @@ mod tests {
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 4,
|
column: 4,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnterminatedString,
|
kind: UnterminatedString(StringKind::Cooked),
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
@ -2031,7 +2021,7 @@ mod tests {
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 4,
|
column: 4,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnterminatedString,
|
kind: UnterminatedString(StringKind::Raw),
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
@ -2052,7 +2042,7 @@ mod tests {
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 0,
|
column: 0,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnterminatedBacktick,
|
kind: UnterminatedString(StringKind::Backtick),
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
@ -2112,7 +2102,7 @@ mod tests {
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 4,
|
column: 4,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnterminatedString,
|
kind: UnterminatedString(StringKind::Cooked),
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
|
@ -117,6 +117,7 @@ mod setting;
|
|||||||
mod settings;
|
mod settings;
|
||||||
mod shebang;
|
mod shebang;
|
||||||
mod show_whitespace;
|
mod show_whitespace;
|
||||||
|
mod string_kind;
|
||||||
mod string_literal;
|
mod string_literal;
|
||||||
mod subcommand;
|
mod subcommand;
|
||||||
mod suggestion;
|
mod suggestion;
|
||||||
|
@ -453,11 +453,11 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
|
|
||||||
/// Parse a value, e.g. `(bar)`
|
/// Parse a value, e.g. `(bar)`
|
||||||
fn parse_value(&mut self) -> CompilationResult<'src, Expression<'src>> {
|
fn parse_value(&mut self) -> CompilationResult<'src, Expression<'src>> {
|
||||||
if self.next_is(StringCooked) || self.next_is(StringRaw) {
|
if self.next_is(StringToken(StringKind::Cooked)) || self.next_is(StringToken(StringKind::Raw)) {
|
||||||
Ok(Expression::StringLiteral {
|
Ok(Expression::StringLiteral {
|
||||||
string_literal: self.parse_string_literal()?,
|
string_literal: self.parse_string_literal()?,
|
||||||
})
|
})
|
||||||
} else if self.next_is(Backtick) {
|
} else if self.next_is(StringToken(StringKind::Backtick)) {
|
||||||
let next = self.next()?;
|
let next = self.next()?;
|
||||||
|
|
||||||
let contents = &next.lexeme()[1..next.lexeme().len() - 1];
|
let contents = &next.lexeme()[1..next.lexeme().len() - 1];
|
||||||
@ -486,16 +486,19 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
|
|
||||||
/// Parse a string literal, e.g. `"FOO"`
|
/// Parse a string literal, e.g. `"FOO"`
|
||||||
fn parse_string_literal(&mut self) -> CompilationResult<'src, StringLiteral<'src>> {
|
fn parse_string_literal(&mut self) -> CompilationResult<'src, StringLiteral<'src>> {
|
||||||
let token = self.expect_any(&[StringRaw, StringCooked])?;
|
let token = self.expect_any(&[
|
||||||
|
StringToken(StringKind::Raw),
|
||||||
|
StringToken(StringKind::Cooked),
|
||||||
|
])?;
|
||||||
|
|
||||||
let raw = &token.lexeme()[1..token.lexeme().len() - 1];
|
let raw = &token.lexeme()[1..token.lexeme().len() - 1];
|
||||||
|
|
||||||
match token.kind {
|
match token.kind {
|
||||||
StringRaw => Ok(StringLiteral {
|
StringToken(StringKind::Raw) => Ok(StringLiteral {
|
||||||
raw,
|
raw,
|
||||||
cooked: Cow::Borrowed(raw),
|
cooked: Cow::Borrowed(raw),
|
||||||
}),
|
}),
|
||||||
StringCooked => {
|
StringToken(StringKind::Cooked) => {
|
||||||
let mut cooked = String::new();
|
let mut cooked = String::new();
|
||||||
let mut escape = false;
|
let mut escape = false;
|
||||||
for c in raw.chars() {
|
for c in raw.chars() {
|
||||||
@ -1720,7 +1723,13 @@ mod tests {
|
|||||||
column: 10,
|
column: 10,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnexpectedToken {
|
kind: UnexpectedToken {
|
||||||
expected: vec![Backtick, Identifier, ParenL, StringCooked, StringRaw],
|
expected: vec![
|
||||||
|
Identifier,
|
||||||
|
ParenL,
|
||||||
|
StringToken(StringKind::Backtick),
|
||||||
|
StringToken(StringKind::Cooked),
|
||||||
|
StringToken(StringKind::Raw)
|
||||||
|
],
|
||||||
found: Eol
|
found: Eol
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -1733,7 +1742,13 @@ mod tests {
|
|||||||
column: 10,
|
column: 10,
|
||||||
width: 0,
|
width: 0,
|
||||||
kind: UnexpectedToken {
|
kind: UnexpectedToken {
|
||||||
expected: vec![Backtick, Identifier, ParenL, StringCooked, StringRaw],
|
expected: vec![
|
||||||
|
Identifier,
|
||||||
|
ParenL,
|
||||||
|
StringToken(StringKind::Backtick),
|
||||||
|
StringToken(StringKind::Cooked),
|
||||||
|
StringToken(StringKind::Raw)
|
||||||
|
],
|
||||||
found: Eof,
|
found: Eof,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -1769,7 +1784,14 @@ mod tests {
|
|||||||
column: 9,
|
column: 9,
|
||||||
width: 0,
|
width: 0,
|
||||||
kind: UnexpectedToken{
|
kind: UnexpectedToken{
|
||||||
expected: vec![Backtick, Identifier, ParenL, ParenR, StringCooked, StringRaw],
|
expected: vec![
|
||||||
|
Identifier,
|
||||||
|
ParenL,
|
||||||
|
ParenR,
|
||||||
|
StringToken(StringKind::Backtick),
|
||||||
|
StringToken(StringKind::Cooked),
|
||||||
|
StringToken(StringKind::Raw)
|
||||||
|
],
|
||||||
found: Eof,
|
found: Eof,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -1782,7 +1804,14 @@ mod tests {
|
|||||||
column: 12,
|
column: 12,
|
||||||
width: 2,
|
width: 2,
|
||||||
kind: UnexpectedToken{
|
kind: UnexpectedToken{
|
||||||
expected: vec![Backtick, Identifier, ParenL, ParenR, StringCooked, StringRaw],
|
expected: vec![
|
||||||
|
Identifier,
|
||||||
|
ParenL,
|
||||||
|
ParenR,
|
||||||
|
StringToken(StringKind::Backtick),
|
||||||
|
StringToken(StringKind::Cooked),
|
||||||
|
StringToken(StringKind::Raw)
|
||||||
|
],
|
||||||
found: InterpolationEnd,
|
found: InterpolationEnd,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -1858,7 +1887,7 @@ mod tests {
|
|||||||
column: 14,
|
column: 14,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnexpectedToken {
|
kind: UnexpectedToken {
|
||||||
expected: vec![StringCooked, StringRaw],
|
expected: vec![StringToken(StringKind::Cooked), StringToken(StringKind::Raw)],
|
||||||
found: BracketR,
|
found: BracketR,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -1897,7 +1926,7 @@ mod tests {
|
|||||||
column: 21,
|
column: 21,
|
||||||
width: 0,
|
width: 0,
|
||||||
kind: UnexpectedToken {
|
kind: UnexpectedToken {
|
||||||
expected: vec![BracketR, StringCooked, StringRaw],
|
expected: vec![BracketR, StringToken(StringKind::Cooked), StringToken(StringKind::Raw)],
|
||||||
found: Eof,
|
found: Eof,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
33
src/string_kind.rs
Normal file
33
src/string_kind.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
|
||||||
|
pub(crate) enum StringKind {
|
||||||
|
Backtick,
|
||||||
|
Cooked,
|
||||||
|
Raw,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StringKind {
|
||||||
|
pub(crate) fn delimiter(self) -> char {
|
||||||
|
match self {
|
||||||
|
Self::Backtick => '`',
|
||||||
|
Self::Cooked => '"',
|
||||||
|
Self::Raw => '\'',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn token_kind(self) -> TokenKind {
|
||||||
|
TokenKind::StringToken(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unterminated_error_kind(self) -> CompilationErrorKind<'static> {
|
||||||
|
CompilationErrorKind::UnterminatedString(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn processes_escape_sequences(self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Backtick | Self::Raw => false,
|
||||||
|
Self::Cooked => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,6 @@ use crate::common::*;
|
|||||||
pub(crate) enum TokenKind {
|
pub(crate) enum TokenKind {
|
||||||
Asterisk,
|
Asterisk,
|
||||||
At,
|
At,
|
||||||
Backtick,
|
|
||||||
BangEquals,
|
BangEquals,
|
||||||
BraceL,
|
BraceL,
|
||||||
BraceR,
|
BraceR,
|
||||||
@ -27,8 +26,7 @@ pub(crate) enum TokenKind {
|
|||||||
ParenL,
|
ParenL,
|
||||||
ParenR,
|
ParenR,
|
||||||
Plus,
|
Plus,
|
||||||
StringCooked,
|
StringToken(StringKind),
|
||||||
StringRaw,
|
|
||||||
Text,
|
Text,
|
||||||
Unspecified,
|
Unspecified,
|
||||||
Whitespace,
|
Whitespace,
|
||||||
@ -40,7 +38,6 @@ impl Display for TokenKind {
|
|||||||
write!(f, "{}", match *self {
|
write!(f, "{}", match *self {
|
||||||
Asterisk => "'*'",
|
Asterisk => "'*'",
|
||||||
At => "'@'",
|
At => "'@'",
|
||||||
Backtick => "backtick",
|
|
||||||
BangEquals => "'!='",
|
BangEquals => "'!='",
|
||||||
BraceL => "'{'",
|
BraceL => "'{'",
|
||||||
BraceR => "'}'",
|
BraceR => "'}'",
|
||||||
@ -63,8 +60,9 @@ impl Display for TokenKind {
|
|||||||
ParenL => "'('",
|
ParenL => "'('",
|
||||||
ParenR => "')'",
|
ParenR => "')'",
|
||||||
Plus => "'+'",
|
Plus => "'+'",
|
||||||
StringCooked => "cooked string",
|
StringToken(StringKind::Backtick) => "backtick",
|
||||||
StringRaw => "raw string",
|
StringToken(StringKind::Cooked) => "cooked string",
|
||||||
|
StringToken(StringKind::Raw) => "raw string",
|
||||||
Text => "command text",
|
Text => "command text",
|
||||||
Whitespace => "whitespace",
|
Whitespace => "whitespace",
|
||||||
Unspecified => "unspecified",
|
Unspecified => "unspecified",
|
||||||
|
@ -20,4 +20,5 @@ mod quiet;
|
|||||||
mod readme;
|
mod readme;
|
||||||
mod search;
|
mod search;
|
||||||
mod shell;
|
mod shell;
|
||||||
|
mod string;
|
||||||
mod working_directory;
|
mod working_directory;
|
||||||
|
165
tests/misc.rs
165
tests/misc.rs
@ -588,18 +588,6 @@ hello := "c"
|
|||||||
"#,
|
"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
|
||||||
name: raw_string,
|
|
||||||
justfile: r#"
|
|
||||||
export EXPORTED_VARIABLE := '\z'
|
|
||||||
|
|
||||||
recipe:
|
|
||||||
printf "$EXPORTED_VARIABLE"
|
|
||||||
"#,
|
|
||||||
stdout: "\\z",
|
|
||||||
stderr: "printf \"$EXPORTED_VARIABLE\"\n",
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: line_error_spacing,
|
name: line_error_spacing,
|
||||||
justfile: r#"
|
justfile: r#"
|
||||||
@ -1508,145 +1496,6 @@ test! {
|
|||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
|
||||||
name: invalid_escape_sequence,
|
|
||||||
justfile: r#"x := "\q"
|
|
||||||
a:"#,
|
|
||||||
args: ("a"),
|
|
||||||
stdout: "",
|
|
||||||
stderr: "error: `\\q` is not a valid escape sequence
|
|
||||||
|
|
|
||||||
1 | x := \"\\q\"
|
|
||||||
| ^^^^
|
|
||||||
",
|
|
||||||
status: EXIT_FAILURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
|
||||||
name: multiline_raw_string,
|
|
||||||
justfile: "
|
|
||||||
string := 'hello
|
|
||||||
whatever'
|
|
||||||
|
|
||||||
a:
|
|
||||||
echo '{{string}}'
|
|
||||||
",
|
|
||||||
args: ("a"),
|
|
||||||
stdout: "hello
|
|
||||||
whatever
|
|
||||||
",
|
|
||||||
stderr: "echo 'hello
|
|
||||||
whatever'
|
|
||||||
",
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
|
||||||
name: error_line_after_multiline_raw_string,
|
|
||||||
justfile: "
|
|
||||||
string := 'hello
|
|
||||||
|
|
||||||
whatever' + 'yo'
|
|
||||||
|
|
||||||
a:
|
|
||||||
echo '{{foo}}'
|
|
||||||
",
|
|
||||||
args: ("a"),
|
|
||||||
stdout: "",
|
|
||||||
stderr: "error: Variable `foo` not defined
|
|
||||||
|
|
|
||||||
7 | echo '{{foo}}'
|
|
||||||
| ^^^
|
|
||||||
",
|
|
||||||
status: EXIT_FAILURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
|
||||||
name: error_column_after_multiline_raw_string,
|
|
||||||
justfile: "
|
|
||||||
string := 'hello
|
|
||||||
|
|
||||||
whatever' + bar
|
|
||||||
|
|
||||||
a:
|
|
||||||
echo '{{string}}'
|
|
||||||
",
|
|
||||||
args: ("a"),
|
|
||||||
stdout: "",
|
|
||||||
stderr: "error: Variable `bar` not defined
|
|
||||||
|
|
|
||||||
4 | whatever' + bar
|
|
||||||
| ^^^
|
|
||||||
",
|
|
||||||
status: EXIT_FAILURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
|
||||||
name: multiline_raw_string_in_interpolation,
|
|
||||||
justfile: r#"
|
|
||||||
a:
|
|
||||||
echo '{{"a" + '
|
|
||||||
' + "b"}}'
|
|
||||||
"#,
|
|
||||||
args: ("a"),
|
|
||||||
stdout: "
|
|
||||||
a
|
|
||||||
b
|
|
||||||
",
|
|
||||||
stderr: "
|
|
||||||
echo 'a
|
|
||||||
b'
|
|
||||||
",
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
|
||||||
name: error_line_after_multiline_raw_string_in_interpolation,
|
|
||||||
justfile: r#"
|
|
||||||
a:
|
|
||||||
echo '{{"a" + '
|
|
||||||
' + "b"}}'
|
|
||||||
|
|
||||||
echo {{b}}
|
|
||||||
"#,
|
|
||||||
args: ("a"),
|
|
||||||
stdout: "",
|
|
||||||
stderr: "error: Variable `b` not defined
|
|
||||||
|
|
|
||||||
6 | echo {{b}}
|
|
||||||
| ^
|
|
||||||
",
|
|
||||||
status: EXIT_FAILURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
|
||||||
name: unterminated_raw_string,
|
|
||||||
justfile: "
|
|
||||||
a b= ':
|
|
||||||
",
|
|
||||||
args: ("a"),
|
|
||||||
stdout: "",
|
|
||||||
stderr: "error: Unterminated string
|
|
||||||
|
|
|
||||||
2 | a b= ':
|
|
||||||
| ^
|
|
||||||
",
|
|
||||||
status: EXIT_FAILURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
|
||||||
name: unterminated_string,
|
|
||||||
justfile: r#"
|
|
||||||
a b= ":
|
|
||||||
"#,
|
|
||||||
args: ("a"),
|
|
||||||
stdout: "",
|
|
||||||
stderr: r#"error: Unterminated string
|
|
||||||
|
|
|
||||||
2 | a b= ":
|
|
||||||
| ^
|
|
||||||
"#,
|
|
||||||
status: EXIT_FAILURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: plus_variadic_recipe,
|
name: plus_variadic_recipe,
|
||||||
justfile: "
|
justfile: "
|
||||||
@ -2059,20 +1908,6 @@ foo:
|
|||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
|
||||||
name: unterminated_backtick,
|
|
||||||
justfile: "
|
|
||||||
foo a=\t`echo blaaaaaah:
|
|
||||||
echo {{a}}",
|
|
||||||
stderr: r#"
|
|
||||||
error: Unterminated backtick
|
|
||||||
|
|
|
||||||
2 | foo a= `echo blaaaaaah:
|
|
||||||
| ^
|
|
||||||
"#,
|
|
||||||
status: EXIT_FAILURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: unknown_start_of_token,
|
name: unknown_start_of_token,
|
||||||
justfile: "
|
justfile: "
|
||||||
|
201
tests/string.rs
Normal file
201
tests/string.rs
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: raw_string,
|
||||||
|
justfile: r#"
|
||||||
|
export EXPORTED_VARIABLE := '\z'
|
||||||
|
|
||||||
|
recipe:
|
||||||
|
printf "$EXPORTED_VARIABLE"
|
||||||
|
"#,
|
||||||
|
stdout: "\\z",
|
||||||
|
stderr: "printf \"$EXPORTED_VARIABLE\"\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: multiline_raw_string,
|
||||||
|
justfile: "
|
||||||
|
string := 'hello
|
||||||
|
whatever'
|
||||||
|
|
||||||
|
a:
|
||||||
|
echo '{{string}}'
|
||||||
|
",
|
||||||
|
args: ("a"),
|
||||||
|
stdout: "hello
|
||||||
|
whatever
|
||||||
|
",
|
||||||
|
stderr: "echo 'hello
|
||||||
|
whatever'
|
||||||
|
",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: multiline_backtick,
|
||||||
|
justfile: "
|
||||||
|
string := `echo hello
|
||||||
|
echo goodbye
|
||||||
|
`
|
||||||
|
|
||||||
|
a:
|
||||||
|
echo '{{string}}'
|
||||||
|
",
|
||||||
|
args: ("a"),
|
||||||
|
stdout: "hello\ngoodbye\n",
|
||||||
|
stderr: "echo 'hello
|
||||||
|
goodbye'
|
||||||
|
",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: multiline_cooked_string,
|
||||||
|
justfile: r#"
|
||||||
|
string := "hello
|
||||||
|
whatever"
|
||||||
|
|
||||||
|
a:
|
||||||
|
echo '{{string}}'
|
||||||
|
"#,
|
||||||
|
args: ("a"),
|
||||||
|
stdout: "hello
|
||||||
|
whatever
|
||||||
|
",
|
||||||
|
stderr: "echo 'hello
|
||||||
|
whatever'
|
||||||
|
",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: invalid_escape_sequence,
|
||||||
|
justfile: r#"x := "\q"
|
||||||
|
a:"#,
|
||||||
|
args: ("a"),
|
||||||
|
stdout: "",
|
||||||
|
stderr: "error: `\\q` is not a valid escape sequence
|
||||||
|
|
|
||||||
|
1 | x := \"\\q\"
|
||||||
|
| ^^^^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: error_line_after_multiline_raw_string,
|
||||||
|
justfile: "
|
||||||
|
string := 'hello
|
||||||
|
|
||||||
|
whatever' + 'yo'
|
||||||
|
|
||||||
|
a:
|
||||||
|
echo '{{foo}}'
|
||||||
|
",
|
||||||
|
args: ("a"),
|
||||||
|
stdout: "",
|
||||||
|
stderr: "error: Variable `foo` not defined
|
||||||
|
|
|
||||||
|
7 | echo '{{foo}}'
|
||||||
|
| ^^^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: error_column_after_multiline_raw_string,
|
||||||
|
justfile: "
|
||||||
|
string := 'hello
|
||||||
|
|
||||||
|
whatever' + bar
|
||||||
|
|
||||||
|
a:
|
||||||
|
echo '{{string}}'
|
||||||
|
",
|
||||||
|
args: ("a"),
|
||||||
|
stdout: "",
|
||||||
|
stderr: "error: Variable `bar` not defined
|
||||||
|
|
|
||||||
|
4 | whatever' + bar
|
||||||
|
| ^^^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: multiline_raw_string_in_interpolation,
|
||||||
|
justfile: r#"
|
||||||
|
a:
|
||||||
|
echo '{{"a" + '
|
||||||
|
' + "b"}}'
|
||||||
|
"#,
|
||||||
|
args: ("a"),
|
||||||
|
stdout: "
|
||||||
|
a
|
||||||
|
b
|
||||||
|
",
|
||||||
|
stderr: "
|
||||||
|
echo 'a
|
||||||
|
b'
|
||||||
|
",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: error_line_after_multiline_raw_string_in_interpolation,
|
||||||
|
justfile: r#"
|
||||||
|
a:
|
||||||
|
echo '{{"a" + '
|
||||||
|
' + "b"}}'
|
||||||
|
|
||||||
|
echo {{b}}
|
||||||
|
"#,
|
||||||
|
args: ("a"),
|
||||||
|
stdout: "",
|
||||||
|
stderr: "error: Variable `b` not defined
|
||||||
|
|
|
||||||
|
6 | echo {{b}}
|
||||||
|
| ^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: unterminated_raw_string,
|
||||||
|
justfile: "
|
||||||
|
a b= ':
|
||||||
|
",
|
||||||
|
args: ("a"),
|
||||||
|
stdout: "",
|
||||||
|
stderr: "error: Unterminated string
|
||||||
|
|
|
||||||
|
2 | a b= ':
|
||||||
|
| ^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: unterminated_string,
|
||||||
|
justfile: r#"
|
||||||
|
a b= ":
|
||||||
|
"#,
|
||||||
|
args: ("a"),
|
||||||
|
stdout: "",
|
||||||
|
stderr: r#"error: Unterminated string
|
||||||
|
|
|
||||||
|
2 | a b= ":
|
||||||
|
| ^
|
||||||
|
"#,
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: unterminated_backtick,
|
||||||
|
justfile: "
|
||||||
|
foo a=\t`echo blaaaaaah:
|
||||||
|
echo {{a}}",
|
||||||
|
stderr: r#"
|
||||||
|
error: Unterminated backtick
|
||||||
|
|
|
||||||
|
2 | foo a= `echo blaaaaaah:
|
||||||
|
| ^
|
||||||
|
"#,
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user