Skip to content

Commit

Permalink
Add regex mismatch conditional operator (#2490)
Browse files Browse the repository at this point in the history
  • Loading branch information
laniakea64 authored Dec 11, 2024
1 parent 5393105 commit 53f8619
Show file tree
Hide file tree
Showing 11 changed files with 89 additions and 9 deletions.
4 changes: 3 additions & 1 deletion src/compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
2 changes: 1 addition & 1 deletion src/compile_error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ pub(crate) enum CompileErrorKind<'src> {
variable: &'src str,
},
UnexpectedCharacter {
expected: char,
expected: Vec<char>,
},
UnexpectedClosingDelimiter {
close: Delimiter,
Expand Down
3 changes: 3 additions & 0 deletions src/conditional_operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub(crate) enum ConditionalOperator {
Inequality,
/// `=~`
RegexMatch,
/// `!~`
RegexMismatch,
}

impl Display for ConditionalOperator {
Expand All @@ -17,6 +19,7 @@ impl Display for ConditionalOperator {
Self::Equality => write!(f, "=="),
Self::Inequality => write!(f, "!="),
Self::RegexMatch => write!(f, "=~"),
Self::RegexMismatch => write!(f, "!~"),
}
}
}
3 changes: 3 additions & 0 deletions src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
32 changes: 26 additions & 6 deletions src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -618,7 +622,7 @@ impl<'src> Lexer<'src> {
&mut self,
first: char,
choices: &[(char, TokenKind)],
otherwise: TokenKind,
otherwise: Option<TokenKind>,
) -> CompileResult<'src> {
self.presume(first)?;

Expand All @@ -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(())
}
Expand Down Expand Up @@ -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],
}))
}
}

Expand Down Expand Up @@ -949,6 +968,7 @@ mod tests {
Asterisk => "*",
At => "@",
BangEquals => "!=",
BangTilde => "!~",
BarBar => "||",
BraceL => "{",
BraceR => "}",
Expand Down Expand Up @@ -2272,7 +2292,7 @@ mod tests {
column: 1,
width: 1,
kind: UnexpectedCharacter {
expected: '&',
expected: vec!['&'],
},
}

Expand Down
2 changes: 2 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ pub enum ConditionalOperator {
Equality,
Inequality,
RegexMatch,
RegexMismatch,
}

impl ConditionalOperator {
Expand All @@ -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,
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/token_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub(crate) enum TokenKind {
At,
Backtick,
BangEquals,
BangTilde,
BarBar,
BraceL,
BraceR,
Expand Down Expand Up @@ -51,6 +52,7 @@ impl Display for TokenKind {
At => "'@'",
Backtick => "backtick",
BangEquals => "'!='",
BangTilde => "'!~'",
BarBar => "'||'",
BraceL => "'{'",
BraceR => "'}'",
Expand Down
2 changes: 1 addition & 1 deletion tests/conditional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
21 changes: 21 additions & 0 deletions tests/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
25 changes: 25 additions & 0 deletions tests/regexes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

0 comments on commit 53f8619

Please sign in to comment.