Skip to content

Fix EOF SyntaxError diagnostics - #8429

Open
chestnut1717 wants to merge 4 commits into
RustPython:mainfrom
chestnut1717:fix/eof-syntax-errors
Open

Fix EOF SyntaxError diagnostics#8429
chestnut1717 wants to merge 4 commits into
RustPython:mainfrom
chestnut1717:fix/eof-syntax-errors

Conversation

@chestnut1717

@chestnut1717 chestnut1717 commented Aug 2, 2026

Copy link
Copy Markdown

Summary

Fix CPython compatibility for EOF-related SyntaxError diagnostics and remove the five expectedFailure markers from test_eof.py

This aligns RustPython with CPython for:

  • Unterminated triple-quoted strings containing non-ASCII characters
  • Source supplied as str, UTF-8 bytes, UTF-8 BOM bytes, and latin-1 bytes
  • Files with UTF-8 BOMs or encoding cookies
  • A line-continuation backslash at EOF
  • Command-line caret output for a file containing only \

Before / after (exec("ä = 5\\")):

Message offset
CPython 3.14 unexpected EOF while parsing 7
RustPython (before) unexpected character after line continuation character 8
RustPython (after) unexpected EOF while parsing 7

The previous behavior also counted UTF-8 bytes instead of Python characters for SyntaxError.offset, included an extra newline in unterminated triple-quoted-string SyntaxError.text, and displayed a caret for a file containing only \.

Approach

  • Use Unicode code-point positions for Python-facing parse diagnostics, while retaining UTF-8 byte positions for AST/codegen internals.
  • Detect a terminal line-continuation backslash by comparing Ruff’s error location with the final source byte. This preserves the existing error for an earlier invalid continuation such as x = 1 \ q \.
  • Preserve CPython’s EOF metadata by setting end_offset to -1.
  • Trim the generated newline from SyntaxError.text only for unterminated triple-quoted-string diagnostics.
  • Reuse decode_source_bytes() for script-file execution so BOMs and PEP 263 encoding cookies are handled consistently with compile(), eval(), and exec().
  • Use RustPython’s native SyntaxError formatter only for a top-level, traceback-free file containing a lone backslash. Runtime errors such as exec(chr(92)) continue through the normal traceback path.

Testing

  • Removed only the five @unittest.expectedFailure decorators from test_eof; assertions and test inputs are unchanged.
  • test_eof, test_syntax, test_cmd_line_script, and test_traceback all pass: 470 tests total.
  • rustpython-compiler unit tests pass: 13 tests.
  • Verified against CPython 3.14.6 for:
    • terminal backslashes with ASCII and non-ASCII prefixes;
    • earlier invalid line continuations followed by a terminal backslash;
    • unterminated triple-quoted strings from str, UTF-8 bytes, BOM-prefixed bytes, and latin-1 bytes;
    • file output for lone backslashes and UTF-8/BOM/latin-1 sources.
  • Targeted Clippy completes successfully; remaining warnings are pre-existing unfulfilled_lint_expectations outside this change.

Summary by CodeRabbit

Bug Fixes

  • Improved syntax error locations and column highlighting for more accurate diagnostics.
  • Clarified unexpected end-of-file errors caused by trailing backslashes.
  • Improved formatting for syntax errors without source spans.
  • Removed parser-added trailing newlines from unterminated string error messages.
  • Improved handling of source files with BOMs and encoding declarations.
  • Improved display of syntax errors raised without a traceback.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: d6ad0e6c-79d5-48c3-99c2-9a4afe5c8ed6

📥 Commits

Reviewing files that changed from the base of the PR and between 485f808 and 4ba2f57.

📒 Files selected for processing (1)
  • crates/compiler/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/compiler/src/lib.rs

📝 Walkthrough

Walkthrough

The compiler now reports UTF-32 source positions and converts terminal-backslash continuation errors to unexpected EOF while parsing. VM syntax-error construction, formatting, exception handling, and parser-enabled source decoding handle these cases.

Changes

Syntax error handling

