From dd0c42ff332013dff6584e896e70638176008307 Mon Sep 17 00:00:00 2001 From: dot Date: Sat, 10 Oct 2026 23:07:48 +0900 Subject: [PATCH] Match obsolete comparison diagnostics with operator tokens Use the existing Barry adapter token spans for ordinary obsolete-comparison diagnostics as well. Preserve parser errors for left-shift suffixes, malformed comparisons, comments and strings, with final Python 3.15 message and span regressions. Assisted-by: OpenAI Codex:model identifier unavailable --- crates/compiler/src/lib.rs | 77 ++++++++++++++----------- extra_tests/snippets/builtin_compile.py | 52 +++++++++++++++++ 2 files changed, 96 insertions(+), 33 deletions(-) diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 71c2dfad9a1..b0612ec7fd5 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -1477,7 +1477,27 @@ impl BarrySource<'_> { parse_error: Option<&parser::ParseError>, source_file: &SourceFile, ) -> Option { - if let Some(range) = parse_error.and_then(|error| self.invalid_legacy_operator(error)) { + if let Some(error) = parse_error + && let Some(range) = self.invalid_legacy_operator(error) + { + if error.location.start() > range.start() { + return Some(CompileError::from_source_error( + source_file, + CpythonDiagnostic::new( + "invalid syntax. Maybe you meant '!=' instead of '<>'?".to_owned(), + range.start().to_usize(), + range.end().to_usize(), + ), + )); + } + // At expression start, '<' is already invalid: the comparison + // production (and its obsolete-operator hint) was never entered. + // Barry mode instead lexed the rewritten '!=' as one token. + let range = if self.source.as_bytes()[range.start().to_usize()] == b'<' { + ruff_text_size::TextRange::at(range.start(), TextSize::new(1)) + } else { + range + }; return Some(barry_as_flufl_invalid_legacy_operator_error( source_file, range, @@ -1520,47 +1540,25 @@ pub fn barry_as_flufl_invalid_legacy_operator_error( ) } -/// Every `<>` in `source`, located by plain text search. Only used where the -/// operator is not rewritten, so an occurrence inside a string or a comment -/// costs nothing: it can never coincide with the location of a parse error. -fn textual_legacy_not_equal(source: &str) -> Vec { - source - .match_indices("<>") - .map(|(offset, matched)| { - ruff_text_size::TextRange::at( - TextSize::new(offset as u32), - TextSize::new(matched.len() as u32), - ) - }) - .collect() -} - #[doc(hidden)] pub fn prepare_barry_as_flufl_source( source: &str, parser_options: parser::ParseOptions, inherited: bool, ) -> BarrySource<'_> { - let scanned = (inherited || source.contains("barry_as_FLUFL")) + let scanned = (inherited || source.contains("barry_as_FLUFL") || source.contains("<>")) .then(|| parser::parse_unchecked(source, parser_options)); - let enabled = scanned.as_ref().is_some_and(|scanned| { - inherited - || codegen::preprocess::future_features(scanned.syntax()) - .contains(core::bytecode::CodeFlags::FUTURE_BARRY_AS_BDFL) - }); - let Some(scanned) = scanned.filter(|_| enabled) else { + let Some(scanned) = scanned else { return BarrySource { source: Cow::Borrowed(source), not_equal: None, - legacy_not_equal: textual_legacy_not_equal(source), + legacy_not_equal: Vec::new(), }; }; - - let not_equal = scanned - .tokens() - .iter() - .find(|token| token.kind() == TokenKind::NotEqual) - .map(Ranged::range); + let enabled = inherited + || codegen::preprocess::future_features(scanned.syntax()) + .contains(core::bytecode::CodeFlags::FUTURE_BARRY_AS_BDFL); + // Match complete adjacent tokens, not the suffix of a left-shift token. let replacements = scanned .tokens() .windows(2) @@ -1575,6 +1573,19 @@ pub fn prepare_barry_as_flufl_source( }) .collect::>(); + if !enabled { + return BarrySource { + source: Cow::Borrowed(source), + not_equal: None, + legacy_not_equal: replacements, + }; + } + let not_equal = scanned + .tokens() + .iter() + .find(|token| token.kind() == TokenKind::NotEqual) + .map(Ranged::range); + let source = if replacements.is_empty() { Cow::Borrowed(source) } else { @@ -2413,7 +2424,7 @@ mod tests { fn obsolete_not_equal_diagnostic_spans_the_whole_operator() { let err = compile("2 <> 3\n", Mode::Exec, "", CompileOpts::default()) .expect_err("'<>' outside Barry mode is a syntax error"); - assert_eq!(err.to_string(), "invalid syntax"); + assert!(matches!(err, CompileError::Parse(_))); assert_eq!(err.python_location(), (1, 3)); assert_eq!(err.python_end_location(), Some((1, 5))); @@ -2426,12 +2437,12 @@ mod tests { assert_eq!(err.python_end_location(), Some((1, 5))); // A `<>` that starts a statement is reported at the `<` too, where the - // parser stops instead of one character in. + // parser stops. Exact Python-visible diagnostics are covered by builtin_compile.py. let err = compile("<>\n", Mode::Exec, "", CompileOpts::default()) .expect_err("a bare '<>' is a syntax error"); assert_eq!(err.to_string(), "invalid syntax"); assert_eq!(err.python_location(), (1, 1)); - assert_eq!(err.python_end_location(), Some((1, 3))); + assert!(matches!(err, CompileError::Parse(_))); // A bracket left open earlier in the source outranks the operator. let err = compile( diff --git a/extra_tests/snippets/builtin_compile.py b/extra_tests/snippets/builtin_compile.py index 9c3cc9a5a94..e1719bab868 100644 --- a/extra_tests/snippets/builtin_compile.py +++ b/extra_tests/snippets/builtin_compile.py @@ -67,6 +67,58 @@ def _check_flags_error(flags): compile("from __future__ import barry_as_FLUFL\nx = 1\n", "", "exec") assert barry_code.co_flags & barry_flag + +# Keep Python-visible obsolete-operator diagnostics in Python coverage. +for source, message, locations in ( + ( + "2 <> 3\n", + "invalid syntax. Maybe you meant '!=' instead of '<>'?", + (1, 3, 1, 5), + ), + ("<>\n", "invalid syntax", (1, 1, 1, 2)), + ("2 <;\n", "invalid syntax", (1, 4, 1, 5)), + ("2 <<> 3\n", "invalid syntax", (1, 5, 1, 6)), + ("(2 <<> 3)\n", "invalid syntax", (1, 6, 1, 7)), + ("# <>\n2 <<> 3\n", "invalid syntax", (2, 5, 2, 6)), + ("(\n2 <> 3\n", "'(' was never closed", (1, 1, 1, 0)), +): + try: + compile(source, "", "exec") + except SyntaxError as error: + assert error.msg == message + assert ( + error.lineno, + error.offset, + error.end_lineno, + error.end_offset, + ) == locations + else: + raise AssertionError("expected an obsolete-operator syntax error") + +assert eval(compile("2 <> 3", "", "eval", flags=barry_flag)) + +for source, locations in ( + ("<>\n", (1, 1, 1, 3)), + ("2 <<> 3\n", (1, 5, 1, 6)), + ("(2 <<> 3)\n", (1, 6, 1, 7)), +): + try: + compile(source, "", "exec", flags=barry_flag) + except SyntaxError as error: + assert error.msg == "invalid syntax" + assert ( + error.lineno, + error.offset, + error.end_lineno, + error.end_offset, + ) == locations + else: + raise AssertionError("expected an invalid operator syntax error") + +for flags in (0, barry_flag): + compile("2 << 3", "", "exec", flags=flags) + compile('"<>"', "", "exec", flags=flags) + n = ast.parse('x = "# type: int"\n', type_comments=True) assert n.body[0].type_comment is None n = ast.parse("x = '# type: int'\n", type_comments=True)