Skip to content

Commit 53f8619

Browse files
authored
Add regex mismatch conditional operator (#2490)
1 parent 5393105 commit 53f8619

11 files changed

+89
-9
lines changed

src/compile_error.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,9 @@ impl Display for CompileError<'_> {
246246
"Non-default parameter `{parameter}` follows default parameter"
247247
),
248248
UndefinedVariable { variable } => write!(f, "Variable `{variable}` not defined"),
249-
UnexpectedCharacter { expected } => write!(f, "Expected character `{expected}`"),
249+
UnexpectedCharacter { expected } => {
250+
write!(f, "Expected character {}", List::or_ticked(expected.iter()))
251+
}
250252
UnexpectedClosingDelimiter { close } => {
251253
write!(f, "Unexpected closing delimiter `{}`", close.close())
252254
}

src/compile_error_kind.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ pub(crate) enum CompileErrorKind<'src> {
107107
variable: &'src str,
108108
},
109109
UnexpectedCharacter {
110-
expected: char,
110+
expected: Vec<char>,
111111
},
112112
UnexpectedClosingDelimiter {
113113
close: Delimiter,

src/conditional_operator.rs

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub(crate) enum ConditionalOperator {
99
Inequality,
1010
/// `=~`
1111
RegexMatch,
12+
/// `!~`
13+
RegexMismatch,
1214
}
1315

1416
impl Display for ConditionalOperator {
@@ -17,6 +19,7 @@ impl Display for ConditionalOperator {
1719
Self::Equality => write!(f, "=="),
1820
Self::Inequality => write!(f, "!="),
1921
Self::RegexMatch => write!(f, "=~"),
22+
Self::RegexMismatch => write!(f, "!~"),
2023
}
2124
}
2225
}

src/evaluator.rs

+3
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,9 @@ impl<'src, 'run> Evaluator<'src, 'run> {
245245
ConditionalOperator::RegexMatch => Regex::new(&rhs_value)
246246
.map_err(|source| Error::RegexCompile { source })?
247247
.is_match(&lhs_value),
248+
ConditionalOperator::RegexMismatch => !Regex::new(&rhs_value)
249+
.map_err(|source| Error::RegexCompile { source })?
250+
.is_match(&lhs_value),
248251
};
249252
Ok(condition)
250253
}

src/lexer.rs

+26-6
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ impl<'src> Lexer<'src> {
475475
match start {
476476
' ' | '\t' => self.lex_whitespace(),
477477
'!' if self.rest().starts_with("!include") => Err(self.error(Include)),
478-
'!' => self.lex_digraph('!', '=', BangEquals),
478+
'!' => self.lex_choices('!', &[('=', BangEquals), ('~', BangTilde)], None),
479479
'#' => self.lex_comment(),
480480
'$' => self.lex_single(Dollar),
481481
'&' => self.lex_digraph('&', '&', AmpersandAmpersand),
@@ -486,7 +486,11 @@ impl<'src> Lexer<'src> {
486486
',' => self.lex_single(Comma),
487487
'/' => self.lex_single(Slash),
488488
':' => self.lex_colon(),
489-
'=' => self.lex_choices('=', &[('=', EqualsEquals), ('~', EqualsTilde)], Equals),
489+
'=' => self.lex_choices(
490+
'=',
491+
&[('=', EqualsEquals), ('~', EqualsTilde)],
492+
Some(Equals),
493+
),
490494
'?' => self.lex_single(QuestionMark),
491495
'@' => self.lex_single(At),
492496
'[' => self.lex_delimiter(BracketL),
@@ -618,7 +622,7 @@ impl<'src> Lexer<'src> {
618622
&mut self,
619623
first: char,
620624
choices: &[(char, TokenKind)],
621-
otherwise: TokenKind,
625+
otherwise: Option<TokenKind>,
622626
) -> CompileResult<'src> {
623627
self.presume(first)?;
624628

@@ -629,7 +633,20 @@ impl<'src> Lexer<'src> {
629633
}
630634
}
631635

632-
self.token(otherwise);
636+
if let Some(token) = otherwise {
637+
self.token(token);
638+
} else {
639+
// Emit an unspecified token to consume the current character,
640+
self.token(Unspecified);
641+
642+
// …and advance past another character,
643+
self.advance()?;
644+
645+
// …so that the error we produce highlights the unexpected character.
646+
return Err(self.error(UnexpectedCharacter {
647+
expected: choices.iter().map(|choice| choice.0).collect(),
648+
}));
649+
}
633650

634651
Ok(())
635652
}
@@ -700,7 +717,9 @@ impl<'src> Lexer<'src> {
700717
self.advance()?;
701718

702719
// …so that the error we produce highlights the unexpected character.
703-
Err(self.error(UnexpectedCharacter { expected: right }))
720+
Err(self.error(UnexpectedCharacter {
721+
expected: vec![right],
722+
}))
704723
}
705724
}
706725

