Source code for rude.rules.pyflakes.control_flow

"""
Control flow rules: statements in wrong context.

F701: BreakOutsideLoop
F702: ContinueOutsideLoop
F704: YieldOutsideFunction
F706: ReturnOutsideFunction
F707: DefaultExceptNotLast

F701-F706 use the ancestor context map from the Rust SemanticModel
when available, falling back to parent-walking for uncovered nodes.
"""

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 ScopeProvider
from rude.providers.semantic import CTX_IN_FUNCTION, CTX_IN_LAMBDA

if TYPE_CHECKING:
    from rude._rust import SemanticModel
    from rude.core.node import Node


def _get_model(node: Node) -> SemanticModel | None:
    """Try to get SemanticModel from context, returns None if unavailable."""
    try:
        sp: ScopeProvider = node.ctx.get_metadata(ScopeProvider)
        return sp.model
    except Exception:
        return None


[docs] class ReturnOutsideFunction(Rule): """ F706: `return` statement outside of a function. Rationale: A ``return`` at module level is a ``SyntaxError``. Example:: # Bad return 1 # F706 - not inside a function # Good def foo(): return 1 """ code: ClassVar[str] = "F706" message: ClassVar[str] = "'return' outside function" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.RETURN_STATEMENT} metadata_dependencies: ClassVar[set[type]] = {ScopeProvider}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: model = _get_model(node) if model is not None: ctx = model.node_context(node.start_byte) if ctx is not None: flags, _, _ = ctx if not (flags & CTX_IN_FUNCTION): yield self.diagnostic(node) return # Fallback: parent-walking if not self._has_function_ancestor(node): yield self.diagnostic(node)
def _has_function_ancestor(self, node: Node) -> bool: current = node.parent while current: if current.type in ("function_definition", "lambda"): return True current = current.parent return False
[docs] class YieldOutsideFunction(Rule): """ F704: `yield` or `yield from` outside of a function. Rationale: A ``yield`` at module level is a ``SyntaxError``. Example:: # Bad yield 1 # F704 - not inside a function # Good def gen(): yield 1 """ code: ClassVar[str] = "F704" message: ClassVar[str] = "'{keyword}' outside function" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.YIELD} metadata_dependencies: ClassVar[set[type]] = {ScopeProvider}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: model = _get_model(node) if model is not None: ctx = model.node_context(node.start_byte) if ctx is not None: flags, _, _ = ctx # yield is valid in function but NOT in lambda-only context in_function = bool(flags & CTX_IN_FUNCTION) in_lambda = bool(flags & CTX_IN_LAMBDA) if not in_function or in_lambda: keyword = "yield from" if "from" in node.text else "yield" yield self.diagnostic(node, self.message.format(keyword=keyword)) return # Fallback: parent-walking if not self._has_function_ancestor(node): keyword = "yield from" if "from" in node.text else "yield" yield self.diagnostic(node, self.message.format(keyword=keyword))
def _has_function_ancestor(self, node: Node) -> bool: current = node.parent while current: if current.type == "function_definition": return True current = current.parent return False
def _has_loop_ancestor(node: Node) -> bool: """Check if node is inside a loop, stopping at function boundaries.""" current = node.parent while current: if current.type in ("for_statement", "while_statement"): return True if current.type in ("function_definition", "lambda"): return False current = current.parent return False
[docs] class ContinueOutsideLoop(Rule): """ F702: `continue` not properly in loop. Rationale: ``continue`` outside a loop is a ``SyntaxError``. Example:: # Bad continue # F702 - not inside a loop # Good for i in range(10): continue """ code: ClassVar[str] = "F702" message: ClassVar[str] = "'continue' not properly in loop" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.CONTINUE_STATEMENT} metadata_dependencies: ClassVar[set[type]] = {ScopeProvider}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: model = _get_model(node) if model is not None: if not model.is_in_loop(node.start_byte): yield self.diagnostic(node) return # Fallback: parent-walking if not _has_loop_ancestor(node): yield self.diagnostic(node)
[docs] class BreakOutsideLoop(Rule): """ F701: `break` not properly in loop. Rationale: ``break`` outside a loop is a ``SyntaxError``. Example:: # Bad break # F701 - not inside a loop # Good while True: break """ code: ClassVar[str] = "F701" message: ClassVar[str] = "'break' not properly in loop" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.BREAK_STATEMENT} metadata_dependencies: ClassVar[set[type]] = {ScopeProvider}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: model = _get_model(node) if model is not None: if not model.is_in_loop(node.start_byte): yield self.diagnostic(node) return # Fallback: parent-walking if not _has_loop_ancestor(node): yield self.diagnostic(node)
[docs] class DefaultExceptNotLast(Rule): """ F707: A bare `except:` clause must be the last exception handler. Rationale: A bare ``except:`` before a typed handler is a ``SyntaxError`` because the typed handler would be unreachable. Example:: # Bad try: pass except: # F707 - bare except must be last pass except TypeError: pass # Good try: pass except TypeError: pass except: pass """ code: ClassVar[str] = "F707" message: ClassVar[str] = "an except clause without an exception type must be last" severity: ClassVar[Severity] = Severity.ERROR node_types = {NodeType.TRY_STATEMENT}
[docs] def check(self, node: Node) -> Iterator[Diagnostic]: except_clauses = [c for c in node.named_children if c.type == "except_clause"] # Find bare except clauses and check their positions for i, clause in enumerate(except_clauses): if self._is_bare_except(clause) and i < len(except_clauses) - 1: # If it's not the last one, it's an error yield self.diagnostic(clause)
def _is_bare_except(self, clause: Node) -> bool: """Check if except clause has no exception type.""" # A bare except has no children between 'except' and ':' # Structure: except_clause -> "except" ":" block # vs: except_clause -> "except" identifier ":" block return all(child.type == "block" for child in clause.named_children)
CONTROL_FLOW_RULES = [ ReturnOutsideFunction, YieldOutsideFunction, ContinueOutsideLoop, BreakOutsideLoop, DefaultExceptNotLast, ] __all__ = [ "CONTROL_FLOW_RULES", "BreakOutsideLoop", "ContinueOutsideLoop", "DefaultExceptNotLast", "ReturnOutsideFunction", "YieldOutsideFunction", ]