Source code for rude.rules.pyflakes.syntax

"""
Syntax rules: invalid or suspicious syntax patterns.

F601: MultiValueRepeatedKeyLiteral - duplicate literal key in dict
F602: MultiValueRepeatedKeyVariable - duplicate variable key in dict
F621: TooManyExpressionsInStarredAssignment
F622: TwoStarredExpressions
F631: AssertTuple - assert with non-empty tuple (always truthy)
F632: IsLiteral - use of `is` with a literal
F633: InvalidPrintSyntax - use of >> with print (Python 2 syntax)
F634: IfTuple - if with non-empty tuple condition (always truthy)
F831: DuplicateArgument - duplicate argument in function definition
F901: RaiseNotImplemented - raise NotImplemented instead of NotImplementedError
"""

from __future__ import annotations

from collections.abc import Callable, 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, Severity

if TYPE_CHECKING:
    from rude.core.node import Node


[docs] class TooManyExpressionsInStarredAssignment(Rule): """ F621: Too many expressions in starred assignment target. Rationale: Python limits unpacking targets to 255 elements. This is a compile-time error caught statically. Example:: # Bad a, *b, c, d, e, f, g, ... = items # Too many targets # Good first, *rest = items """ code: ClassVar[str] = "F621" message: ClassVar[str] = "too many expressions in star-unpacking assignment" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.ASSIGNMENT}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: left = node.child_by_field("left") if not left: return # Count starred expressions and total elements starred_count = 0 total_count = 0 for name in self._extract_targets(left): total_count += 1 if name.type == "list_splat_pattern": starred_count += 1 # Python limit is 256 for tuple unpacking if starred_count > 0 and total_count > 256: yield self.diagnostic(left)
def _extract_targets(self, node: Node) -> Iterator[Node]: """Extract all targets from unpacking pattern.""" if node.type in ("identifier", "list_splat_pattern"): yield node elif node.type in ("tuple", "list", "tuple_pattern", "list_pattern", "pattern_list"): for child in node.named_children: yield from self._extract_targets(child)
[docs] class TwoStarredExpressions(Rule): """ F622: Two or more starred expressions in assignment. Rationale: Python only allows one starred expression per assignment target. This is a ``SyntaxError``. Example:: # Bad *a, *b = [1, 2, 3] # F622 - two starred expressions # Good a, *b = [1, 2, 3] """ code: ClassVar[str] = "F622" message: ClassVar[str] = "two or more starred expressions in assignment" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.ASSIGNMENT}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: left = node.child_by_field("left") if not left: return starred_nodes = list(self._find_starred(left)) if len(starred_nodes) >= 2: yield self.diagnostic(left)
def _find_starred(self, node: Node) -> Iterator[Node]: """Find starred patterns in unpacking target.""" if node.type == "list_splat_pattern": yield node elif node.type in ("tuple", "list", "tuple_pattern", "list_pattern", "pattern_list"): for child in node.named_children: yield from self._find_starred(child)
def _is_nonempty_tuple(node: Node) -> bool: """Check if node is a non-empty tuple (always truthy).""" if node.type != "tuple": return False elements = [c for c in node.named_children if c.type not in ("(", ")", ",")] return len(elements) > 0
[docs] class AssertTuple(Rule): """ F631: Assert test is a non-empty tuple, which is always True. Rationale: A tuple like ``(x, y)`` is always truthy, so the assertion never fails. This is usually a misplaced comma. Example:: # Bad assert (x, y) # F631 - tuple is always truthy # Good assert x and y """ code: ClassVar[str] = "F631" message: ClassVar[str] = "assertion test is a non-empty tuple, which is always True" severity: ClassVar[Severity] = Severity.WARNING node_types = {NodeType.ASSERT_STATEMENT}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: # assert_statement children: "assert" expression [expression] # First expression is the condition children = node.named_children if not children: return condition = children[0] if _is_nonempty_tuple(condition): yield self.diagnostic(condition)
[docs] class IfTuple(Rule): """ F634: If test is a non-empty tuple, which is always True. Rationale: A non-empty tuple is always truthy, so the branch always executes. This is usually a misplaced comma. Example:: # Bad if (x, y): # F634 - tuple is always truthy pass # Good if x and y: pass """ code: ClassVar[str] = "F634" message: ClassVar[str] = "if test is a non-empty tuple, which is always True" severity: ClassVar[Severity] = Severity.WARNING node_types = {NodeType.IF_STATEMENT}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: condition = node.child_by_field("condition") if condition and _is_nonempty_tuple(condition): yield self.diagnostic(condition) # Also check elif clauses for child in node.named_children: if child.type == "elif_clause": elif_cond = child.child_by_field("condition") if elif_cond and _is_nonempty_tuple(elif_cond): yield self.diagnostic(elif_cond)
[docs] class IsLiteral(Rule): """ F632: Use of `is` or `is not` with a literal. Using `is` with literals can have surprising behavior due to Python's interning. Use `==` instead. Example:: x is 1 # F632 - use `==` instead x is "foo" # F632 x is None # OK - None is a singleton x is True # OK - True/False are singletons """ code: ClassVar[str] = "F632" message: ClassVar[str] = "use ==/!= to compare '{literal}', not is/is not" severity: ClassVar[Severity] = Severity.WARNING node_types = {NodeType.COMPARISON_OPERATOR} # Literals that are NOT safe to compare with `is` LITERAL_TYPES = {"integer", "float", "string", "concatenated_string"}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: children = node.children i = 0 while i < len(children): child = children[i] # Determine if this is an "is" or "is not" operator if child.text == "is not": operator_text = "is not" replacement = "!=" op_start = child.start_byte op_end = child.end_byte elif child.text == "is": # Check if next token is "not" (separate tokens) if i + 1 < len(children) and children[i + 1].text == "not": operator_text = "is not" replacement = "!=" op_start = child.start_byte op_end = children[i + 1].end_byte else: operator_text = "is" replacement = "==" op_start = child.start_byte op_end = child.end_byte else: i += 1 continue fix = Fix( description=f"Replace {operator_text!r} with {replacement!r}", edits=(Edit(op_start, op_end, replacement),), ) # Check operand before if i > 0: left = children[i - 1] if left.type in self.LITERAL_TYPES: yield self.diagnostic( left, self.message.format(literal=self._truncate(left.text)), fix=fix, ) # Check operand after (skip past "not" if separate) right_idx = i + 1 if operator_text == "is not" and child.text == "is": right_idx = i + 2 # Skip the separate "not" token if right_idx < len(children): right = children[right_idx] if right.type in self.LITERAL_TYPES: yield self.diagnostic( right, self.message.format(literal=self._truncate(right.text)), fix=fix, ) i += 1
def _truncate(self, text: str, max_len: int = 20) -> str: if len(text) > max_len: return text[:max_len] + "..." return text
[docs] class RaiseNotImplemented(Rule): """ F901: `raise NotImplemented` should be `raise NotImplementedError`. NotImplemented is a special value used for binary operations, not an exception. Example:: raise NotImplemented # F901 - wrong! raise NotImplementedError() # OK raise NotImplementedError # OK """ code: ClassVar[str] = "F901" message: ClassVar[str] = "raise NotImplemented should be raise NotImplementedError" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.RAISE_STATEMENT}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: # Check what is being raised for child in node.named_children: if child.is_identifier and child.text == "NotImplemented": yield self.diagnostic( child, fix=Fix.replace(child, "NotImplementedError"), ) elif child.is_call: func = child.child_by_field("function") if func and func.is_identifier and func.text == "NotImplemented": yield self.diagnostic( func, fix=Fix.replace(func, "NotImplementedError"), )
def _find_repeated_keys( node: Node, key_predicate: Callable[[Node], bool], ) -> Iterator[tuple[str, Node]]: """Yield (key_text, key_node) for repeated dict keys with different values.""" key_occurrences: dict[str, list[tuple[str, Node]]] = {} for child in node.named_children: if child.type == "pair": key_node = child.child_by_field("key") value_node = child.child_by_field("value") if key_node and key_predicate(key_node): key_text = key_node.text value_text = value_node.text if value_node else "" key_occurrences.setdefault(key_text, []).append((value_text, key_node)) for key_text, occurrences in key_occurrences.items(): if len(occurrences) > 1: values = {v for v, _ in occurrences} if len(values) > 1: for _, key_node in occurrences: yield key_text, key_node
[docs] class MultiValueRepeatedKeyLiteral(Rule): """ F601: Dictionary literal contains repeated key (literal). Pyflakes reports ALL occurrences of a key when: - The key appears more than once - The values are different Example:: {"a": 1, "a": 2} # F601 - both "a" keys reported {"a": 1, "a": 1} # OK - same value, no report """ code: ClassVar[str] = "F601" message: ClassVar[str] = "dictionary key {key!r} repeated" severity: ClassVar[Severity] = Severity.WARNING node_types = {NodeType.DICTIONARY} _LITERAL_TYPES = frozenset({"string", "integer", "float", "true", "false", "none"})
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: for key_text, key_node in _find_repeated_keys( node, lambda k: k.type in self._LITERAL_TYPES ): yield self.diagnostic(key_node, self.message.format(key=key_text))
[docs] class MultiValueRepeatedKeyVariable(Rule): """ F602: Dictionary literal contains repeated key (variable). Pyflakes reports ALL occurrences of a variable key when: - The variable key appears more than once - The values are different Example:: x = "key" {x: 1, x: 2} # F602 - both x keys reported {x: 1, x: 1} # OK - same value, no report """ code: ClassVar[str] = "F602" message: ClassVar[str] = "dictionary key variable {key!r} repeated" severity: ClassVar[Severity] = Severity.WARNING node_types = {NodeType.DICTIONARY}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: for key_text, key_node in _find_repeated_keys(node, lambda k: k.is_identifier): yield self.diagnostic(key_node, self.message.format(key=key_text))
[docs] class InvalidPrintSyntax(Rule): """ F633: Use of >> is invalid with print function. In Python 2, `print >> file, data` was used to redirect print output. This syntax is invalid in Python 3. Example:: print >> sys.stderr, "error" # F633 - invalid syntax print("error", file=sys.stderr) # OK - Python 3 syntax """ code: ClassVar[str] = "F633" message: ClassVar[str] = "use of >> is invalid with print function" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.PRINT_STATEMENT}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: # tree-sitter parses `print >> file` as a print_statement with chevron child for child in node.named_children: if child.type == "chevron": yield self.diagnostic(node) return
[docs] class DuplicateArgument(Rule): """ F831: Duplicate argument in function definition. Rationale: Duplicate parameter names cause a ``SyntaxError`` in Python 3. Example:: # Bad def foo(a, a): # F831 - duplicate argument 'a' pass # Good def foo(a, b): pass """ code: ClassVar[str] = "F831" message: ClassVar[str] = "duplicate argument '{name}' in function definition" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.FUNCTION_DEFINITION, NodeType.LAMBDA}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: params_node = node.child_by_field("parameters") if not params_node: return seen_names: dict[str, Node] = {} for param in params_node.named_children: name = self._extract_param_name(param) if name: if name in seen_names: yield self.diagnostic( param, self.message.format(name=name), ) else: seen_names[name] = param
def _extract_param_name(self, param: Node) -> str | None: """Extract parameter name from various parameter node types.""" if param.is_identifier: return param.text if param.type in ("typed_parameter", "default_parameter", "typed_default_parameter"): name = param.child_by_field("name") return name.text if name else None if param.type in ("list_splat_pattern", "dictionary_splat_pattern"): for child in param.named_children: if child.is_identifier: return child.text return None
SYNTAX_RULES = [ TooManyExpressionsInStarredAssignment, TwoStarredExpressions, AssertTuple, IfTuple, IsLiteral, InvalidPrintSyntax, RaiseNotImplemented, MultiValueRepeatedKeyLiteral, MultiValueRepeatedKeyVariable, DuplicateArgument, ] __all__ = [ "SYNTAX_RULES", "AssertTuple", "DuplicateArgument", "IfTuple", "InvalidPrintSyntax", "IsLiteral", "MultiValueRepeatedKeyLiteral", "MultiValueRepeatedKeyVariable", "RaiseNotImplemented", "TooManyExpressionsInStarredAssignment", "TwoStarredExpressions", ]