@@ -949,6 +968,7 @@ mod tests {
949968
Asterisk => "*",
950969
At => "@",
951970
BangEquals => "!=",
971+
BangTilde => "!~",
952972
BarBar => "||",
953973
BraceL => "{",
954974
BraceR => "}",
@@ -2272,7 +2292,7 @@ mod tests {
22722292
column: 1,
22732293
width: 1,
22742294
kind: UnexpectedCharacter {
2275-
expected: '&',
2295+
expected: vec!['&'],
22762296
},
22772297
}
22782298

src/parser.rs

+2
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,8 @@ impl<'run, 'src> Parser<'run, 'src> {
606606
ConditionalOperator::Inequality
607607
} else if self.accepted(EqualsTilde)? {
608608
ConditionalOperator::RegexMatch
609+
} else if self.accepted(BangTilde)? {
610+
ConditionalOperator::RegexMismatch
609611
} else {
610612
self.expect(EqualsEquals)?;
611613
ConditionalOperator::Equality

src/summary.rs

+2
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ pub enum ConditionalOperator {
360360
Equality,
361361
Inequality,
362362
RegexMatch,
363+
RegexMismatch,
363364
}
364365

365366
impl ConditionalOperator {
@@ -368,6 +369,7 @@ impl ConditionalOperator {
368369
full::ConditionalOperator::Equality => Self::Equality,
369370
full::ConditionalOperator::Inequality => Self::Inequality,
370371
full::ConditionalOperator::RegexMatch => Self::RegexMatch,
372+
full::ConditionalOperator::RegexMismatch => Self::RegexMismatch,
371373
}
372374
}
373375
}

src/token_kind.rs

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub(crate) enum TokenKind {
77
At,
88
Backtick,
99
BangEquals,
10+
BangTilde,
1011
BarBar,
1112
BraceL,
1213
BraceR,
@@ -51,6 +52,7 @@ impl Display for TokenKind {
5152
At => "'@'",
5253
Backtick => "backtick",
5354
BangEquals => "'!='",
55+
BangTilde => "'!~'",
5456
BarBar => "'||'",
5557
BraceL => "'{'",
5658
BraceR => "'}'",

tests/conditional.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ test! {
136136
",
137137
stdout: "",
138138
stderr: "
139-
error: Expected '&&', '!=', '||', '==', '=~', '+', or '/', but found identifier
139+
error: Expected '&&', '!=', '!~', '||', '==', '=~', '+', or '/', but found identifier
140140
——▶ justfile:1:12
141141
142142
1 │ a := if '' a '' { '' } else { b }

tests/parser.rs

+21
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,24 @@ fn dont_run_duplicate_recipes() {
1111
)
1212
.run();
1313
}
14+
15+
#[test]
16+
fn invalid_bang_operator() {
17+
Test::new()
18+
.justfile(
19+
"
20+
x := if '' !! '' { '' } else { '' }
21+
",
22+
)
23+
.status(1)
24+
.stderr(
25+
r"
26+
error: Expected character `=` or `~`
27+
——▶ justfile:1:13
28+
29+
1 │ x := if '' !! '' { '' } else { '' }
30+
│ ^
31+
",
32+
)
33+
.run();
34+
}

tests/regexes.rs

+25
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,28 @@ fn bad_regex_fails_at_runtime() {
6464
.status(EXIT_FAILURE)
6565
.run();
6666
}
67+
68+
#[test]
69+
fn mismatch() {
70+
Test::new()
71+
.justfile(
72+
"
73+
foo := if 'Foo' !~ '^ab+c' {
74+
'mismatch'
75+
} else {
76+
'match'
77+
}
78+
79+
bar := if 'Foo' !~ 'Foo' {
80+
'mismatch'
81+
} else {
82+
'match'
83+
}
84+
85+
@default:
86+
echo {{ foo }} {{ bar }}
87+
",
88+
)
89+
.stdout("mismatch match\n")
90+
.run();
91+
}

0 commit comments

Comments
 (0)