From bc176f28ecbfc1f83452dfd34adaefc15468b600 Mon Sep 17 00:00:00 2001 From: laniakea64 Date: Sat, 30 Nov 2024 13:06:27 -0500 Subject: [PATCH 1/6] Add "not matching regex" conditional --- src/conditional_operator.rs | 3 +++ src/evaluator.rs | 3 +++ src/lexer.rs | 3 ++- src/parser.rs | 2 ++ src/summary.rs | 2 ++ src/token_kind.rs | 2 ++ tests/conditional.rs | 2 +- tests/regexes.rs | 20 ++++++++++++++++++++ 8 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/conditional_operator.rs b/src/conditional_operator.rs index bb297c22ab..d3fe826b9f 100644 --- a/src/conditional_operator.rs +++ b/src/conditional_operator.rs @@ -9,6 +9,8 @@ pub(crate) enum ConditionalOperator { Inequality, /// `=~` RegexMatch, + /// `!~` + RegexNotMatch, } 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::RegexNotMatch => write!(f, "!~"), } } } diff --git a/src/evaluator.rs b/src/evaluator.rs index b83c8cf70c..c69ea1fc78 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::RegexNotMatch => !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..eaa4417535 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)], Unspecified), '#' => self.lex_comment(), '$' => self.lex_single(Dollar), '&' => self.lex_digraph('&', '&', AmpersandAmpersand), @@ -949,6 +949,7 @@ mod tests { Asterisk => "*", At => "@", BangEquals => "!=", + BangTilde => "!~", BarBar => "||", BraceL => "{", BraceR => "}", diff --git a/src/parser.rs b/src/parser.rs index 32a5575159..adfe8fbd9e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -614,6 +614,8 @@ impl<'run, 'src> Parser<'run, 'src> { ConditionalOperator::Inequality } else if self.accepted(EqualsTilde)? { ConditionalOperator::RegexMatch + } else if self.accepted(BangTilde)? { + ConditionalOperator::RegexNotMatch } else { self.expect(EqualsEquals)?; ConditionalOperator::Equality diff --git a/src/summary.rs b/src/summary.rs index 76483d63ec..bab3d5fd5e 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -360,6 +360,7 @@ pub enum ConditionalOperator { Equality, Inequality, RegexMatch, + RegexNotMatch, } 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::RegexNotMatch => Self::RegexNotMatch, } } } 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/regexes.rs b/tests/regexes.rs index 7a53a0af5c..3b13a72578 100644 --- a/tests/regexes.rs +++ b/tests/regexes.rs @@ -64,3 +64,23 @@ fn bad_regex_fails_at_runtime() { .status(EXIT_FAILURE) .run(); } + +#[test] +fn not_matching_regex() { + Test::new() + .justfile( + " + foo := if 'Foo' !~ '^ab+c' { + 'no' + } else { + 'yes' + } + + default: + echo {{ foo }} + ", + ) + .stderr("echo no\n") + .stdout("no\n") + .run(); +} From f0752d646e598e8155b0efff26997836e2428f17 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 10 Dec 2024 13:50:22 -0800 Subject: [PATCH 2/6] Rename to RegexMismatch --- src/conditional_operator.rs | 4 ++-- src/evaluator.rs | 2 +- src/parser.rs | 2 +- src/summary.rs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/conditional_operator.rs b/src/conditional_operator.rs index d3fe826b9f..87832a036f 100644 --- a/src/conditional_operator.rs +++ b/src/conditional_operator.rs @@ -10,7 +10,7 @@ pub(crate) enum ConditionalOperator { /// `=~` RegexMatch, /// `!~` - RegexNotMatch, + RegexMismatch, } impl Display for ConditionalOperator { @@ -19,7 +19,7 @@ impl Display for ConditionalOperator { Self::Equality => write!(f, "=="), Self::Inequality => write!(f, "!="), Self::RegexMatch => write!(f, "=~"), - Self::RegexNotMatch => write!(f, "!~"), + Self::RegexMismatch => write!(f, "!~"), } } } diff --git a/src/evaluator.rs b/src/evaluator.rs index c69ea1fc78..46955980f4 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -245,7 +245,7 @@ impl<'src, 'run> Evaluator<'src, 'run> { ConditionalOperator::RegexMatch => Regex::new(&rhs_value) .map_err(|source| Error::RegexCompile { source })? .is_match(&lhs_value), - ConditionalOperator::RegexNotMatch => !Regex::new(&rhs_value) + ConditionalOperator::RegexMismatch => !Regex::new(&rhs_value) .map_err(|source| Error::RegexCompile { source })? .is_match(&lhs_value), }; diff --git a/src/parser.rs b/src/parser.rs index adfe8fbd9e..b2a28d1d6c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -615,7 +615,7 @@ impl<'run, 'src> Parser<'run, 'src> { } else if self.accepted(EqualsTilde)? { ConditionalOperator::RegexMatch } else if self.accepted(BangTilde)? { - ConditionalOperator::RegexNotMatch + ConditionalOperator::RegexMismatch } else { self.expect(EqualsEquals)?; ConditionalOperator::Equality diff --git a/src/summary.rs b/src/summary.rs index bab3d5fd5e..79adaba4df 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -360,7 +360,7 @@ pub enum ConditionalOperator { Equality, Inequality, RegexMatch, - RegexNotMatch, + RegexMismatch, } impl ConditionalOperator { @@ -369,7 +369,7 @@ impl ConditionalOperator { full::ConditionalOperator::Equality => Self::Equality, full::ConditionalOperator::Inequality => Self::Inequality, full::ConditionalOperator::RegexMatch => Self::RegexMatch, - full::ConditionalOperator::RegexNotMatch => Self::RegexNotMatch, + full::ConditionalOperator::RegexMismatch => Self::RegexMismatch, } } } From f145c1736fc81f60a9a356929759dbc8f3c19b58 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 10 Dec 2024 13:52:24 -0800 Subject: [PATCH 3/6] Expand test to cover matching case --- tests/regexes.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/regexes.rs b/tests/regexes.rs index 3b13a72578..8194381efb 100644 --- a/tests/regexes.rs +++ b/tests/regexes.rs @@ -66,21 +66,26 @@ fn bad_regex_fails_at_runtime() { } #[test] -fn not_matching_regex() { +fn mismatch() { Test::new() .justfile( " foo := if 'Foo' !~ '^ab+c' { - 'no' + 'mismatch' } else { - 'yes' + 'match' } - default: - echo {{ foo }} + bar := if 'Foo' !~ 'Foo' { + 'mismatch' + } else { + 'match' + } + + @default: + echo {{ foo }} {{ bar }} ", ) - .stderr("echo no\n") - .stdout("no\n") + .stdout("mismatch match\n") .run(); } From fe2d79b328e630a3bd87340ef564f913a0149397 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 10 Dec 2024 14:08:28 -0800 Subject: [PATCH 4/6] Add failing test --- tests/parser.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/parser.rs b/tests/parser.rs index 307f1aea5a..42054416cb 100644 --- a/tests/parser.rs +++ b/tests/parser.rs @@ -11,3 +11,14 @@ fn dont_run_duplicate_recipes() { ) .run(); } + +#[test] +fn invalid_bang_operator() { + Test::new() + .justfile( + " + x := if '' !! '' { '' } else { '' } + ", + ) + .run(); +} From 8c4f729ba23cfede50a74e4e37d8acc58787bed1 Mon Sep 17 00:00:00 2001 From: laniakea64 Date: Tue, 10 Dec 2024 19:32:40 -0500 Subject: [PATCH 5/6] Improve --- src/compile_error.rs | 14 +++++++++++++- src/compile_error_kind.rs | 2 +- src/lexer.rs | 31 +++++++++++++++++++++++++------ tests/parser.rs | 10 ++++++++++ 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/compile_error.rs b/src/compile_error.rs index 7fa2e0a3cb..6d31ae4b03 100644 --- a/src/compile_error.rs +++ b/src/compile_error.rs @@ -246,7 +246,19 @@ 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")?; + for (i, choice) in expected.iter().enumerate() { + if i > 0 && i == expected.len() - 1 { + write!(f, " or")?; + } + write!(f, " `{choice}`")?; + if expected.len() > 2 && i < expected.len() - 1 { + write!(f, ",")?; + } + } + Ok(()) + } 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/lexer.rs b/src/lexer.rs index eaa4417535..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_choices('!', &[('=', BangEquals), ('~', BangTilde)], Unspecified), + '!' => 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], + })) } } @@ -2273,7 +2292,7 @@ mod tests { column: 1, width: 1, kind: UnexpectedCharacter { - expected: '&', + expected: vec!['&'], }, } diff --git a/tests/parser.rs b/tests/parser.rs index 42054416cb..3512991ca0 100644 --- a/tests/parser.rs +++ b/tests/parser.rs @@ -20,5 +20,15 @@ fn invalid_bang_operator() { x := if '' !! '' { '' } else { '' } ", ) + .status(1) + .stderr( + r" +error: Expected character `=` or `~` + ——▶ justfile:1:13 + │ +1 │ x := if '' !! '' { '' } else { '' } + │ ^ +", + ) .run(); } From a47118450d08cf2b2173a41404ca8bc8ed939c7d Mon Sep 17 00:00:00 2001 From: laniakea64 Date: Tue, 10 Dec 2024 19:44:08 -0500 Subject: [PATCH 6/6] Reuse --- src/compile_error.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/compile_error.rs b/src/compile_error.rs index 6d31ae4b03..02e29af213 100644 --- a/src/compile_error.rs +++ b/src/compile_error.rs @@ -247,17 +247,7 @@ impl Display for CompileError<'_> { ), UndefinedVariable { variable } => write!(f, "Variable `{variable}` not defined"), UnexpectedCharacter { expected } => { - write!(f, "Expected character")?; - for (i, choice) in expected.iter().enumerate() { - if i > 0 && i == expected.len() - 1 { - write!(f, " or")?; - } - write!(f, " `{choice}`")?; - if expected.len() > 2 && i < expected.len() - 1 { - write!(f, ",")?; - } - } - Ok(()) + write!(f, "Expected character {}", List::or_ticked(expected.iter())) } UnexpectedClosingDelimiter { close } => { write!(f, "Unexpected closing delimiter `{}`", close.close())