Layer / File(s) Summary
Source decoding and compiler diagnostics
crates/vm/src/vm/python_run.rs, crates/compiler/src/lib.rs
Parser-enabled file execution honors BOMs and encoding cookies. Compiler locations use UTF-32 offsets. Terminal backslashes produce an unexpected EOF while parsing diagnostic at the line end. Parser-error conversion receives the compilation mode.
SyntaxError construction and output
crates/vm/src/vm/vm_new.rs, crates/vm/src/exceptions.rs, crates/vm/src/stdlib/sys.rs
Syntax-error text removes parser-added newlines for unterminated triple-quoted strings. Unexpected EOF errors have no end span. Traceback-less terminal-backslash errors use direct formatting without a caret.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FileExecution
  participant Compiler
  participant SyntaxErrorConstructor
  participant sys_excepthook
  participant write_syntaxerror
  FileExecution->>Compiler: provide decoded source
  Compiler->>SyntaxErrorConstructor: report source location and EOF diagnostic
  SyntaxErrorConstructor->>sys_excepthook: create SyntaxError
  sys_excepthook->>write_syntaxerror: format traceback-less EOF error
  write_syntaxerror-->>sys_excepthook: omit terminal-backslash caret
Loading

Possibly related PRs

Suggested reviewers: youknowone

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the pull request's main change to EOF-related SyntaxError diagnostics.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[x] test: cpython/Lib/test/test_exceptions.py (TODO: 22)
[ ] test: cpython/Lib/test/test_baseexception.py
[x] test: cpython/Lib/test/test_except_star.py (TODO: 1)
[x] test: cpython/Lib/test/test_exception_group.py (TODO: 5)
[x] test: cpython/Lib/test/test_exception_hierarchy.py (TODO: 2)
[x] test: cpython/Lib/test/test_exception_variations.py

dependencies:

dependent tests: (no tests depend on exception)

[x] test: cpython/Lib/test/test_eof.py

dependencies:

dependent tests: (no tests depend on eof)

[x] lib: cpython/Lib/tokenize.py
[x] test: cpython/Lib/test/test_tokenize.py (TODO: 7)

dependencies:

  • tokenize

dependent tests: (150 tests)

  • tokenize: test_inspect test_linecache test_peg_generator test_tabnanny test_tokenize test_unparse
    • idlelib: test_idle
    • importlib._bootstrap_external: test_importlib test_unittest
      • modulefinder: test_importlib test_modulefinder
      • py_compile: test_argparse test_cmd_line_script test_compileall test_importlib test_multiprocessing_main_handling test_py_compile test_pydoc test_runpy
      • pydoc: test_enum
    • inspect: test_abc test_asyncgen test_buffer test_builtin test_clinic test_code test_collections test_coroutines test_decimal test_functools test_generators test_grammar test_monitoring test_ntpath test_operator test_patma test_posixpath test_signal test_sqlite3 test_traceback test_turtle test_type_annotations test_types test_typing test_unittest test_yield_from test_zipimport test_zipimport_support test_zoneinfo
      • ast: test_ast test_compile test_compiler_codegen test_dis test_fstring test_future_stmt test_peepholer test_site test_ssl test_type_comments test_ucn
      • bdb: test_bdb test_pdb
      • cmd: test_cmd
      • dataclasses: test__colorize test_copy test_ctypes test_genericalias test_pprint test_regrtest
      • pkgutil: test_pkgutil test_pyrepl
      • rlcompleter: test_pyrepl test_rlcompleter
      • trace: test_trace
      • xmlrpc.server: test_docxmlrpc test_xmlrpc
    • linecache:
      • timeit: test_timeit
      • traceback: test_asyncio test_code_module test_contextlib test_contextlib_async test_dictcomps test_exceptions test_http_cookiejar test_importlib test_iter test_listcomps test_pyexpat test_setcomps test_socket test_subprocess test_sys test_threadedtempfile test_threading test_unittest test_with
      • tracemalloc: test_tracemalloc
    • traceback:
      • concurrent.futures.process: test_concurrent_futures
      • http.cookiejar: test_urllib2
      • logging: test_asyncio test_hashlib test_logging test_support test_urllib2net
      • multiprocessing: test_asyncio test_concurrent_futures test_fcntl test_memoryview test_re
      • socketserver: test_imaplib test_socketserver test_wsgiref
      • threading: test_android test_asyncio test_bytes test_bz2 test_concurrent_futures test_context test_ctypes test_email test_external_inspection test_fork1 test_frame test_ftplib test_gc test_httplib test_httpservers test_importlib test_io test_ioctl test_itertools test_largefile test_opcache test_pathlib test_poll test_poplib test_pyrepl test_queue test_robotparser test_sched test_smtplib test_super test_syslog test_termios test_threading_local test_time test_urllib2_localnet test_weakref test_winreg test_zstd

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

@youknowone youknowone added the z-ca-2026 Tag to track Contribution Academy 2026 label Aug 2, 2026
let SyntaxErrorInfo { msg, narrow_caret } = syntax_error_info;
let unterminated_triple_quoted_string =
msg.starts_with("unterminated triple-quoted string literal");
let unexpected_eof_error = msg == "unexpected EOF while parsing";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ShaharNaveh is this currently the best way to detect specific kind of error?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

