diff --git a/src/compile_error.rs b/src/compile_error.rs index 7fa2e0a3cb..02e29af213 100644 --- a/src/compile_error.rs +++ b/src/compile_error.rs @@ -246,7 +246,9 @@ impl Display for CompileError<'_> { "Non-default parameter `{parameter}` follows default parameter" ), UndefinedVariable { variable } => write!(f, "Variable `{variable}` not defined"), - UnexpectedCharacter { expected } => write!(f, "Expected character `{expected}`"), + UnexpectedCharacter { expected } => { + write!(f, "Expected character {}", List::or_ticked(expected.iter())) + } UnexpectedClosingDelimiter { close } => { write!(f, "Unexpected closing delimiter `{}`", close.close()) } diff --git a/src/compile_error_kind.rs b/src/compile_error_kind.rs index bc013b9025..91c5cd071b 100644 --- a/src/compile_error_kind.rs +++ b/src/compile_error_kind.rs @@ -107,7 +107,7 @@ pub(crate) enum CompileErrorKind<'src> { variable: &'src str, }, UnexpectedCharacter { - expected: char, + expected: Vec, }, UnexpectedClosingDelimiter { close: Delimiter, diff --git a/src/conditional_operator.rs b/src/conditional_operator.rs index bb297c22ab..87832a036f 100644 --- a/src/conditional_operator.rs +++ b/src/conditional_operator.rs @@ -9,6 +9,8 @@ pub(crate) enum ConditionalOperator { Inequality, /// `=~` RegexMatch, + /// `!~` + RegexMismatch, } impl Display for ConditionalOperator { @@ -17,6 +19,7 @@ impl Display for ConditionalOperator { Self::Equality => write!(f, "=="), Self::Inequality => write!(f, "!="), Self::RegexMatch => write!(f, "=~"), + Self::RegexMismatch => write!(f, "!~"), } } } diff --git a/src/evaluator.rs b/src/evaluator.rs index b83c8cf70c..46955980f4 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -245,6 +245,9 @@ impl<'src, 'run> Evaluator<'src, 'run> { ConditionalOperator::RegexMatch => Regex::new(&rhs_value) .map_err(|source| Error::RegexCompile { source })? .is_match(&lhs_value), + ConditionalOperator::RegexMismatch => !Regex::new(&rhs_value) + .map_err(|source| Error::RegexCompile { source })? + .is_match(&lhs_value), }; Ok(condition) } diff --git a/src/lexer.rs b/src/lexer.rs index 2c56db9dcf..0e7f76c67e 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -475,7 +475,7 @@ impl<'src> Lexer<'src> { match start { ' ' | '\t' => self.lex_whitespace(), '!' if self.rest().starts_with("!include") => Err(self.error(Include)), - '!' => self.lex_digraph('!', '=', BangEquals), + '!' => self.lex_choices('!', &[('=', BangEquals), ('~', BangTilde)], None), '#' => self.lex_comment(), '$' => self.lex_single(Dollar), '&' => self.lex_digraph('&', '&', AmpersandAmpersand), @@ -486,7 +486,11 @@ impl<'src> Lexer<'src> { ',' => self.lex_single(Comma), '/' => self.lex_single(Slash), ':' => self.lex_colon(), - '=' => self.lex_choices('=', &[('=', EqualsEquals), ('~', EqualsTilde)], Equals), + '=' => self.lex_choices( + '=', + &[('=', EqualsEquals), ('~', EqualsTilde)], + Some(Equals), + ), '?' => self.lex_single(QuestionMark), '@' => self.lex_single(At), '[' => self.lex_delimiter(BracketL), @@ -618,7 +622,7 @@ impl<'src> Lexer<'src> { &mut self, first: char, choices: &[(char, TokenKind)], - otherwise: TokenKind, + otherwise: Option, ) -> CompileResult<'src> { self.presume(first)?; @@ -629,7 +633,20 @@ impl<'src> Lexer<'src> { } } - self.token(otherwise); + if let Some(token) = otherwise { + self.token(token); + } else { + // Emit an unspecified token to consume the current character, + self.token(Unspecified); + + // …and advance past another character, + self.advance()?; + + // …so that the error we produce highlights the unexpected character. + return Err(self.error(UnexpectedCharacter { + expected: choices.iter().map(|choice| choice.0).collect(), + })); + } Ok(()) } @@ -700,7 +717,9 @@ impl<'src> Lexer<'src> { self.advance()?; // …so that the error we produce highlights the unexpected character. - Err(self.error(UnexpectedCharacter { expected: right })) + Err(self.error(UnexpectedCharacter { + expected: vec![right], + })) } } @@ -949,6 +968,7 @@ mod tests { Asterisk => "*", At => "@", BangEquals => "!=", + BangTilde => "!~", BarBar => "||", BraceL => "{", BraceR => "}", @@ -2272,7 +2292,7 @@ mod tests { column: 1, width: 1, kind: UnexpectedCharacter { - expected: '&', + expected: vec!['&'], }, } diff --git a/src/parser.rs b/src/parser.rs index 5903e5b492..5d7821a335 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -606,6 +606,8 @@ impl<'run, 'src> Parser<'run, 'src> { ConditionalOperator::Inequality } else if self.accepted(EqualsTilde)? { ConditionalOperator::RegexMatch + } else if self.accepted(BangTilde)? { + ConditionalOperator::RegexMismatch } else { self.expect(EqualsEquals)?; ConditionalOperator::Equality diff --git a/src/summary.rs b/src/summary.rs index 76483d63ec..79adaba4df 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -360,6 +360,7 @@ pub enum ConditionalOperator { Equality, Inequality, RegexMatch, + RegexMismatch, } impl ConditionalOperator { @@ -368,6 +369,7 @@ impl ConditionalOperator { full::ConditionalOperator::Equality => Self::Equality, full::ConditionalOperator::Inequality => Self::Inequality, full::ConditionalOperator::RegexMatch => Self::RegexMatch, + full::ConditionalOperator::RegexMismatch => Self::RegexMismatch, } } } diff --git a/src/token_kind.rs b/src/token_kind.rs index 850afa9629..be8af19f5f 100644 --- a/src/token_kind.rs +++ b/src/token_kind.rs @@ -7,6 +7,7 @@ pub(crate) enum TokenKind { At, Backtick, BangEquals, + BangTilde, BarBar, BraceL, BraceR, @@ -51,6 +52,7 @@ impl Display for TokenKind { At => "'@'", Backtick => "backtick", BangEquals => "'!='", + BangTilde => "'!~'", BarBar => "'||'", BraceL => "'{'", BraceR => "'}'", diff --git a/tests/conditional.rs b/tests/conditional.rs index 4eab2f4d72..8eae1351d6 100644 --- a/tests/conditional.rs +++ b/tests/conditional.rs @@ -136,7 +136,7 @@ test! { ", stdout: "", stderr: " - error: Expected '&&', '!=', '||', '==', '=~', '+', or '/', but found identifier + error: Expected '&&', '!=', '!~', '||', '==', '=~', '+', or '/', but found identifier ——▶ justfile:1:12 │ 1 │ a := if '' a '' { '' } else { b } diff --git a/tests/parser.rs b/tests/parser.rs index 307f1aea5a..3512991ca0 100644 --- a/tests/parser.rs +++ b/tests/parser.rs @@ -11,3 +11,24 @@ fn dont_run_duplicate_recipes() { ) .run(); } + +#[test] +fn invalid_bang_operator() { + Test::new() + .justfile( + " + x := if '' !! '' { '' } else { '' } + ", + ) + .status(1) + .stderr( + r" +error: Expected character `=` or `~` + ——▶ justfile:1:13 + │ +1 │ x := if '' !! '' { '' } else { '' } + │ ^ +", + ) + .run(); +} diff --git a/tests/regexes.rs b/tests/regexes.rs index 7a53a0af5c..8194381efb 100644 --- a/tests/regexes.rs +++ b/tests/regexes.rs @@ -64,3 +64,28 @@ fn bad_regex_fails_at_runtime() { .status(EXIT_FAILURE) .run(); } + +#[test] +fn mismatch() { + Test::new() + .justfile( + " + foo := if 'Foo' !~ '^ab+c' { + 'mismatch' + } else { + 'match' + } + + bar := if 'Foo' !~ 'Foo' { + 'mismatch' + } else { + 'match' + } + + @default: + echo {{ foo }} {{ bar }} + ", + ) + .stdout("mismatch match\n") + .run(); +}