Source code for rude.providers

"""
Metadata providers for advanced analysis.

Providers compute additional information about the AST that rules
can use for more sophisticated checks. They are lazy-computed and cached.

Available providers:
- ParentProvider: Track parent of each node
- ScopeProvider: Scope and binding analysis (Rust-based, high-performance)
- QualifiedNameProvider: Resolve names to their qualified form
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Self, cast

# Re-export semantic types
from rude.providers.semantic import (
    NO_SCOPE,
    Binding,
    Scope,
    ScopeId,
    ScopeType,
    SemanticModel,
)

if TYPE_CHECKING:
    from rude.core.node import Node
    from rude.core.types import FileContext


# ─────────────────────────────────────────────────────────────────────────────
# Parent Provider
# ─────────────────────────────────────────────────────────────────────────────


[docs] class ParentProvider: """Provides parent node lookup.""" __slots__ = ("_nodes", "_parents")
[docs] def __init__(self) -> None: self._parents: dict[int, int] = {} self._nodes: dict[int, Node] = {}
def compute(self, ctx: FileContext) -> Self: from rude.core.node import Node root = Node(ctx.tree.root_node, ctx) self._build(root, None) return self def _build(self, node: Node, parent: Node | None) -> None: self._nodes[node.raw.id] = node if parent: self._parents[node.raw.id] = parent.raw.id for child in node.children: self._build(child, node) def get(self, node: Node) -> Node | None: parent_id = self._parents.get(node.raw.id) if parent_id is None: return None return self._nodes.get(parent_id)
# ───────────────────────────────────────────────────────────────────────────── # Scope Provider # ─────────────────────────────────────────────────────────────────────────────
[docs] class ScopeProvider: """ Provides scope and binding analysis using Rust-based analyzer. Uses a single call to analyze_source() which parses and traverses the AST entirely in Rust for maximum performance. Usage:: sp = ctx.get_metadata(ScopeProvider) m = sp.model # Access module scope scope = m.scopes[m.module_scope] # Check unused bindings for name, bid in scope.bindings.items(): binding = m.bindings[bid] if not binding.references: print(f"Unused: {name}") """ __slots__ = ("model",) model: SemanticModel
[docs] def __init__(self) -> None: self.model = cast("SemanticModel", None) # uninitialized; set by compute() or from_model()
def compute(self, ctx: FileContext) -> Self: from rude.providers.semantic import analyze_source self.model = analyze_source(tree=ctx.tree) return self
[docs] @classmethod def from_model(cls, model: SemanticModel) -> Self: """Create a ScopeProvider with a pre-built SemanticModel.""" inst = cls.__new__(cls) inst.model = model return inst
# ───────────────────────────────────────────────────────────────────────────── # Qualified Name Provider # ─────────────────────────────────────────────────────────────────────────────
[docs] class QualifiedNameProvider: """Resolves names to their qualified form based on imports.""" __slots__ = ("_from_imports", "_imports")
[docs] def __init__(self) -> None: self._imports: dict[str, str] = {} self._from_imports: dict[str, str] = {}
def compute(self, ctx: FileContext) -> Self: from rude.core.node import Node root = Node(ctx.tree.root_node, ctx) self._collect_imports(root) return self def _collect_imports(self, node: Node) -> None: if node.type == "import_statement": for child in node.named_children: if child.type == "dotted_name": parts = child.text.split(".") self._imports[parts[0]] = parts[0] elif child.type == "aliased_import": name = child.child_by_field("name") alias = child.child_by_field("alias") if name: qname = name.text local = alias.text if alias else qname.split(".")[0] self._imports[local] = qname elif node.type == "import_from_statement": named_children = node.named_children module_node: Node | None = None module_name = "" for child in named_children: if child.type == "dotted_name": module_node = child module_name = child.text break for child in named_children: if module_node and child.raw.id == module_node.raw.id: continue if child.type == "dotted_name": self._from_imports[child.text] = ( f"{module_name}.{child.text}" if module_name else child.text ) elif child.type == "aliased_import": name = child.child_by_field("name") alias = child.child_by_field("alias") if name: qname = f"{module_name}.{name.text}" if module_name else name.text local = alias.text if alias else name.text self._from_imports[local] = qname for child in node.children: self._collect_imports(child) def resolve(self, node: Node) -> str | None: if node.is_call: func = node.child_by_field("function") if not func: return None return self._resolve_expr(func) elif node.is_identifier: return self._resolve_name(node.text) elif node.is_attribute: return self._resolve_expr(node) return None def _resolve_name(self, name: str) -> str: if name in self._from_imports: return self._from_imports[name] if name in self._imports: return self._imports[name] return name def _resolve_expr(self, node: Node) -> str: if node.is_identifier: return self._resolve_name(node.text) elif node.is_attribute: obj = node.child_by_field("object") attr = node.child_by_field("attribute") if obj and attr: obj_resolved = self._resolve_expr(obj) return f"{obj_resolved}.{attr.text}" return node.text
__all__ = [ "NO_SCOPE", "Binding", # Providers "ParentProvider", "QualifiedNameProvider", "Scope", "ScopeId", "ScopeProvider", # Semantic types "ScopeType", ]