atm, yes:(

Either ruff don't expose the exact reason, or we need to do a major refactor of how we propagate the errors from the compiler

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how could it be detected after the major refactor? we can't do this message matching forever 😂

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ig you're right...

maybe we can't escape it and just have our own ruff fork/have a rustpython-ast crate

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sorry if the last comment was confusing. I'd like to ask what kind of refactor do you have in mind?

Comment thread crates/vm/src/vm/python_run.rs
Comment thread crates/compiler/src/lib.rs
@youknowone

Copy link
Copy Markdown
Member

you seem to fix more tests! please check CI result about failing tests

e.g.

UNEXPECTED SUCCESS: test_assertion_error_location (test.test_exceptions.AssertionErrorTests.test_assertion_error_location)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/compiler/src/lib.rs (2)

252-269: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract the shared diagnostic construction.

Both branches repeat NormalizedParseDiagnostic::new(..., loc, loc). Extract the diagnostic error and location first. Call the constructor once.

As per coding guidelines, when branches differ only in a value but share common logic, extract the differing value first, then call the common logic once.

Suggested refactor
-        if !matches!(mode, Mode::Eval)
-            && terminal_backslash == Some(error.location.start().to_usize())
-        {
-            let loc = source_line_end_location(source_file, error.location.start());
-            return Some(NormalizedParseDiagnostic::new(
-                parser::ParseErrorType::OtherError("unexpected EOF while parsing".to_owned()),
-                loc,
-                loc,
-            ));
-        }
-        let loc = source_location(source_file, error.location.start() + TextSize::from(1));
-        return Some(NormalizedParseDiagnostic::new(
-            error.error.clone(),
-            loc,
-            loc,
-        ));
+        let (diagnostic_error, loc) = if !matches!(mode, Mode::Eval)
+            && terminal_backslash == Some(error.location.start().to_usize())
+        {
+            (
+                parser::ParseErrorType::OtherError("unexpected EOF while parsing".to_owned()),
+                source_line_end_location(source_file, error.location.start()),
+            )
+        } else {
+            (
+                error.error.clone(),
+                source_location(source_file, error.location.start() + TextSize::from(1)),
+            )
+        };
+        return Some(NormalizedParseDiagnostic::new(
+            diagnostic_error,
+            loc,
+            loc,
+        ));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/compiler/src/lib.rs` around lines 252 - 269, Refactor the diagnostic
handling around the terminal-backslash check so each branch computes only its
differing error and location values. Then call NormalizedParseDiagnostic::new
once with the selected error and location, preserving the existing EOF-specific
error/location and default error/location behavior.

Source: Coding guidelines


252-263: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle CR-only physical lines in line-range diagnostics.

Python accepts LF, CRLF, and CR as physical-line terminators. source_line_end_location currently splits source_text() on \n, so CR-only lines are counted as one line; line-based diagnostics can show the wrong ending column instead of using a newline-aware full-line API such as source_file.to_source_code().full_line_str(...). Add CR-only and mixed line-ending coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/compiler/src/lib.rs` around lines 252 - 263, Update
source_line_end_location and its callers to determine physical line boundaries
using the source file’s newline-aware full-line API, preserving correct line
ranges for LF, CRLF, CR-only, and mixed line endings. Add coverage for CR-only
and mixed newline inputs, including the terminal-backslash diagnostic path
around NormalizedParseDiagnostic::new.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/compiler/src/lib.rs`:
- Around line 252-269: Refactor the diagnostic handling around the
terminal-backslash check so each branch computes only its differing error and
location values. Then call NormalizedParseDiagnostic::new once with the selected
error and location, preserving the existing EOF-specific error/location and
default error/location behavior.
- Around line 252-263: Update source_line_end_location and its callers to
determine physical line boundaries using the source file’s newline-aware
full-line API, preserving correct line ranges for LF, CRLF, CR-only, and mixed
line endings. Add coverage for CR-only and mixed newline inputs, including the
terminal-backslash diagnostic path around NormalizedParseDiagnostic::new.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: b78b4251-e883-41b3-92bc-830b4f846101

📥 Commits

Reviewing files that changed from the base of the PR and between e7e4d1d and 485f808.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_exceptions.py is excluded by !Lib/**
  • Lib/test/test_tokenize.py is excluded by !Lib/**
📒 Files selected for processing (1)
  • crates/compiler/src/lib.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

z-ca-2026 Tag to track Contribution Academy 2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants