"""
Whitespace rules: E2xx.
E201: whitespace after '('
E202: whitespace before ')'
E203: whitespace before ':'
E211: whitespace before '(' (function call)
E221: multiple spaces before operator
E222: multiple spaces after operator
E223: tab before operator
E224: tab after operator
E225: missing whitespace around operator
E226: missing whitespace around arithmetic operator (ignored by default)
E227: missing whitespace around bitwise operator
E228: missing whitespace around modulo operator
E231: missing whitespace after ','
E241: multiple spaces after ',' (ignored by default)
E242: tab after ',' (ignored by default)
E251: unexpected spaces around keyword / parameter equals
E261: at least two spaces before inline comment
E262: inline comment should start with '# '
E265: block comment should start with '# '
E266: too many leading '#' for block comment
E271: multiple spaces after keyword
E272: multiple spaces before keyword
E273: tab after keyword
E274: tab before keyword
E275: missing whitespace after keyword
"""
from __future__ import annotations
import re
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, Location, Severity
if TYPE_CHECKING:
from rude._rust import LineInfo
from rude.core.node import Node
# style_flags bit constants (LineInfo.style_flags field)
_DOUBLE_SPACE_AROUND_OP = 0x01 # E221, E222
_TAB_AROUND_OP = 0x02 # E223, E224
_DOUBLE_SPACE_AFTER_COMMA = 0x04 # E241
_TAB_AFTER_COMMA = 0x08 # E242
_DOUBLE_SPACE_AROUND_KW = 0x10 # E271, E272
_TAB_AROUND_KW = 0x20 # E273, E274
[docs]
class WhitespaceAfterOpenBracket(Rule):
"""
E201: Whitespace after '(', '[', or '{'.
Rationale: PEP 8 requires no whitespace immediately after opening
brackets.
Example::
spam( ham) # E201
spam(ham) # OK
"""
code: ClassVar[str] = "E201"
message: ClassVar[str] = "whitespace after '{bracket}'"
node_types = {
NodeType.ARGUMENT_LIST,
NodeType.PARAMETERS,
NodeType.TUPLE,
NodeType.LIST,
NodeType.DICTIONARY,
NodeType.SET,
NodeType.SUBSCRIPT,
}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
# Quick-reject: empty brackets can't have whitespace issues
if node.named_child_count == 0:
return
children = node.children
if not children:
return
for i, child in enumerate(children):
if child.type in ("(", "[", "{") and i + 1 < len(children):
next_child = children[i + 1]
# Same line, space after bracket; check it's not empty brackets
if (
next_child.line == child.line
and next_child.column > child.column + 1
and next_child.type not in (")", "]", "}")
):
yield self.diagnostic_at(
child.line,
child.column + 1,
self.message.format(bracket=child.text),
)
[docs]
class WhitespaceBeforeCloseBracket(Rule):
"""
E202: Whitespace before ')', ']', or '}'.
Rationale: PEP 8 requires no whitespace immediately before closing
brackets.
Example::
spam(ham ) # E202
spam(ham) # OK
"""
code: ClassVar[str] = "E202"
message: ClassVar[str] = "whitespace before '{bracket}'"
node_types = {
NodeType.ARGUMENT_LIST,
NodeType.PARAMETERS,
NodeType.TUPLE,
NodeType.LIST,
NodeType.DICTIONARY,
NodeType.SET,
NodeType.SUBSCRIPT,
}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
# Quick-reject: empty brackets can't have whitespace issues
if node.named_child_count == 0:
return
children = node.children
if not children:
return
for i, child in enumerate(children):
if child.type in (")", "]", "}") and i > 0:
prev_child = children[i - 1]
expected_col = prev_child.end_column
# Same line, space before bracket; check it's not empty brackets
if (
prev_child.line == child.line
and child.column > expected_col
and prev_child.type not in ("(", "[", "{")
):
yield self.diagnostic_at(
child.line,
expected_col,
self.message.format(bracket=child.text),
)
[docs]
class WhitespaceBeforeColon(Rule):
"""
E203: Whitespace before ':' or ','.
Rationale: PEP 8 requires no whitespace immediately before colons,
commas, or semicolons.
Example::
spam[1 :] # E203
spam[1:] # OK
"""
code: ClassVar[str] = "E203"
message: ClassVar[str] = "whitespace before '{char}'"
node_types = {
NodeType.SLICE,
NodeType.PAIR,
NodeType.DICTIONARY,
NodeType.ARGUMENT_LIST,
NodeType.PARAMETERS,
}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
if node.named_child_count == 0:
return
for child in node.children:
if child.type in (":", ","):
prev = child.prev_sibling
if prev and prev.line == child.line:
expected = prev.end_column
if child.column > expected:
yield self.diagnostic_at(
child.line, expected, self.message.format(char=child.text)
)
[docs]
class WhitespaceBeforeParameters(Rule):
"""
E211: Whitespace before '(' in function call.
Rationale: PEP 8 requires no whitespace between a function name
and its argument list.
Example::
spam (1) # E211
spam(1) # OK
"""
code: ClassVar[str] = "E211"
message: ClassVar[str] = "whitespace before '('"
node_types = {NodeType.CALL}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
func = node.child_by_field("function")
args = node.child_by_field("arguments")
if func and args and args.line == func.end_line and args.column > func.end_column:
# Check if there's space between function and arguments
yield self.diagnostic_at(func.end_line, func.end_column)
[docs]
class MissingWhitespaceAfterComma(Rule):
"""
E231: Missing whitespace after ',', ';', or ':'.
Rationale: PEP 8 requires a space after commas, semicolons, and
colons for readability.
Example::
[1,2,3] # E231 (multiple)
[1, 2, 3] # OK
"""
code: ClassVar[str] = "E231"
message: ClassVar[str] = "missing whitespace after '{char}'"
node_types = {
NodeType.ARGUMENT_LIST,
NodeType.PARAMETERS,
NodeType.TUPLE,
NodeType.LIST,
NodeType.DICTIONARY,
NodeType.PAIR,
NodeType.TYPED_PARAMETER,
}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
for child in node.children:
if child.type in (",", ":"):
next_sib = child.next_sibling
if (
next_sib
and next_sib.line == child.line
and next_sib.column == child.column + 1
and next_sib.type not in (")", "]", "}")
):
# Next token should start at column + 2 (punctuation + space)
yield self.diagnostic_at(
child.line, child.column + 1, self.message.format(char=child.type)
)
[docs]
class UnexpectedSpacesAroundKeywordEquals(Rule):
"""
E251: Unexpected spaces around keyword / parameter equals.
Rationale: PEP 8 requires no spaces around ``=`` in default
parameter values and keyword arguments.
Example::
def foo(x = 1): # E251
def foo(x=1): # OK
"""
code: ClassVar[str] = "E251"
message: ClassVar[str] = "unexpected spaces around keyword / parameter equals"
node_types = {NodeType.DEFAULT_PARAMETER, NodeType.KEYWORD_ARGUMENT}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
for child in node.children:
if child.type == "=":
prev = child.prev_sibling
next_sib = child.next_sibling
has_space_before = (
prev and prev.line == child.line and child.column > prev.end_column
)
has_space_after = (
next_sib and next_sib.line == child.line and next_sib.column > child.column + 1
)
if has_space_before or has_space_after:
yield self.diagnostic(child)
# ─────────────────────────────────────────────────────────────────────────────
# E261-E266: Comment whitespace (optimized with LineRule)
# ─────────────────────────────────────────────────────────────────────────────
def _is_inline_comment(line: str, comment_pos: int) -> bool:
"""Check if the comment is inline (has code before it)."""
before = line[:comment_pos]
return bool(before.strip())
# ─────────────────────────────────────────────────────────────────────────────
# E221-E224: Whitespace around operators (optimized with LineRule)
# ─────────────────────────────────────────────────────────────────────────────
# Operator pattern shared by E221-E224
_OPERATOR_PATTERN = r"[-+*/%@&|^<>=!]+|:=|\*\*|//"
# E221: Multiple spaces (no tabs) before operator
_E221_PATTERN = re.compile(rf"[^\s]( {{2,}})({_OPERATOR_PATTERN})")
# E222: Multiple spaces (no tabs) after operator
_E222_PATTERN = re.compile(rf"({_OPERATOR_PATTERN})( {{2,}})[^\s]")
# E223: Tab before operator
_E223_PATTERN = re.compile(rf"[^\s](\s*\t\s*)({_OPERATOR_PATTERN})")
# E224: Tab after operator
_E224_PATTERN = re.compile(rf"({_OPERATOR_PATTERN})(\s*\t\s*)[^\s]")
[docs]
class MultipleSpacesBeforeOperator(LineRule):
"""
E221: Multiple spaces before operator.
Rationale: PEP 8 requires exactly one space around operators
(except for alignment, which is discouraged).
Example::
a = 4 + 5 # E221
a = 4 + 5 # OK
"""
code: ClassVar[str] = "E221"
message: ClassVar[str] = "multiple spaces before operator"
uses_line_infos: ClassVar[bool] = True
[docs]
def check_line_info(
self,
lineno: int,
info: LineInfo,
ctx: FileContext,
) -> Iterator[Diagnostic]:
if not (info.style_flags & _DOUBLE_SPACE_AROUND_OP):
return
line = ctx.text_lines[lineno - 1]
comment_pos = info.comment_start
yield from self.check_line(line, lineno, ctx, comment_pos=comment_pos)
[docs]
def check_line(
self, line: str, lineno: int, ctx: FileContext, *, comment_pos: int = -1
) -> Iterator[Diagnostic]:
code = line[:comment_pos] if comment_pos >= 0 else line
for match in _E221_PATTERN.finditer(code):
col = match.start(1)
yield self.diagnostic_at(
lineno,
col,
fix=self._make_fix(lineno, match, ctx),
)
def _make_fix(self, lineno: int, match: re.Match[str], ctx: FileContext) -> Fix:
line_start = ctx.line_start_byte(lineno)
start = line_start + match.start(1)
end = line_start + match.end(1)
return Fix(description="Replace with single space", edits=(Edit(start, end, " "),))
[docs]
class MultipleSpacesAfterOperator(LineRule):
"""
E222: Multiple spaces after operator.
Rationale: PEP 8 requires exactly one space around operators.
Example::
a = 4 + 5 # E222
a = 4 + 5 # OK
"""
code: ClassVar[str] = "E222"
message: ClassVar[str] = "multiple spaces after operator"
uses_line_infos: ClassVar[bool] = True
[docs]
def check_line_info(
self,
lineno: int,
info: LineInfo,
ctx: FileContext,
) -> Iterator[Diagnostic]:
if not (info.style_flags & _DOUBLE_SPACE_AROUND_OP):
return
line = ctx.text_lines[lineno - 1]
comment_pos = info.comment_start
yield from self.check_line(line, lineno, ctx, comment_pos=comment_pos)
[docs]
def check_line(
self, line: str, lineno: int, ctx: FileContext, *, comment_pos: int = -1
) -> Iterator[Diagnostic]:
code = line[:comment_pos] if comment_pos >= 0 else line
for match in _E222_PATTERN.finditer(code):
col = match.start(2)
yield self.diagnostic_at(
lineno,
col,
fix=self._make_fix(lineno, match, ctx),
)
def _make_fix(self, lineno: int, match: re.Match[str], ctx: FileContext) -> Fix:
line_start = ctx.line_start_byte(lineno)
start = line_start + match.start(2)
end = line_start + match.end(2)
return Fix(description="Replace with single space", edits=(Edit(start, end, " "),))
[docs]
class TabBeforeOperator(LineRule):
"""
E223: Tab before operator.
Rationale: PEP 8 requires spaces, not tabs, around operators.
Example::
a = 4\\t+ 5 # E223
a = 4 + 5 # OK
"""
code: ClassVar[str] = "E223"
message: ClassVar[str] = "tab before operator"
uses_line_infos: ClassVar[bool] = True
[docs]
def check_line_info(
self,
lineno: int,
info: LineInfo,
ctx: FileContext,
) -> Iterator[Diagnostic]:
if not (info.style_flags & _TAB_AROUND_OP):
return
line = ctx.text_lines[lineno - 1]
comment_pos = info.comment_start
yield from self.check_line(line, lineno, ctx, comment_pos=comment_pos)
[docs]
def check_line(
self, line: str, lineno: int, ctx: FileContext, *, comment_pos: int = -1
) -> Iterator[Diagnostic]:
code = line[:comment_pos] if comment_pos >= 0 else line
for match in _E223_PATTERN.finditer(code):
col = match.start(1)
yield self.diagnostic_at(
lineno,
col,
fix=self._make_fix(lineno, match, ctx),
)
def _make_fix(self, lineno: int, match: re.Match[str], ctx: FileContext) -> Fix:
line_start = ctx.line_start_byte(lineno)
start = line_start + match.start(1)
end = line_start + match.end(1)
return Fix(description="Replace tab with space", edits=(Edit(start, end, " "),))
[docs]
class TabAfterOperator(LineRule):
"""
E224: Tab after operator.
Rationale: PEP 8 requires spaces, not tabs, around operators.
Example::
a = 4 +\\t5 # E224
a = 4 + 5 # OK
"""
code: ClassVar[str] = "E224"
message: ClassVar[str] = "tab after operator"
uses_line_infos: ClassVar[bool] = True
[docs]
def check_line_info(
self,
lineno: int,
info: LineInfo,
ctx: FileContext,
) -> Iterator[Diagnostic]:
if not (info.style_flags & _TAB_AROUND_OP):
return
line = ctx.text_lines[lineno - 1]
comment_pos = info.comment_start
yield from self.check_line(line, lineno, ctx, comment_pos=comment_pos)
[docs]
def check_line(
self, line: str, lineno: int, ctx: FileContext, *, comment_pos: int = -1
) -> Iterator[Diagnostic]:
code = line[:comment_pos] if comment_pos >= 0 else line
for match in _E224_PATTERN.finditer(code):
col = match.start(2)
yield self.diagnostic_at(
lineno,
col,
fix=self._make_fix(lineno, match, ctx),
)
def _make_fix(self, lineno: int, match: re.Match[str], ctx: FileContext) -> Fix:
line_start = ctx.line_start_byte(lineno)
start = line_start + match.start(2)
end = line_start + match.end(2)
return Fix(description="Replace tab with space", edits=(Edit(start, end, " "),))
# ─────────────────────────────────────────────────────────────────────────────
# E225-E228: Missing whitespace around operators
# ─────────────────────────────────────────────────────────────────────────────
[docs]
class MissingWhitespaceAroundOperator(Rule):
"""
E225: Missing whitespace around operator.
Rationale: PEP 8 requires spaces around assignment, comparison, and
binary operators for readability.
Example::
i=i+1 # E225
i = i + 1 # OK
"""
code: ClassVar[str] = "E225"
message: ClassVar[str] = "missing whitespace around operator"
node_types = {
NodeType.BINARY_OPERATOR,
NodeType.AUGMENTED_ASSIGNMENT,
NodeType.COMPARISON_OPERATOR,
NodeType.ASSIGNMENT,
}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
# Skip slices and default parameters
if node.parent_type in (
"slice",
"default_parameter",
"typed_default_parameter",
"keyword_argument",
):
return
children = node.children
if len(children) < 3:
return
# Find the operator
for i, child in enumerate(children):
if child.type in (
"+",
"-",
"*",
"/",
"//",
"%",
"**",
"@",
"+=",
"-=",
"*=",
"/=",
"//=",
"%=",
"**=",
"@=",
"&=",
"|=",
"^=",
">>=",
"<<=",
"==",
"!=",
"<",
">",
"<=",
">=",
":=",
"=",
):
# Check space before
if i > 0:
prev = children[i - 1]
if prev.line == child.line and child.column == prev.end_column:
yield self.diagnostic(child)
return
# Check space after
if i + 1 < len(children):
next_child = children[i + 1]
if next_child.line == child.line and next_child.column == child.end_column:
yield self.diagnostic(child)
return
[docs]
class MissingWhitespaceAroundArithmeticOperator(Rule):
"""
E226: Missing whitespace around arithmetic operator.
Rationale: PEP 8 allows omitting spaces around arithmetic operators
for grouping, but consistent spacing improves readability.
This rule is ignored by default per pycodestyle convention.
Example::
c = (a+b) * (a-b) # E226
c = (a + b) * (a - b) # OK
"""
code: ClassVar[str] = "E226"
message: ClassVar[str] = "missing whitespace around arithmetic operator"
node_types = {NodeType.BINARY_OPERATOR}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
parent = node.parent
if parent and parent.type in ("slice",):
return
children = node.children
if len(children) < 3:
return
# Check for arithmetic operators: +, -, *, /, //, **, @
for i, child in enumerate(children):
if child.type in ("+", "-", "*", "/", "//", "**", "@"):
# Exclude unary operators
if i == 0:
continue
prev = children[i - 1]
next_child = children[i + 1] if i + 1 < len(children) else None
# Check missing space before
if prev.line == child.line and child.column == prev.end_column:
yield self.diagnostic(child)
return
# Check missing space after
if (
next_child
and next_child.line == child.line
and next_child.column == child.end_column
):
yield self.diagnostic(child)
return
[docs]
class MissingWhitespaceAroundBitwiseOperator(Rule):
"""
E227: Missing whitespace around bitwise or shift operator.
Rationale: PEP 8 requires spaces around bitwise and shift operators
for readability.
Example::
x = x|y # E227
x = x | y # OK
"""
code: ClassVar[str] = "E227"
message: ClassVar[str] = "missing whitespace around bitwise or shift operator"
node_types = {NodeType.BINARY_OPERATOR}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
children = node.children
if len(children) < 3:
return
for i, child in enumerate(children):
if child.type in ("&", "|", "^", "<<", ">>"):
if i == 0:
continue
prev = children[i - 1]
next_child = children[i + 1] if i + 1 < len(children) else None
if prev.line == child.line and child.column == prev.end_column:
yield self.diagnostic(child)
return
if (
next_child
and next_child.line == child.line
and next_child.column == child.end_column
):
yield self.diagnostic(child)
return
[docs]
class MissingWhitespaceAroundModuloOperator(Rule):
"""
E228: Missing whitespace around modulo operator.
Rationale: PEP 8 requires spaces around the modulo operator for
readability.
Example::
x = x%y # E228
x = x % y # OK
"""
code: ClassVar[str] = "E228"
message: ClassVar[str] = "missing whitespace around modulo operator"
node_types = {NodeType.BINARY_OPERATOR}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
children = node.children
if len(children) < 3:
return
for i, child in enumerate(children):
if child.type == "%":
if i == 0:
continue
prev = children[i - 1]
next_child = children[i + 1] if i + 1 < len(children) else None
if prev.line == child.line and child.column == prev.end_column:
yield self.diagnostic(child)
return
if (
next_child
and next_child.line == child.line
and next_child.column == child.end_column
):
yield self.diagnostic(child)
return
# ─────────────────────────────────────────────────────────────────────────────
# E241-E242: Multiple spaces/tab after comma (optimized with LineRule)
# ─────────────────────────────────────────────────────────────────────────────
# E241: Multiple spaces (no tabs) after comma/semicolon
_E241_PATTERN = re.compile(r"([,;])( {2,})[^\s]")
# E242: Tab after comma/semicolon
_E242_PATTERN = re.compile(r"([,;])(\s*\t\s*)[^\s]")
[docs]
class MultipleSpacesAfterComma(LineRule):
"""
E241: Multiple spaces after ',', ';', or ':'.
Rationale: PEP 8 requires exactly one space after commas and
semicolons. This rule is ignored by default per pycodestyle
convention.
Example::
a = (1, 2) # E241
a = (1, 2) # OK
"""
code: ClassVar[str] = "E241"
message: ClassVar[str] = "multiple spaces after '{char}'"
uses_line_infos: ClassVar[bool] = True
[docs]
def check_line_info(
self,
lineno: int,
info: LineInfo,
ctx: FileContext,
) -> Iterator[Diagnostic]:
if not (info.style_flags & _DOUBLE_SPACE_AFTER_COMMA):
return
line = ctx.text_lines[lineno - 1]
comment_pos = info.comment_start
yield from self.check_line(line, lineno, ctx, comment_pos=comment_pos)
[docs]
def check_line(
self, line: str, lineno: int, ctx: FileContext, *, comment_pos: int = -1
) -> Iterator[Diagnostic]:
code = line[:comment_pos] if comment_pos >= 0 else line
for match in _E241_PATTERN.finditer(code):
char = match.group(1)
col = match.start(2)
yield self.diagnostic_at(
lineno,
col,
self.message.format(char=char),
fix=self._make_fix(lineno, match, ctx),
)
def _make_fix(self, lineno: int, match: re.Match[str], ctx: FileContext) -> Fix:
line_start = ctx.line_start_byte(lineno)
start = line_start + match.start(2)
end = line_start + match.end(2)
return Fix(description="Replace with single space", edits=(Edit(start, end, " "),))
[docs]
class TabAfterComma(LineRule):
"""
E242: Tab after ',', ';', or ':'.
Rationale: PEP 8 requires spaces, not tabs, after commas and
semicolons. This rule is ignored by default per pycodestyle
convention.
Example::
a = (1,\\t2) # E242
a = (1, 2) # OK
"""
code: ClassVar[str] = "E242"
message: ClassVar[str] = "tab after '{char}'"
uses_line_infos: ClassVar[bool] = True
[docs]
def check_line_info(
self,
lineno: int,
info: LineInfo,
ctx: FileContext,
) -> Iterator[Diagnostic]:
if not (info.style_flags & _TAB_AFTER_COMMA):
return
line = ctx.text_lines[lineno - 1]
comment_pos = info.comment_start
yield from self.check_line(line, lineno, ctx, comment_pos=comment_pos)
[docs]
def check_line(
self, line: str, lineno: int, ctx: FileContext, *, comment_pos: int = -1
) -> Iterator[Diagnostic]:
code = line[:comment_pos] if comment_pos >= 0 else line
for match in _E242_PATTERN.finditer(code):
char = match.group(1)
col = match.start(2)
yield self.diagnostic_at(
lineno,
col,
self.message.format(char=char),
fix=self._make_fix(lineno, match, ctx),
)
def _make_fix(self, lineno: int, match: re.Match[str], ctx: FileContext) -> Fix:
line_start = ctx.line_start_byte(lineno)
start = line_start + match.start(2)
end = line_start + match.end(2)
return Fix(description="Replace tab with space", edits=(Edit(start, end, " "),))
# ─────────────────────────────────────────────────────────────────────────────
# E271-E275: Whitespace around keywords (optimized with LineRule)
# ─────────────────────────────────────────────────────────────────────────────
#
# These rules use LineRule for efficient single-pass line iteration.
# Patterns are pre-compiled at class level with all keywords combined.
_KEYWORDS = (
"and",
"or",
"not",
"in",
"is",
"if",
"elif",
"else",
"for",
"while",
"with",
"as",
"try",
"except",
"finally",
"return",
"yield",
"import",
"from",
"def",
"class",
"lambda",
"raise",
"assert",
"global",
"nonlocal",
"pass",
"break",
"continue",
"async",
"await",
)
_BINARY_KEYWORDS = ("and", "or", "in", "is", "not", "if", "else")
# Pre-compiled patterns with all keywords combined (single regex per rule)
_KEYWORDS_PATTERN = "|".join(_KEYWORDS)
_BINARY_KEYWORDS_PATTERN = "|".join(_BINARY_KEYWORDS)
# E271: keyword followed by 2+ spaces (not tabs) then non-space/colon
_E271_PATTERN = re.compile(rf"(?<![a-zA-Z_])({_KEYWORDS_PATTERN})( {{2,}})[^\s:]")
# E272: 2+ spaces (not tabs) before binary keyword
_E272_PATTERN = re.compile(rf"[^\s]( {{2,}})({_BINARY_KEYWORDS_PATTERN})(?![a-zA-Z_])")
# E273: keyword followed by tab
_E273_PATTERN = re.compile(rf"(?<![a-zA-Z_])({_KEYWORDS_PATTERN})(\s*\t\s*)[^\s:]")
# E274: tab before binary keyword
_E274_PATTERN = re.compile(rf"[^\s](\s*\t\s*)({_BINARY_KEYWORDS_PATTERN})(?![a-zA-Z_])")
[docs]
class MultipleSpacesAfterKeyword(LineRule):
"""
E271: Multiple spaces after keyword.
Rationale: PEP 8 requires exactly one space after keywords.
Example::
if x: # E271
if x: # OK
"""
code: ClassVar[str] = "E271"
message: ClassVar[str] = "multiple spaces after keyword"
uses_line_infos: ClassVar[bool] = True
[docs]
def check_line_info(
self,
lineno: int,
info: LineInfo,
ctx: FileContext,
) -> Iterator[Diagnostic]:
if not (info.style_flags & _DOUBLE_SPACE_AROUND_KW):
return
line = ctx.text_lines[lineno - 1]
comment_pos = info.comment_start
yield from self.check_line(line, lineno, ctx, comment_pos=comment_pos)
[docs]
def check_line(
self, line: str, lineno: int, ctx: FileContext, *, comment_pos: int = -1
) -> Iterator[Diagnostic]:
code = line[:comment_pos] if comment_pos >= 0 else line
for match in _E271_PATTERN.finditer(code):
col = match.start(2)
yield self.diagnostic_at(
lineno,
col,
fix=self._make_fix(lineno, match, ctx),
)
def _make_fix(self, lineno: int, match: re.Match[str], ctx: FileContext) -> Fix:
line_start = ctx.line_start_byte(lineno)
start = line_start + match.start(2)
end = line_start + match.end(2)
return Fix(description="Replace with single space", edits=(Edit(start, end, " "),))
[docs]
class MultipleSpacesBeforeKeyword(LineRule):
"""
E272: Multiple spaces before keyword.
Rationale: PEP 8 requires exactly one space before keywords.
Example::
True and False # E272
True and False # OK
"""
code: ClassVar[str] = "E272"
message: ClassVar[str] = "multiple spaces before keyword"
uses_line_infos: ClassVar[bool] = True
[docs]
def check_line_info(
self,
lineno: int,
info: LineInfo,
ctx: FileContext,
) -> Iterator[Diagnostic]:
if not (info.style_flags & _DOUBLE_SPACE_AROUND_KW):
return
line = ctx.text_lines[lineno - 1]
comment_pos = info.comment_start
yield from self.check_line(line, lineno, ctx, comment_pos=comment_pos)
[docs]
def check_line(
self, line: str, lineno: int, ctx: FileContext, *, comment_pos: int = -1
) -> Iterator[Diagnostic]:
code = line[:comment_pos] if comment_pos >= 0 else line
for match in _E272_PATTERN.finditer(code):
col = match.start(1)
yield self.diagnostic_at(
lineno,
col,
fix=self._make_fix(lineno, match, ctx),
)
def _make_fix(self, lineno: int, match: re.Match[str], ctx: FileContext) -> Fix:
line_start = ctx.line_start_byte(lineno)
start = line_start + match.start(1)
end = line_start + match.end(1)
return Fix(description="Replace with single space", edits=(Edit(start, end, " "),))
[docs]
class TabAfterKeyword(LineRule):
"""
E273: Tab after keyword.
Rationale: PEP 8 requires spaces, not tabs, after keywords.
Example::
if\\tx: # E273
if x: # OK
"""
code: ClassVar[str] = "E273"
message: ClassVar[str] = "tab after keyword"
uses_line_infos: ClassVar[bool] = True
[docs]
def check_line_info(
self,
lineno: int,
info: LineInfo,
ctx: FileContext,
) -> Iterator[Diagnostic]:
if not (info.style_flags & _TAB_AROUND_KW):
return
line = ctx.text_lines[lineno - 1]
comment_pos = info.comment_start
yield from self.check_line(line, lineno, ctx, comment_pos=comment_pos)
[docs]
def check_line(
self, line: str, lineno: int, ctx: FileContext, *, comment_pos: int = -1
) -> Iterator[Diagnostic]:
code = line[:comment_pos] if comment_pos >= 0 else line
for match in _E273_PATTERN.finditer(code):
col = match.start(2)
yield self.diagnostic_at(
lineno,
col,
fix=self._make_fix(lineno, match, ctx),
)
def _make_fix(self, lineno: int, match: re.Match[str], ctx: FileContext) -> Fix:
line_start = ctx.line_start_byte(lineno)
start = line_start + match.start(2)
end = line_start + match.end(2)
return Fix(description="Replace tab with space", edits=(Edit(start, end, " "),))
[docs]
class TabBeforeKeyword(LineRule):
"""
E274: Tab before keyword.
Rationale: PEP 8 requires spaces, not tabs, before keywords.
Example::
True\\tand False # E274
True and False # OK
"""
code: ClassVar[str] = "E274"
message: ClassVar[str] = "tab before keyword"
uses_line_infos: ClassVar[bool] = True
[docs]
def check_line_info(
self,
lineno: int,
info: LineInfo,
ctx: FileContext,
) -> Iterator[Diagnostic]:
if not (info.style_flags & _TAB_AROUND_KW):
return
line = ctx.text_lines[lineno - 1]
comment_pos = info.comment_start
yield from self.check_line(line, lineno, ctx, comment_pos=comment_pos)
[docs]
def check_line(
self, line: str, lineno: int, ctx: FileContext, *, comment_pos: int = -1
) -> Iterator[Diagnostic]:
code = line[:comment_pos] if comment_pos >= 0 else line
for match in _E274_PATTERN.finditer(code):
col = match.start(1)
yield self.diagnostic_at(
lineno,
col,
fix=self._make_fix(lineno, match, ctx),
)
def _make_fix(self, lineno: int, match: re.Match[str], ctx: FileContext) -> Fix:
line_start = ctx.line_start_byte(lineno)
start = line_start + match.start(1)
end = line_start + match.end(1)
return Fix(description="Replace tab with space", edits=(Edit(start, end, " "),))
_E275_PATTERN = re.compile(
rb"\b(assert|del|elif|except|if|import|in|not|raise|return|while|yield|"
rb"for|with|from|as|class|def|async|await|lambda|global|nonlocal)\("
)
[docs]
class MissingWhitespaceAfterKeyword(Rule):
"""
E275: Missing whitespace after keyword.
Rationale: PEP 8 requires a space between keywords and opening
parentheses.
Uses regex + tree-sitter point check to avoid false positives in strings.
This is 17x faster than token-based checking.
Example::
if(x): # E275
if (x): # OK
x = "if(x)" # OK (no false positive)
"""
code: ClassVar[str] = "E275"
message: ClassVar[str] = "missing whitespace after keyword"
# Run once per module
node_types = {NodeType.MODULE}
[docs]
def check(self, node: Node) -> Iterator[Diagnostic]:
ctx = node.ctx
source = ctx.source
tree = ctx.tree
for match in _E275_PATTERN.finditer(source):
pos = match.start()
ts_node = tree.root_node.descendant_for_byte_range(pos, pos + 1)
if ts_node is None:
continue
if ts_node.type in ("string", "string_content", "comment"):
continue
parent = ts_node.parent
if parent and parent.type in ("string", "comment"):
continue
line = source[:pos].count(b"\n") + 1
line_start = source.rfind(b"\n", 0, pos) + 1
col = pos - line_start
# Insert position: just before the "("
insert_pos = match.end() - 1
yield Diagnostic(
code=self.code,
message=self.message,
location=Location(line=line, column=col),
severity=Severity.WARNING,
fix=Fix(
description="Add space after keyword",
edits=(Edit(insert_pos, insert_pos, " "),),
),
)
WHITESPACE_RULES = [
WhitespaceAfterOpenBracket,
WhitespaceBeforeCloseBracket,
WhitespaceBeforeColon,
WhitespaceBeforeParameters,
# E221-E224
MultipleSpacesBeforeOperator,
MultipleSpacesAfterOperator,
TabBeforeOperator,
TabAfterOperator,
# E225-E228
MissingWhitespaceAroundOperator,
MissingWhitespaceAroundArithmeticOperator,
MissingWhitespaceAroundBitwiseOperator,
MissingWhitespaceAroundModuloOperator,
MissingWhitespaceAfterComma,
# E241-E242
MultipleSpacesAfterComma,
TabAfterComma,
UnexpectedSpacesAroundKeywordEquals,
TwoSpacesBeforeInlineComment,
InlineCommentShouldStartWithSpace,
BlockCommentShouldStartWithSpace,
TooManyHashesForBlockComment,
# E271-E275
MultipleSpacesAfterKeyword,
MultipleSpacesBeforeKeyword,
TabAfterKeyword,
TabBeforeKeyword,
MissingWhitespaceAfterKeyword,
]
__all__ = [
"WHITESPACE_RULES",
"BlockCommentShouldStartWithSpace",
"InlineCommentShouldStartWithSpace",
"MissingWhitespaceAfterComma",
"MissingWhitespaceAfterKeyword",
"MissingWhitespaceAroundArithmeticOperator",
"MissingWhitespaceAroundBitwiseOperator",
"MissingWhitespaceAroundModuloOperator",
# E225-E228
"MissingWhitespaceAroundOperator",
# E241-E242
"MultipleSpacesAfterComma",
# E271-E275
"MultipleSpacesAfterKeyword",
"MultipleSpacesAfterOperator",
"MultipleSpacesBeforeKeyword",
# E221-E224
"MultipleSpacesBeforeOperator",
"TabAfterComma",
"TabAfterKeyword",
"TabAfterOperator",
"TabBeforeKeyword",
"TabBeforeOperator",
"TooManyHashesForBlockComment",
"TwoSpacesBeforeInlineComment",
"UnexpectedSpacesAroundKeywordEquals",
"WhitespaceAfterOpenBracket",
"WhitespaceBeforeCloseBracket",
"WhitespaceBeforeColon",
"WhitespaceBeforeParameters",
]