Source code for rude.rules.pyflakes.names

"""
Name-related rules requiring scope analysis.

F821: UndefinedName - use of name that is not defined
F822: UndefinedExport - name in __all__ not defined in module
F823: UndefinedLocal - local variable used before assignment
"""

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, Severity
from rude.providers import NO_SCOPE, ScopeProvider

if TYPE_CHECKING:
    from rude.core.node import Node


# Python built-in names that are always available
BUILTINS = frozenset(
    {
        # Built-in functions
        "abs",
        "aiter",
        "all",
        "anext",
        "any",
        "ascii",
        "bin",
        "bool",
        "breakpoint",
        "bytearray",
        "bytes",
        "callable",
        "chr",
        "classmethod",
        "compile",
        "complex",
        "delattr",
        "dict",
        "dir",
        "divmod",
        "enumerate",
        "eval",
        "exec",
        "filter",
        "float",
        "format",
        "frozenset",
        "getattr",
        "globals",
        "hasattr",
        "hash",
        "help",
        "hex",
        "id",
        "input",
        "int",
        "isinstance",
        "issubclass",
        "iter",
        "len",
        "list",
        "locals",
        "map",
        "max",
        "memoryview",
        "min",
        "next",
        "object",
        "oct",
        "open",
        "ord",
        "pow",
        "print",
        "property",
        "range",
        "repr",
        "reversed",
        "round",
        "set",
        "setattr",
        "slice",
        "sorted",
        "staticmethod",
        "str",
        "sum",
        "super",
        "tuple",
        "type",
        "vars",
        "zip",
        # Built-in exceptions
        "BaseException",
        "BaseExceptionGroup",
        "Exception",
        "ExceptionGroup",
        "ArithmeticError",
        "AssertionError",
        "AttributeError",
        "BlockingIOError",
        "BrokenPipeError",
        "BufferError",
        "BytesWarning",
        "ChildProcessError",
        "ConnectionAbortedError",
        "ConnectionError",
        "ConnectionRefusedError",
        "ConnectionResetError",
        "DeprecationWarning",
        "EOFError",
        "EnvironmentError",
        "FileExistsError",
        "FileNotFoundError",
        "FloatingPointError",
        "FutureWarning",
        "GeneratorExit",
        "IOError",
        "ImportError",
        "ImportWarning",
        "IndentationError",
        "IndexError",
        "InterruptedError",
        "IsADirectoryError",
        "KeyError",
        "KeyboardInterrupt",
        "LookupError",
        "MemoryError",
        "ModuleNotFoundError",
        "NameError",
        "NotADirectoryError",
        "NotImplemented",
        "NotImplementedError",
        "OSError",
        "OverflowError",
        "PendingDeprecationWarning",
        "PermissionError",
        "ProcessLookupError",
        "RecursionError",
        "ReferenceError",
        "ResourceWarning",
        "RuntimeError",
        "RuntimeWarning",
        "StopAsyncIteration",
        "StopIteration",
        "SyntaxError",
        "SyntaxWarning",
        "SystemError",
        "SystemExit",
        "TabError",
        "TimeoutError",
        "TypeError",
        "UnboundLocalError",
        "UnicodeDecodeError",
        "UnicodeEncodeError",
        "UnicodeError",
        "UnicodeTranslateError",
        "UnicodeWarning",
        "UserWarning",
        "ValueError",
        "Warning",
        "ZeroDivisionError",
        # Built-in constants
        "True",
        "False",
        "None",
        "Ellipsis",
        "__debug__",
        # Special names
        "__name__",
        "__doc__",
        "__package__",
        "__loader__",
        "__spec__",
        "__file__",
        "__cached__",
        "__builtins__",
        "__annotations__",
        "__import__",
        # Site-specific built-ins (added by site module)
        "exit",
        "quit",
        "copyright",
        "credits",
        "license",
    }
)


