Source code for rude.rules.pycodestyle.warnings

"""
Warning rules: W291-W293, W391, W605.

W291: trailing whitespace
W292: no newline at end of file
W293: blank line contains whitespace
W391: blank line at end of file
W605: invalid escape sequence
"""

from __future__ import annotations

from collections.abc import Iterator
from typing import TYPE_CHECKING, ClassVar

from rude.core.node_types import NodeType
from rude.core.rule import LineRule, Rule
from rude.core.types import Diagnostic, Edit, FileContext, Fix

if TYPE_CHECKING:
    from rude._rust import LineInfo
    from rude.core.node import Node


[docs] class TrailingWhitespace(LineRule): """ W291: Trailing whitespace. Rationale: Trailing whitespace creates noisy diffs and is banned by PEP 8. Example:: x = 1 \\n # W291 - trailing spaces x = 1\\n # OK """ code: ClassVar[str] = "W291" message: ClassVar[str] = "trailing whitespace" uses_line_infos: ClassVar[bool] = True
[docs] def check_line( self, line: str, lineno: int, ctx: FileContext, *, comment_pos: int = -1 ) -> Iterator[Diagnostic]: # Check for trailing whitespace (but not blank lines - that's W293) if line and line != line.rstrip(): yield self.diagnostic_at( lineno, len(line.rstrip()), fix=self._make_fix(lineno, line, ctx), )
[docs] def check_line_info( self, lineno: int, info: LineInfo, ctx: FileContext, ) -> Iterator[Diagnostic]: line_len = info.line_len trailing_ws = info.trailing_ws if line_len > 0 and trailing_ws > 0: col = line_len - trailing_ws yield self.diagnostic_at( lineno, col, fix=self._make_fix_from_info(lineno, line_len, trailing_ws, ctx), )
def _make_fix(self, lineno: int, line: str, ctx: FileContext) -> Fix: """Create fix to remove trailing whitespace.""" line_start = ctx.line_start_byte(lineno) old_end = line_start + len(line) new_end = line_start + len(line.rstrip()) return Fix( description="Remove trailing whitespace", edits=(Edit(new_end, old_end, ""),), ) def _make_fix_from_info( self, lineno: int, line_len: int, trailing_ws: int, ctx: FileContext, ) -> Fix: """Create fix using pre-computed line info (no decode needed).""" line_start = ctx.line_start_byte(lineno) old_end = line_start + line_len new_end = old_end - trailing_ws return Fix( description="Remove trailing whitespace", edits=(Edit(new_end, old_end, ""),), )
[docs] class NoNewlineAtEndOfFile(Rule): """ W292: No newline at end of file. Rationale: POSIX requires text files to end with a newline. Missing newlines cause issues with some tools and diffs. Example:: x = 1<EOF> # W292 - missing final newline x = 1\\n<EOF> # OK """ code: ClassVar[str] = "W292" message: ClassVar[str] = "no newline at end of file" node_types = {NodeType.MODULE}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: if node.parent is not None: return ctx = node.ctx if not ctx.source: return # Check if file ends with newline if not ctx.source.endswith(b"\n"): last_line = len(ctx.lines) last_col = len(ctx.lines[-1]) if ctx.lines else 0 yield self.diagnostic_at( last_line, last_col, fix=Fix( description="Add newline at end of file", edits=(Edit(len(ctx.source), len(ctx.source), "\n"),), ), )
[docs] class BlankLineContainsWhitespace(LineRule): """ W293: Blank line contains whitespace. Rationale: Invisible whitespace on blank lines creates noisy diffs and wastes bytes. Example:: x = 1 \\n # W293 - blank line with spaces y = 2 x = 1 \\n # OK - truly blank y = 2 """ code: ClassVar[str] = "W293" message: ClassVar[str] = "blank line contains whitespace" uses_line_infos: ClassVar[bool] = True
[docs] def check_line( self, line: str, lineno: int, ctx: FileContext, *, comment_pos: int = -1 ) -> Iterator[Diagnostic]: # Blank line with whitespace (line is already stripped of newlines) if line and line.isspace(): yield self.diagnostic_at( lineno, 0, fix=self._make_fix(lineno, line, ctx), )
[docs] def check_line_info( self, lineno: int, info: LineInfo, ctx: FileContext, ) -> Iterator[Diagnostic]: line_len = info.line_len is_blank = info.is_blank if is_blank and line_len > 0: yield self.diagnostic_at( lineno, 0, fix=self._make_fix_from_info(lineno, line_len, ctx), )
def _make_fix(self, lineno: int, line: str, ctx: FileContext) -> Fix: """Create fix to remove whitespace from blank line.""" line_start = ctx.line_start_byte(lineno) return Fix( description="Remove whitespace from blank line", edits=(Edit(line_start, line_start + len(line), ""),), ) def _make_fix_from_info(self, lineno: int, line_len: int, ctx: FileContext) -> Fix: """Create fix using pre-computed line info (no decode needed).""" line_start = ctx.line_start_byte(lineno) return Fix( description="Remove whitespace from blank line", edits=(Edit(line_start, line_start + line_len, ""),), )
[docs] class BlankLineAtEndOfFile(Rule): """ W391: Blank line at end of file. Rationale: Trailing blank lines add no value and create noisy diffs. Example:: x = 1 \\n<EOF> # W391 - trailing blank line x = 1\\n<EOF> # OK """ code: ClassVar[str] = "W391" message: ClassVar[str] = "blank line at end of file" node_types = {NodeType.MODULE}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: if node.parent is not None: return ctx = node.ctx lines = ctx.lines if len(lines) < 2: return # Check for blank lines at end blank_count = 0 for line in reversed(lines): stripped = line.decode("utf-8", errors="replace").rstrip("\r\n") if not stripped: blank_count += 1 else: break if blank_count > 0: # Report on first blank line at end yield self.diagnostic_at(len(lines) - blank_count + 1, 0)
[docs] class InvalidEscapeSequence(Rule): """ W605: Invalid escape sequence. Rationale: Invalid escape sequences raise ``DeprecationWarning`` in Python 3.12+ and will become ``SyntaxError`` in a future version. Example:: x = "\\d+" # W605 - invalid escape sequence x = r"\\d+" # OK - raw string x = "\\\\d+" # OK - escaped backslash """ code: ClassVar[str] = "W605" message: ClassVar[str] = "invalid escape sequence '\\{char}'" node_types = {NodeType.STRING} # Valid escape sequences in Python VALID_ESCAPES = set("\\abfnrtvx01234567NuU'\"")
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: text = node.text # Skip raw strings if text.startswith( ( "r'", 'r"', "R'", 'R"', "br'", 'br"', "Br'", 'Br"', "bR'", 'bR"', "BR'", 'BR"', "rb'", 'rb"', "rB'", 'rB"', "Rb'", 'Rb"', "RB'", 'RB"', ) ): return # Skip f-strings that are also raw if text.startswith( ( "fr'", 'fr"', "Fr'", 'Fr"', "fR'", 'fR"', "FR'", 'FR"', "rf'", 'rf"', "rF'", 'rF"', "Rf'", 'Rf"', "RF'", 'RF"', ) ): return # Find string content (after quotes) quote_char = None triple_quote = False start_idx = 0 for i, c in enumerate(text): if c in ('"', "'"): quote_char = c if text[i : i + 3] == c * 3: triple_quote = True start_idx = i + 3 else: start_idx = i + 1 break if quote_char is None: return end_idx = len(text) - (3 if triple_quote else 1) content = text[start_idx:end_idx] # Find invalid escape sequences i = 0 while i < len(content): if content[i] == "\\": if i + 1 < len(content): next_char = content[i + 1] if next_char not in self.VALID_ESCAPES and next_char != "\n": # Calculate position in original source # This is approximate - good enough for most cases yield self.diagnostic( node, self.message.format(char=next_char), ) # Only report first invalid escape per string return i += 2 else: i += 1
WARNING_RULES = [ TrailingWhitespace, NoNewlineAtEndOfFile, BlankLineContainsWhitespace, BlankLineAtEndOfFile, InvalidEscapeSequence, ] __all__ = [ "WARNING_RULES", "BlankLineAtEndOfFile", "BlankLineContainsWhitespace", "InvalidEscapeSequence", "NoNewlineAtEndOfFile", "TrailingWhitespace", ]