diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index 699e90b..dc04382 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -800,6 +800,10 @@ impl<'a> Lexer<'a> { fn lex_symbol(&mut self, start: usize) -> Result<(), LexError> { let c = self.peek().unwrap(); + if c == b'&' && self.peek2() == Some(b'&') { + self.pos += 2; + return Err(self.err(start, "use `and` instead of `&&`")); + } // Two-character operators first. if c == b'|' && self.peek2() == Some(b'>') { self.pos += 2; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 270dd5a..1409b99 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1809,6 +1809,13 @@ impl Parser { } Tok::LBrace => return self.parse_record(start), Tok::LBracket => return self.parse_list(start), + // Built directly: `self.error` would append `, found `|``. + Tok::Bar if matches!(self.peek2(), Tok::Bar) => { + return Err(ParseError { + message: "use `or` instead of `||`".to_string(), + span: self.span(), + }); + } Tok::Return if self.ce_items.is_empty() => { return Err(ParseError { message: diff --git a/tests/roundtrip.rs b/tests/roundtrip.rs index dd40c4c..48cc0d0 100644 --- a/tests/roundtrip.rs +++ b/tests/roundtrip.rs @@ -916,6 +916,26 @@ fn an_active_pattern_is_top_level_only() { ); } +#[test] +fn suggests_keyword_operators_for_symbolic_logical_operator_typos() { + for (source, expected) in [ + ("let ok = ready && valid", "use `and` instead of `&&`"), + ("let ok = ready || valid", "use `or` instead of `||`"), + ] { + let err = parse(source).unwrap_err(); + assert!( + err.message().contains(expected), + "{source} -> {}", + err.message() + ); + } + // The message ends at the suggestion, with no `, found` tacked on. + assert_eq!( + parse("let ok = ready || valid").unwrap_err().message(), + "use `or` instead of `||`" + ); +} + #[test] fn return_outside_computation_expression_suggests_dropping_return() { let err = parse("let double x = return x * 2").unwrap_err();