[docs] class UndefinedName(Rule): """ F821: Name is not defined. Rationale: Using an undefined name causes a ``NameError`` at runtime. Example:: # Bad print(undefined_variable) # F821 # Good x = 1 print(x) Optimized to use pre-computed unresolved names from ScopeProvider. """ code: ClassVar[str] = "F821" message: ClassVar[str] = "undefined name '{name}'" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.MODULE} metadata_dependencies: ClassVar[set[type]] = {ScopeProvider}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: from rude.core.types import Location scope_prov: ScopeProvider = node.ctx.get_metadata(ScopeProvider) model = scope_prov.model if model.module_scope == NO_SCOPE: return module_scope = model.scope(model.module_scope) # Iterate over pre-computed unresolved names # Note: Attribute accesses (obj.attr) are already filtered by ScopeProvider for uref in model.unresolved: # Skip builtins if uref.name in BUILTINS: continue # Skip private/dunder names (often dynamically set) if uref.name.startswith("_"): continue # Check if declared as global/nonlocal in containing scope scope = model.scope(uref.scope_id) if uref.name in scope.globals or uref.name in scope.nonlocals: continue # Skip if defined at module level (forward reference) # This handles cases like: def foo(): use_bar() ... def bar(): pass # But check if it's an exception handler that's no longer valid if uref.name in module_scope.bindings: binding = model.binding(module_scope.bindings[uref.name]) # Exception handler variables are deleted after the except block if ( binding.valid_until_byte is not None and uref.start_byte >= binding.valid_until_byte ): yield Diagnostic( code=self.code, message=self.message.format(name=uref.name), location=Location(line=uref.line, column=uref.column), severity=self.severity, ) continue yield Diagnostic( code=self.code, message=self.message.format(name=uref.name), location=Location(line=uref.line, column=uref.column), severity=self.severity, )
[docs] class UndefinedExport(Rule): """ F822: Name in __all__ is not defined. Rationale: Listing an undefined name in ``__all__`` causes an ``AttributeError`` when the module is imported with ``*``. Example:: # Bad __all__ = ["foo", "bar"] # F822 - bar is not defined def foo(): pass # Good __all__ = ["foo"] def foo(): pass """ code: ClassVar[str] = "F822" message: ClassVar[str] = "undefined name '{name}' in __all__" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.ASSIGNMENT} metadata_dependencies: ClassVar[set[type]] = {ScopeProvider}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: # Check if this is an assignment to __all__ left = node.child_by_field("left") if not left or not left.is_identifier or left.text != "__all__": return right = node.child_by_field("right") if not right: return # Get scope provider scope_prov: ScopeProvider = node.ctx.get_metadata(ScopeProvider) model = scope_prov.model if model.module_scope == NO_SCOPE: return module_scope = model.scope(model.module_scope) # Extract names from __all__ names = self._extract_all_names(right) for name, name_node in names: # Check if name is defined in module scope if name not in module_scope.bindings and name not in BUILTINS: # Also check builtins yield self.diagnostic( name_node, self.message.format(name=name), )
def _extract_all_names(self, node: Node) -> Iterator[tuple[str, Node]]: """Extract string names from __all__ list/tuple.""" if node.type in ("list", "tuple"): for child in node.named_children: if child.type == "string": # Remove quotes text = child.text if len(text) >= 2: # Handle various quote styles if text.startswith('"""') or text.startswith("'''"): name = text[3:-3] elif text.startswith('"') or text.startswith("'"): name = text[1:-1] else: continue yield (name, child)
[docs] class UndefinedLocal(Rule): """ F823: Local variable referenced before assignment. Rationale: Using a local variable before it is assigned causes an ``UnboundLocalError`` at runtime. Example:: # Bad def foo(): print(x) # F823 - x used before assignment x = 1 # Good def foo(): x = 1 print(x) Optimized: Uses pre-computed undefined_locals from Rust SemanticModel. """ code: ClassVar[str] = "F823" message: ClassVar[str] = "local variable '{name}' referenced before assignment" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.MODULE} metadata_dependencies: ClassVar[set[type]] = {ScopeProvider}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: """Check for uses before definitions using pre-computed list from Rust.""" from rude.core.types import Location scope_prov: ScopeProvider = node.ctx.get_metadata(ScopeProvider) model = scope_prov.model # Iterate over pre-computed undefined locals for entry in model.undefined_locals: yield Diagnostic( code=self.code, message=self.message.format(name=entry.name), location=Location(line=entry.line, column=entry.column), severity=self.severity, )
NAME_RULES = [ UndefinedName, UndefinedExport, UndefinedLocal, ] __all__ = [ "NAME_RULES", "UndefinedExport", "UndefinedLocal", "UndefinedName", ]