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 = #([^!].*)?$
|
||||
DEDENT = emitted when indentation decreases
|
||||
EOF = emitted at the end of the file
|
||||
@ -17,7 +17,7 @@ INDENT = emitted when indentation increases
|
||||
LINE = emitted before a recipe line
|
||||
NAME = [a-zA-Z_][a-zA-Z0-9_-]*
|
||||
NEWLINE = \n|\r\n
|
||||
RAW_STRING = '[^'\r\n]*'
|
||||
RAW_STRING = '[^']*'
|
||||
STRING = "[^"]*" # also processes \n \r \t \" \\ escapes
|
||||
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 := " "
|
||||
```
|
||||
|
||||
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
|
||||
escapes := '\t\n\r\"\\'
|
||||
|
||||
line-breaks := 'hello
|
||||
this
|
||||
is
|
||||
a
|
||||
raw
|
||||
string!
|
||||
'
|
||||
```
|
||||
|
||||
```sh
|
||||
$ just --evaluate
|
||||
escapes := "\t\n\r\"\\"
|
||||
|
||||
line-breaks := "hello
|
||||
this
|
||||
is
|
||||
a
|
||||
raw
|
||||
string!
|
||||
"
|
||||
```
|
||||
|
||||
=== Ignoring Errors
|
||||
|
@ -55,10 +55,10 @@ pub(crate) use crate::{
|
||||
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
|
||||
scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set,
|
||||
setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
|
||||
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
|
||||
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
|
||||
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
|
||||
verbosity::Verbosity, warning::Warning,
|
||||
string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand,
|
||||
suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind,
|
||||
unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe,
|
||||
use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning,
|
||||
};
|
||||
|
||||
// type aliases
|
||||
|
@ -242,10 +242,10 @@ impl Display for CompilationError<'_> {
|
||||
UnterminatedInterpolation => {
|
||||
writeln!(f, "Unterminated interpolation")?;
|
||||
},
|
||||
UnterminatedString => {
|
||||
UnterminatedString(StringKind::Cooked) | UnterminatedString(StringKind::Raw) => {
|
||||
writeln!(f, "Unterminated string")?;
|
||||
},
|
||||
UnterminatedBacktick => {
|
||||
UnterminatedString(StringKind::Backtick) => {
|
||||
writeln!(f, "Unterminated backtick")?;
|
||||
},
|
||||
Internal { ref message } => {
|
||||
|
@ -107,6 +107,5 @@ pub(crate) enum CompilationErrorKind<'src> {
|
||||
open_line: usize,
|
||||
},
|
||||
UnterminatedInterpolation,
|
||||
UnterminatedString,
|
||||
UnterminatedBacktick,
|
||||
UnterminatedString(StringKind),
|
||||
}
|
||||
|
176
src/lexer.rs
176
src/lexer.rs
@ -30,8 +30,8 @@ pub(crate) struct Lexer<'src> {
|
||||
recipe_body: bool,
|
||||
/// Indentation stack
|
||||
indentation: Vec<&'src str>,
|
||||
/// Current interpolation start token
|
||||
interpolation_start: Option<Token<'src>>,
|
||||
/// Interpolation token start stack
|
||||
interpolation_stack: Vec<Token<'src>>,
|
||||
/// Current open delimiters
|
||||
open_delimiters: Vec<(Delimiter, usize)>,
|
||||
}
|
||||
@ -60,7 +60,7 @@ impl<'src> Lexer<'src> {
|
||||
token_end: start,
|
||||
recipe_body_pending: false,
|
||||
recipe_body: false,
|
||||
interpolation_start: None,
|
||||
interpolation_stack: Vec::new(),
|
||||
open_delimiters: Vec::new(),
|
||||
chars,
|
||||
next,
|
||||
@ -210,10 +210,8 @@ impl<'src> Lexer<'src> {
|
||||
|
||||
// The width of the error site to highlight depends on the kind of error:
|
||||
let length = match kind {
|
||||
// highlight ' or "
|
||||
UnterminatedString => 1,
|
||||
// highlight `
|
||||
UnterminatedBacktick => 1,
|
||||
// highlight ', ", or `
|
||||
UnterminatedString(_) => 1,
|
||||
// highlight the full token
|
||||
_ => self.lexeme().len(),
|
||||
};
|
||||
@ -280,7 +278,7 @@ impl<'src> Lexer<'src> {
|
||||
|
||||
match self.next {
|
||||
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)?
|
||||
} else if self.recipe_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));
|
||||
}
|
||||
|
||||
@ -477,10 +475,10 @@ impl<'src> Lexer<'src> {
|
||||
'}' => self.lex_delimiter(BraceR),
|
||||
'+' => self.lex_single(Plus),
|
||||
'#' => self.lex_comment(),
|
||||
'`' => self.lex_backtick(),
|
||||
' ' => self.lex_whitespace(),
|
||||
'"' => self.lex_cooked_string(),
|
||||
'\'' => self.lex_raw_string(),
|
||||
'`' => self.lex_string(StringKind::Backtick),
|
||||
'"' => self.lex_string(StringKind::Cooked),
|
||||
'\'' => self.lex_string(StringKind::Raw),
|
||||
'\n' => self.lex_eol(),
|
||||
'\r' => self.lex_eol(),
|
||||
'\t' => self.lex_whitespace(),
|
||||
@ -500,7 +498,13 @@ impl<'src> Lexer<'src> {
|
||||
) -> CompilationResult<'src, ()> {
|
||||
if self.rest_starts_with("}}") {
|
||||
// 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
|
||||
self.lex_double(InterpolationEnd)
|
||||
} else if self.at_eol_or_eof() {
|
||||
@ -530,7 +534,7 @@ impl<'src> Lexer<'src> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some('\n') = self.next {
|
||||
if self.rest_starts_with("\n") {
|
||||
break Newline;
|
||||
}
|
||||
|
||||
@ -542,7 +546,7 @@ impl<'src> Lexer<'src> {
|
||||
break Interpolation;
|
||||
}
|
||||
|
||||
if self.next.is_none() {
|
||||
if self.rest().is_empty() {
|
||||
break EndOfFile;
|
||||
}
|
||||
|
||||
@ -559,7 +563,9 @@ impl<'src> Lexer<'src> {
|
||||
NewlineCarriageReturn => self.lex_double(Eol),
|
||||
Interpolation => {
|
||||
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(())
|
||||
},
|
||||
EndOfFile => Ok(()),
|
||||
@ -738,25 +744,6 @@ impl<'src> Lexer<'src> {
|
||||
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]+
|
||||
fn lex_whitespace(&mut self) -> CompilationResult<'src, ()> {
|
||||
while self.next_is_whitespace() {
|
||||
@ -768,48 +755,29 @@ impl<'src> Lexer<'src> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lex raw string: '[^']*'
|
||||
fn lex_raw_string(&mut self) -> CompilationResult<'src, ()> {
|
||||
self.presume('\'')?;
|
||||
|
||||
loop {
|
||||
match self.next {
|
||||
Some('\'') => break,
|
||||
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('"')?;
|
||||
/// Lex a backtick, cooked string, or raw string.
|
||||
///
|
||||
/// Backtick: `[^`]*`
|
||||
/// Cooked string: "[^"]*" # also processes escape sequences
|
||||
/// Raw string: '[^']*'
|
||||
fn lex_string(&mut self, kind: StringKind) -> CompilationResult<'src, ()> {
|
||||
self.presume(kind.delimiter())?;
|
||||
|
||||
let mut escape = false;
|
||||
|
||||
loop {
|
||||
match self.next {
|
||||
Some('\r') | Some('\n') | None => return Err(self.error(UnterminatedString)),
|
||||
Some('"') if !escape => break,
|
||||
Some('\\') if !escape => escape = true,
|
||||
_ => escape = false,
|
||||
Some(c) if c == kind.delimiter() && !escape => break,
|
||||
Some('\\') if kind.processes_escape_sequences() && !escape => escape = true,
|
||||
Some(_) => escape = false,
|
||||
None => return Err(self.error(kind.unterminated_error_kind())),
|
||||
}
|
||||
|
||||
self.advance()?;
|
||||
}
|
||||
|
||||
// advance over closing "
|
||||
self.advance()?;
|
||||
|
||||
self.token(StringCooked);
|
||||
self.presume(kind.delimiter())?;
|
||||
self.token(kind.token_kind());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -821,6 +789,10 @@ mod tests {
|
||||
|
||||
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 {
|
||||
{
|
||||
name: $name:ident,
|
||||
@ -929,7 +901,7 @@ mod tests {
|
||||
Dedent | Eof => "",
|
||||
|
||||
// Variable lexemes
|
||||
Text | StringCooked | StringRaw | Identifier | Comment | Backtick | Unspecified =>
|
||||
Text | StringToken(_) | Identifier | Comment | Unspecified =>
|
||||
panic!("Token {:?} has no default lexeme", kind),
|
||||
}
|
||||
}
|
||||
@ -993,19 +965,37 @@ mod tests {
|
||||
test! {
|
||||
name: backtick,
|
||||
text: "`echo`",
|
||||
tokens: (Backtick:"`echo`"),
|
||||
tokens: (STRING_BACKTICK:"`echo`"),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: backtick_multi_line,
|
||||
text: "`echo\necho`",
|
||||
tokens: (STRING_BACKTICK:"`echo\necho`"),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: raw_string,
|
||||
text: "'hello'",
|
||||
tokens: (StringRaw:"'hello'"),
|
||||
tokens: (STRING_RAW:"'hello'"),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: raw_string_multi_line,
|
||||
text: "'hello\ngoodbye'",
|
||||
tokens: (STRING_RAW:"'hello\ngoodbye'"),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: cooked_string,
|
||||
text: "\"hello\"",
|
||||
tokens: (StringCooked:"\"hello\""),
|
||||
tokens: (STRING_COOKED:"\"hello\""),
|
||||
}
|
||||
|
||||
test! {
|
||||
name: cooked_string_multi_line,
|
||||
text: "\"hello\ngoodbye\"",
|
||||
tokens: (STRING_COOKED:"\"hello\ngoodbye\""),
|
||||
}
|
||||
|
||||
test! {
|
||||
@ -1066,11 +1056,11 @@ mod tests {
|
||||
Whitespace,
|
||||
Equals,
|
||||
Whitespace,
|
||||
StringRaw:"'foo'",
|
||||
STRING_RAW:"'foo'",
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
StringRaw:"'bar'",
|
||||
STRING_RAW:"'bar'",
|
||||
)
|
||||
}
|
||||
|
||||
@ -1085,16 +1075,16 @@ mod tests {
|
||||
Equals,
|
||||
Whitespace,
|
||||
ParenL,
|
||||
StringRaw:"'foo'",
|
||||
STRING_RAW:"'foo'",
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
StringRaw:"'bar'",
|
||||
STRING_RAW:"'bar'",
|
||||
ParenR,
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
Backtick:"`baz`",
|
||||
STRING_BACKTICK:"`baz`",
|
||||
),
|
||||
}
|
||||
|
||||
@ -1421,11 +1411,11 @@ mod tests {
|
||||
Indent:" ",
|
||||
Text:"echo ",
|
||||
InterpolationStart,
|
||||
Backtick:"`echo hello`",
|
||||
STRING_BACKTICK:"`echo hello`",
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
Backtick:"`echo goodbye`",
|
||||
STRING_BACKTICK:"`echo goodbye`",
|
||||
InterpolationEnd,
|
||||
Dedent,
|
||||
),
|
||||
@ -1441,7 +1431,7 @@ mod tests {
|
||||
Indent:" ",
|
||||
Text:"echo ",
|
||||
InterpolationStart,
|
||||
StringRaw:"'\n'",
|
||||
STRING_RAW:"'\n'",
|
||||
InterpolationEnd,
|
||||
Dedent,
|
||||
),
|
||||
@ -1513,19 +1503,19 @@ mod tests {
|
||||
Whitespace,
|
||||
Equals,
|
||||
Whitespace,
|
||||
StringCooked:"\"'a'\"",
|
||||
STRING_COOKED:"\"'a'\"",
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
StringRaw:"'\"b\"'",
|
||||
STRING_RAW:"'\"b\"'",
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
StringCooked:"\"'c'\"",
|
||||
STRING_COOKED:"\"'c'\"",
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
StringRaw:"'\"d\"'",
|
||||
STRING_RAW:"'\"d\"'",
|
||||
Comment:"#echo hello",
|
||||
)
|
||||
}
|
||||
@ -1593,7 +1583,7 @@ mod tests {
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
StringCooked:"\"z\"",
|
||||
STRING_COOKED:"\"z\"",
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
@ -1717,7 +1707,7 @@ mod tests {
|
||||
Eol,
|
||||
Identifier:"A",
|
||||
Equals,
|
||||
StringRaw:"'1'",
|
||||
STRING_RAW:"'1'",
|
||||
Eol,
|
||||
Identifier:"echo",
|
||||
Colon,
|
||||
@ -1742,11 +1732,11 @@ mod tests {
|
||||
Indent:" ",
|
||||
Text:"echo ",
|
||||
InterpolationStart,
|
||||
Backtick:"`echo hello`",
|
||||
STRING_BACKTICK:"`echo hello`",
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
Backtick:"`echo goodbye`",
|
||||
STRING_BACKTICK:"`echo goodbye`",
|
||||
InterpolationEnd,
|
||||
Dedent
|
||||
),
|
||||
@ -1775,11 +1765,11 @@ mod tests {
|
||||
Whitespace,
|
||||
Equals,
|
||||
Whitespace,
|
||||
Backtick:"`echo hello`",
|
||||
STRING_BACKTICK:"`echo hello`",
|
||||
Whitespace,
|
||||
Plus,
|
||||
Whitespace,
|
||||
Backtick:"`echo goodbye`",
|
||||
STRING_BACKTICK:"`echo goodbye`",
|
||||
),
|
||||
}
|
||||
|
||||
@ -2021,7 +2011,7 @@ mod tests {
|
||||
line: 0,
|
||||
column: 4,
|
||||
width: 1,
|
||||
kind: UnterminatedString,
|
||||
kind: UnterminatedString(StringKind::Cooked),
|
||||
}
|
||||
|
||||
error! {
|
||||
@ -2031,7 +2021,7 @@ mod tests {
|
||||
line: 0,
|
||||
column: 4,
|
||||
width: 1,
|
||||
kind: UnterminatedString,
|
||||
kind: UnterminatedString(StringKind::Raw),
|
||||
}
|
||||
|
||||
error! {
|
||||
@ -2052,7 +2042,7 @@ mod tests {
|
||||
line: 0,
|
||||
column: 0,
|
||||
width: 1,
|
||||
kind: UnterminatedBacktick,
|
||||
kind: UnterminatedString(StringKind::Backtick),
|
||||
}
|
||||
|
||||
error! {
|
||||
@ -2112,7 +2102,7 @@ mod tests {
|
||||
line: 0,
|
||||
column: 4,
|
||||
width: 1,
|
||||
kind: UnterminatedString,
|
||||
kind: UnterminatedString(StringKind::Cooked),
|
||||
}
|
||||
|
||||
error! {
|
||||
|
@ -117,6 +117,7 @@ mod setting;
|
||||
mod settings;
|
||||
mod shebang;
|
||||
mod show_whitespace;
|
||||
mod string_kind;
|
||||
mod string_literal;
|
||||
mod subcommand;
|
||||
mod suggestion;
|
||||
|
@ -453,11 +453,11 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
||||
|
||||
/// Parse a value, e.g. `(bar)`
|
||||
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 {
|
||||
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 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"`
|
||||
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];
|
||||
|
||||
match token.kind {
|
||||
StringRaw => Ok(StringLiteral {
|
||||
StringToken(StringKind::Raw) => Ok(StringLiteral {
|
||||
raw,
|
||||
cooked: Cow::Borrowed(raw),
|
||||
}),
|
||||
StringCooked => {
|
||||
StringToken(StringKind::Cooked) => {
|
||||
let mut cooked = String::new();
|
||||
let mut escape = false;
|
||||
for c in raw.chars() {
|
||||
@ -1720,7 +1723,13 @@ mod tests {
|
||||
column: 10,
|
||||
width: 1,
|
||||
kind: UnexpectedToken {
|
||||
expected: vec![Backtick, Identifier, ParenL, StringCooked, StringRaw],
|
||||
expected: vec![
|
||||
Identifier,
|
||||
ParenL,
|
||||
StringToken(StringKind::Backtick),
|
||||
StringToken(StringKind::Cooked),
|
||||
StringToken(StringKind::Raw)
|
||||
],
|
||||
found: Eol
|
||||
},
|
||||
}
|
||||
@ -1733,7 +1742,13 @@ mod tests {
|
||||
column: 10,
|
||||
width: 0,
|
||||
kind: UnexpectedToken {
|
||||
expected: vec![Backtick, Identifier, ParenL, StringCooked, StringRaw],
|
||||
expected: vec![
|
||||
Identifier,
|
||||
ParenL,
|
||||
StringToken(StringKind::Backtick),
|
||||
StringToken(StringKind::Cooked),
|
||||
StringToken(StringKind::Raw)
|
||||
],
|
||||
found: Eof,
|
||||
},
|
||||
}
|
||||
@ -1769,7 +1784,14 @@ mod tests {
|
||||
column: 9,
|
||||
width: 0,
|
||||
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,
|
||||
},
|
||||
}
|
||||
@ -1782,7 +1804,14 @@ mod tests {
|
||||
column: 12,
|
||||
width: 2,
|
||||
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,
|
||||
},
|
||||
}
|
||||
@ -1858,7 +1887,7 @@ mod tests {
|
||||
column: 14,
|
||||
width: 1,
|
||||
kind: UnexpectedToken {
|
||||
expected: vec![StringCooked, StringRaw],
|
||||
expected: vec![StringToken(StringKind::Cooked), StringToken(StringKind::Raw)],
|
||||
found: BracketR,
|
||||
},
|
||||
}
|
||||
@ -1897,7 +1926,7 @@ mod tests {
|
||||
column: 21,
|
||||
width: 0,
|
||||
kind: UnexpectedToken {
|
||||
expected: vec![BracketR, StringCooked, StringRaw],
|
||||
expected: vec![BracketR, StringToken(StringKind::Cooked), StringToken(StringKind::Raw)],
|
||||
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 {
|
||||
Asterisk,
|
||||
At,
|
||||
Backtick,
|
||||
BangEquals,
|
||||
BraceL,
|
||||
BraceR,
|
||||
@ -27,8 +26,7 @@ pub(crate) enum TokenKind {
|
||||
ParenL,
|
||||
ParenR,
|
||||
Plus,
|
||||
StringCooked,
|
||||
StringRaw,
|
||||
StringToken(StringKind),
|
||||
Text,
|
||||
Unspecified,
|
||||
Whitespace,
|
||||
@ -40,7 +38,6 @@ impl Display for TokenKind {
|
||||
write!(f, "{}", match *self {
|
||||
Asterisk => "'*'",
|
||||
At => "'@'",
|
||||
Backtick => "backtick",
|
||||
BangEquals => "'!='",
|
||||
BraceL => "'{'",
|
||||
BraceR => "'}'",
|
||||
@ -63,8 +60,9 @@ impl Display for TokenKind {
|
||||
ParenL => "'('",
|
||||
ParenR => "')'",
|
||||
Plus => "'+'",
|
||||
StringCooked => "cooked string",
|
||||
StringRaw => "raw string",
|
||||
StringToken(StringKind::Backtick) => "backtick",
|
||||
StringToken(StringKind::Cooked) => "cooked string",
|
||||
StringToken(StringKind::Raw) => "raw string",
|
||||
Text => "command text",
|
||||
Whitespace => "whitespace",
|
||||
Unspecified => "unspecified",
|
||||
|
@ -20,4 +20,5 @@ mod quiet;
|
||||
mod readme;
|
||||
mod search;
|
||||
mod shell;
|
||||
mod string;
|
||||
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! {
|
||||
name: line_error_spacing,
|
||||
justfile: r#"
|
||||
@ -1508,145 +1496,6 @@ test! {
|
||||
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! {
|
||||
name: plus_variadic_recipe,
|
||||
justfile: "
|
||||
@ -2059,20 +1908,6 @@ foo:
|
||||
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! {
|
||||
name: unknown_start_of_token,
|
||||
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