Source code for rude.rules.pyflakes.imports

"""
Import-related rules.

F402: ImportShadowedByLoopVar - import shadowed by loop variable
F403: ImportStarUsed - star import used
F404: LateFutureImport - from __future__ import not at beginning of file
F406: ImportStarNotPermitted - star import outside module level
F407: FutureFeatureNotDefined - undefined __future__ feature
F811: RedefinedWhileUnused - redefinition of unused name
"""

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

if TYPE_CHECKING:
    from rude.core.node import Node


# Valid __future__ features as of Python 3.13
FUTURE_FEATURES = frozenset(
    {
        "nested_scopes",
        "generators",
        "division",
        "absolute_import",
        "with_statement",
        "print_function",
        "unicode_literals",
        "barry_as_FLUFL",
        "generator_stop",
        "annotations",
    }
)


[docs] class LateFutureImport(Rule): """ F404: `from __future__` import not at the beginning of the file. Future imports must appear at the beginning of the file, after any module docstrings or comments, but before any other code. Example:: x = 1 from __future__ import annotations # F404 - too late! from __future__ import annotations x = 1 # OK """ code: ClassVar[str] = "F404" message: ClassVar[str] = "from __future__ imports must occur at the beginning of the file" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.IMPORT_FROM_STATEMENT}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: # Check if this is a __future__ import module = node.child_by_field("module") if not module or module.text != "__future__": return # Check if it's at module level if node.parent_type != "module": # Not at module level yield self.diagnostic(node) return # Check what comes before this import parent = node.parent if parent is None: return for sibling in parent.children: if sibling.raw.id == node.raw.id: # Reached our import, all OK break # Skip comments, docstrings, and other future imports if sibling.type == "comment": continue if sibling.type == "expression_statement": child = sibling.named_children[0] if sibling.named_children else None if child and child.type == "string": # Could be a docstring - only OK if it's the first statement continue if sibling.type == "import_from_statement": # Check if it's another future import sib_module = sibling.child_by_field("module") if sib_module and sib_module.text == "__future__": continue # Found a non-allowed statement before our future import yield self.diagnostic(node) return
[docs] class FutureFeatureNotDefined(Rule): """ F407: Undefined name in __future__ import. Example:: from __future__ import nonexistent_feature # F407 from __future__ import annotations # OK """ code: ClassVar[str] = "F407" message: ClassVar[str] = "future feature '{name}' is not defined" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.IMPORT_FROM_STATEMENT, NodeType.FUTURE_IMPORT_STATEMENT}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: # Handle future_import_statement specially if node.type == "future_import_statement": for child in node.named_children: if child.type == "dotted_name": # Get the actual identifier inside dotted_name for subchild in child.named_children: if subchild.type == "identifier": name = subchild.text if name not in FUTURE_FEATURES: yield self.diagnostic( subchild, self.message.format(name=name), ) return # Check if this is a __future__ import (import_from_statement) module = node.child_by_field("module") if not module or module.text != "__future__": return # Check each imported name for child in node.named_children: if child == module: continue imported_name: str | None = None name_node: Node | None = None if child.type == "dotted_name": imported_name = child.text name_node = child elif child.type == "identifier": # Single identifier import imported_name = child.text name_node = child elif child.type == "aliased_import": name_field = child.child_by_field("name") if name_field: imported_name = name_field.text name_node = name_field if imported_name and imported_name not in FUTURE_FEATURES: yield self.diagnostic( name_node or child, self.message.format(name=imported_name), )
def _get_module_name(node: Node) -> str: """Get the module name from an import_from_statement.""" for child in node.named_children: if child.type == "dotted_name": return child.text if child.type == "relative_import": for subchild in child.named_children: if subchild.type == "dotted_name": return subchild.text return "." return "module"
[docs] class ImportStarNotPermitted(Rule): """ F406: Star import (`from X import *`) used outside module level. Star imports are only allowed at the module level, not inside functions or classes. Example:: from os import * # OK - at module level def foo(): from os import * # F406 - not at module level """ code: ClassVar[str] = "F406" message: ClassVar[str] = "'from {module} import *' only allowed at module level" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.IMPORT_FROM_STATEMENT}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: # Check if this is a star import has_star = False for child in node.named_children: if child.type == "wildcard_import": has_star = True break if not has_star: return # Check if at module level if node.parent_type == "module": return # OK - at module level # Not at module level module_name = _get_module_name(node) yield self.diagnostic(node, self.message.format(module=module_name))
[docs] class ImportStarUsed(Rule): """ F403: `from X import *` used; unable to detect undefined names. Rationale: Star imports make it impossible to statically determine what names are defined, which can hide errors and cause name collisions. Example:: # Bad from os import * # F403 - star import used # Good from os import path, getcwd """ code: ClassVar[str] = "F403" message: ClassVar[str] = "'from {module} import *' used; unable to detect undefined names" severity: ClassVar[Severity] = Severity.WARNING node_types = {NodeType.IMPORT_FROM_STATEMENT}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: # Check if this is a star import has_star = False for child in node.named_children: if child.type == "wildcard_import": has_star = True break if not has_star: return # Only report at module level (F406 handles non-module level) if node.parent_type != "module": return module_name = _get_module_name(node) yield self.diagnostic(node, self.message.format(module=module_name))
[docs] class RedefinedWhileUnused(Rule): """ F811: Redefinition of unused name from line N. Rationale: Redefining a name before it is used usually indicates a copy-paste error or a redundant import. Example:: # Bad import os import os # F811 - redefinition of unused 'os' from line 1 # Good import os Optimized to use SemanticModel.redefinitions which tracks all binding redefinitions (assignment overwriting existing binding). Also uses model.bindings for import redefinitions. """ code: ClassVar[str] = "F811" message: ClassVar[str] = "redefinition of unused '{name}' from line {line}" severity: ClassVar[Severity] = Severity.WARNING node_types = {NodeType.MODULE} metadata_dependencies: ClassVar[set[type]] = {ScopeProvider}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: scope_prov: ScopeProvider = node.ctx.get_metadata(ScopeProvider) model = scope_prov.model # Check redefinitions tracked by Rust analyzer. # The Rust side only records redefinitions where the OLD binding is an # import (matching flake8/pyflakes behavior: F811 only fires when an # import is overwritten by another import, def, class, or assignment). for redef in model.redefinitions: if not model.has_use_between( redef.name, redef.scope_id, redef.old_line, redef.new_line ): yield Diagnostic( code=self.code, message=self.message.format(name=redef.name, line=redef.old_line), location=Location(line=redef.new_line, column=redef.new_column), severity=self.severity, )
[docs] class ImportShadowedByLoopVar(Rule): """ F402: Import shadowed by loop variable. Rationale: Reusing an imported name as a loop variable makes the import inaccessible after the loop, which is usually unintentional. Example:: # Bad from os import path for path in paths: # F402 - shadows import print(path) # Good from os import path for p in paths: print(p) Optimized: Uses pre-computed shadowed_imports from Rust SemanticModel. """ code: ClassVar[str] = "F402" message: ClassVar[str] = "import '{name}' from line {line} shadowed by loop variable" severity: ClassVar[Severity] = Severity.WARNING node_types = {NodeType.MODULE} metadata_dependencies: ClassVar[set[type]] = {ScopeProvider}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: scope_prov: ScopeProvider = node.ctx.get_metadata(ScopeProvider) model = scope_prov.model # Iterate over pre-computed shadowed imports for shadow in model.shadowed_imports: yield Diagnostic( code=self.code, message=self.message.format(name=shadow.name, line=shadow.import_line), location=Location(line=shadow.loop_line, column=shadow.loop_column), severity=self.severity, )
IMPORT_RULES = [ ImportShadowedByLoopVar, ImportStarUsed, LateFutureImport, FutureFeatureNotDefined, ImportStarNotPermitted, RedefinedWhileUnused, # ImportStarUsage (F405) is not included as it needs more infrastructure ] __all__ = [ "IMPORT_RULES", "FutureFeatureNotDefined", "ImportShadowedByLoopVar", "ImportStarNotPermitted", "ImportStarUsed", "LateFutureImport", "RedefinedWhileUnused", ]