diff --git a/GRAMMAR.md b/GRAMMAR.md index 01e25305..c2c409af 100644 --- a/GRAMMAR.md +++ b/GRAMMAR.md @@ -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 ``` diff --git a/README.adoc b/README.adoc index 554a352c..46d9c20e 100644 --- a/README.adoc +++ b/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 diff --git a/src/common.rs b/src/common.rs index f0ecb033..6b2288bd 100644 --- a/src/common.rs +++ b/src/common.rs @@ -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 diff --git a/src/compilation_error.rs b/src/compilation_error.rs index c4f9e5e7..83d1129f 100644 --- a/src/compilation_error.rs +++ b/src/compilation_error.rs @@ -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 } => { diff --git a/src/compilation_error_kind.rs b/src/compilation_error_kind.rs index 7763cf62..4ac83748 100644 --- a/src/compilation_error_kind.rs +++ b/src/compilation_error_kind.rs @@ -107,6 +107,5 @@ pub(crate) enum CompilationErrorKind<'src> { open_line: usize, }, UnterminatedInterpolation, - UnterminatedString, - UnterminatedBacktick, + UnterminatedString(StringKind), } diff --git a/src/lexer.rs b/src/lexer.rs index 9b1e181d..916c94d5 100644 --- a/src/lexer.rs +++ b/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>, + /// Interpolation token start stack + interpolation_stack: Vec>, /// 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! { diff --git a/src/lib.rs b/src/lib.rs index fe9a0a44..9fccd163 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,6 +117,7 @@ mod setting; mod settings; mod shebang; mod show_whitespace; +mod string_kind; mod string_literal; mod subcommand; mod suggestion; diff --git a/src/parser.rs b/src/parser.rs index 25f4c6e9..1d252595 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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, }, } diff --git a/src/string_kind.rs b/src/string_kind.rs new file mode 100644 index 00000000..d4847205 --- /dev/null +++ b/src/string_kind.rs @@ -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, + } + } +} diff --git a/src/token_kind.rs b/src/token_kind.rs index 42564d91..3be221b3 100644 --- a/src/token_kind.rs +++ b/src/token_kind.rs @@ -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", diff --git a/tests/lib.rs b/tests/lib.rs index 76ac0797..ef906786 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -20,4 +20,5 @@ mod quiet; mod readme; mod search; mod shell; +mod string; mod working_directory; diff --git a/tests/misc.rs b/tests/misc.rs index eb9ef9e7..69e4b1c1 100644 --- a/tests/misc.rs +++ b/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: " diff --git a/tests/string.rs b/tests/string.rs new file mode 100644 index 00000000..0b684751 --- /dev/null +++ b/tests/string.rs @@ -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, +}