"""
Statement rules: E701-E704, E722, E731, E741-E743.
E701: multiple statements on one line (colon)
E702: multiple statements on one line (semicolon)
E703: statement ends with semicolon
E704: multiple statements on one line (def)
E722: bare except
E731: lambda assignment
E741-E743: ambiguous names
"""
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 Rule
from rude.core.types import Diagnostic, Edit, Fix
if TYPE_CHECKING:
from rude.core.node import Node
[docs]
class MultipleStatementsOnOneLineColon(Rule):
"""
E701: Multiple statements on one line (colon).
Rationale: PEP 8 requires compound statements to have the body
on a separate line for readability.
Example::
if x: return y # E701
if x: # OK
return y
"""
code: ClassVar[str] = "E701"
message: ClassVar[str] = "multiple statements on one line (colon)"
node_types = {
NodeType.IF_STATEMENT,
NodeType.FOR_STATEMENT,
NodeType.WHILE_STATEMENT,
NodeType.WITH_STATEMENT,
NodeType.TRY_STATEMENT,
NodeType.EXCEPT_CLAUSE,
NodeType.FINALLY_CLAUSE,
NodeType.ELSE_CLAUSE,
NodeType.ELIF_CLAUSE,
}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
# Check if statement body is on same line as header
body = node.child_by_field("body") or node.child_by_field("consequence")
if not body:
# Try to find block child
for child in node.named_children:
if child.type == "block":
body = child
break
if (
body
and body.line == node.line
and (
body.type != "block"
or (body.named_children and body.named_children[0].line == node.line)
)
):
# Body is on same line - check if it's a simple statement, not a block
yield self.diagnostic(node)
[docs]
class MultipleStatementsOnOneLineSemicolon(Rule):
"""
E702: Multiple statements on one line (semicolon).
Rationale: PEP 8 discourages semicolons to separate statements.
Use separate lines instead.
Example::
x = 1; y = 2 # E702
x = 1 # OK
y = 2
"""
code: ClassVar[str] = "E702"
message: ClassVar[str] = "multiple statements on one line (semicolon)"
node_types = {
NodeType.EXPRESSION_STATEMENT,
NodeType.ASSIGNMENT,
NodeType.RETURN_STATEMENT,
NodeType.PASS_STATEMENT,
NodeType.BREAK_STATEMENT,
NodeType.CONTINUE_STATEMENT,
}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
# Check if there's a semicolon in the source after this statement on the same line
ctx = node.ctx
line = ctx.get_line(node.end_line)
col = node.end_column
# Look for semicolon after this node on same line
remaining = line[col:]
if ";" in remaining:
# Check it's not in a string
semicolon_pos = remaining.find(";")
before_semi = remaining[:semicolon_pos].strip()
if not before_semi or before_semi.isspace():
yield self.diagnostic(node)
[docs]
class StatementEndsWithSemicolon(Rule):
"""
E703: Statement ends with semicolon.
Rationale: Trailing semicolons are unnecessary in Python and are
a common artifact from C/Java habits.
Example::
x = 1; # E703
x = 1 # OK
"""
code: ClassVar[str] = "E703"
message: ClassVar[str] = "statement ends with a semicolon"
node_types = {NodeType.MODULE}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
# Only check at module level
if node.parent_type is not None:
return
ctx = node.ctx
string_lines = ctx.string_lines
for lineno, line_bytes in enumerate(ctx.lines, 1):
# Skip lines inside multi-line strings (SQL, docstrings, etc.)
if lineno in string_lines:
continue
line = line_bytes.decode("utf-8", errors="replace").rstrip("\r\n")
stripped = line.rstrip()
if not stripped.endswith(";"):
continue
# Skip comment lines
lstripped = stripped.lstrip()
if lstripped.startswith("#"):
continue
# Skip if semicolon is after a comment start
code_part = stripped
from rude.utils import find_comment_start
comment_pos = find_comment_start(stripped)
if comment_pos >= 0:
code_part = stripped[:comment_pos].rstrip()
if not code_part.endswith(";"):
continue
semi_col = len(stripped) - 1
# Edit offsets are in bytes; len(stripped) is a char count, so encode
# the line to find the trailing ';' on non-ASCII lines (e.g. "café;").
line_start = ctx.line_start_byte(lineno)
semi_byte = line_start + len(stripped.encode("utf-8")) - 1
fix = Fix(
description="Remove trailing semicolon",
edits=(Edit(semi_byte, semi_byte + 1, ""),),
)
yield self.diagnostic_at(lineno, semi_col, fix=fix)
[docs]
class MultipleStatementsOnOneLineDef(Rule):
"""
E704: Multiple statements on one line (def).
Rationale: PEP 8 requires function bodies on a separate line for
readability and consistent style.
Example::
def f(): return 1 # E704
def f(): # OK
return 1
"""
code: ClassVar[str] = "E704"
message: ClassVar[str] = "multiple statements on one line (def)"
node_types = {NodeType.FUNCTION_DEFINITION}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
body = node.child_by_field("body")
if body and body.line == node.line:
yield self.diagnostic(node)
[docs]
class BareExcept(Rule):
"""
E722: Do not use bare 'except'.
Rationale: A bare ``except:`` catches all exceptions including
``KeyboardInterrupt`` and ``SystemExit``, which is almost never
intended.
Example::
try:
x = 1
except: # E722
pass
try:
x = 1
except Exception: # OK
pass
"""
code: ClassVar[str] = "E722"
message: ClassVar[str] = "do not use bare 'except'"
node_types = {NodeType.EXCEPT_CLAUSE}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
# Single pass: scan children between "except" and ":"
found_except = False
has_type = False
except_node = None
colon_node = None
for child in node.children:
if child.type == "except":
found_except = True
except_node = child
continue
if found_except:
if child.type == ":":
colon_node = child
break
if child.type == "as":
break # "as" means type was already found
if child.type != "comment":
has_type = True
break
if not has_type and except_node and colon_node:
fix = Fix(
description="Replace bare except with except Exception",
edits=(Edit(except_node.end_byte, colon_node.start_byte, " Exception"),),
)
yield self.diagnostic(node, fix=fix)
[docs]
class LambdaAssignment(Rule):
"""
E731: Do not assign a lambda expression, use a def.
Rationale: Assigning a lambda defeats its purpose as an anonymous
function. A ``def`` provides a name for tracebacks and is clearer.
Example::
f = lambda x: x + 1 # E731
def f(x): # OK
return x + 1
"""
code: ClassVar[str] = "E731"
message: ClassVar[str] = "do not assign a lambda expression, use a def"
node_types = {NodeType.ASSIGNMENT}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
right = node.child_by_field("right")
if right and right.type == "lambda":
left = node.child_by_field("left")
# Only flag simple name assignments (f = lambda: ...),
# not attribute (obj.f = lambda: ...) or subscript assignments.
if left and left.type == "identifier":
yield self.diagnostic(node)
[docs]
class AmbiguousVariableName(Rule):
"""
E741: Ambiguous variable name.
Rationale: The names ``l``, ``O``, ``I`` are easily confused with
``1``, ``0``, ``l`` in many fonts.
Example::
l = 1 # E741
O = 2 # E741
I = 3 # E741
length = 1 # OK
"""
code: ClassVar[str] = "E741"
message: ClassVar[str] = "ambiguous variable name '{name}'"
node_types = {NodeType.ASSIGNMENT, NodeType.FOR_STATEMENT, NodeType.WITH_STATEMENT}
AMBIGUOUS_NAMES = {"l", "O", "I"}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
if node.type == "assignment" or node.type == "for_statement":
left = node.child_by_field("left")
if left:
yield from self._check_target(left)
elif node.type == "with_statement":
for child in node.named_children:
if child.type == "with_clause":
for item in child.named_children:
if item.type == "with_item":
alias = item.child_by_field("alias")
if alias:
yield from self._check_target(alias)
def _check_target(self, node: Node) -> Iterator[Diagnostic]:
if node.is_identifier and node.text in self.AMBIGUOUS_NAMES:
yield self.diagnostic(node, self.message.format(name=node.text))
elif node.type in ("tuple", "list", "pattern_list", "tuple_pattern", "list_pattern"):
for child in node.named_children:
yield from self._check_target(child)
[docs]
class AmbiguousClassName(Rule):
"""
E742: Ambiguous class name.
Rationale: Single-letter class names like ``I``, ``O``, ``l`` are
easily confused with digits in many fonts.
Example::
class I: # E742
pass
class Index: # OK
pass
"""
code: ClassVar[str] = "E742"
message: ClassVar[str] = "ambiguous class name '{name}'"
node_types = {NodeType.CLASS_DEFINITION}
AMBIGUOUS_NAMES = {"l", "O", "I"}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
name_node = node.child_by_field("name")
if name_node and name_node.text in self.AMBIGUOUS_NAMES:
yield self.diagnostic(name_node, self.message.format(name=name_node.text))
[docs]
class AmbiguousFunctionName(Rule):
"""
E743: Ambiguous function name.
Rationale: Single-letter function names like ``I``, ``O``, ``l``
are easily confused with digits in many fonts.
Example::
def l(): # E743
pass
def length(): # OK
pass
"""
code: ClassVar[str] = "E743"
message: ClassVar[str] = "ambiguous function name '{name}'"
node_types = {NodeType.FUNCTION_DEFINITION}
AMBIGUOUS_NAMES = {"l", "O", "I"}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
name_node = node.child_by_field("name")
if name_node and name_node.text in self.AMBIGUOUS_NAMES:
yield self.diagnostic(name_node, self.message.format(name=name_node.text))
STATEMENT_RULES = [
MultipleStatementsOnOneLineColon,
MultipleStatementsOnOneLineSemicolon,
StatementEndsWithSemicolon,
MultipleStatementsOnOneLineDef,
BareExcept,
LambdaAssignment,
AmbiguousVariableName,
AmbiguousClassName,
AmbiguousFunctionName,
]
__all__ = [
"STATEMENT_RULES",
"AmbiguousClassName",
"AmbiguousFunctionName",
"AmbiguousVariableName",
"BareExcept",
"LambdaAssignment",
"MultipleStatementsOnOneLineColon",
"MultipleStatementsOnOneLineDef",
"MultipleStatementsOnOneLineSemicolon",
"StatementEndsWithSemicolon",
]