Diffcontext / diffcontext /symbols.py
trakshan-mishra
Deploy FastAPI & MCP server over SSE
036a2db
Raw
History Blame Contribute Delete
8.9 kB
"""
symbols.py — Attribute ownership extraction for resolving self.attr.method() chains.
Given a class with `self.router = APIRouter()`, this module figures out that
`self.router` has type `APIRouter`, enabling cross-file method resolution.
"""
import ast
from typing import Dict, Optional, Tuple, List
def _iter_statements(body):
"""Yield statements in source order, recursing into if/for/while/with/try."""
for stmt in body:
yield stmt
if isinstance(stmt, (ast.If, ast.For, ast.AsyncFor, ast.While)):
yield from _iter_statements(stmt.body)
yield from _iter_statements(stmt.orelse)
elif isinstance(stmt, (ast.With, ast.AsyncWith)):
yield from _iter_statements(stmt.body)
elif isinstance(stmt, ast.Try):
yield from _iter_statements(stmt.body)
for handler in stmt.handlers:
yield from _iter_statements(handler.body)
yield from _iter_statements(stmt.orelse)
yield from _iter_statements(stmt.finalbody)
def _extract_annotation_name(annotation) -> Optional[Tuple[Optional[str], str]]:
"""
Returns (qualifier, bare_name) or None.
Router -> (None, "Router")
Optional[Router]-> (None, "Router") <- unwrap subscript
routing.Router -> ("routing", "Router")
"""
if isinstance(annotation, ast.Name):
return (None, annotation.id)
if isinstance(annotation, ast.Attribute) and isinstance(annotation.value, ast.Name):
return (annotation.value.id, annotation.attr)
# Optional[Router], List[Router], etc. — unwrap one level
if isinstance(annotation, ast.Subscript):
return _extract_annotation_name(annotation.slice)
return None
def _extract_call_owner(value) -> Optional[Tuple[Optional[str], str]]:
"""
Returns (qualifier, bare_name) or None.
APIRouter() -> (None, "APIRouter")
routing.APIRouter() -> ("routing", "APIRouter")
make_helper() -> (None, "make_helper")
"""
if not isinstance(value, ast.Call):
return None
func = value.func
if isinstance(func, ast.Name):
return (None, func.id)
if isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
return (func.value.id, func.attr)
if isinstance(func, ast.Attribute):
return (None, func.attr)
return None
def _extract_assign_owner(value, local_var_types=None, param_types=None):
"""
Handles non-Call RHS cases that still indicate a type:
self.x = router or Router() -> BoolOp -> try each operand
self.x = router -> bare Name -> no type info (return None)
"""
if local_var_types is None:
local_var_types = {}
if param_types is None:
param_types = {}
if isinstance(value, ast.Call):
return _extract_call_owner(value)
if isinstance(value, ast.Name):
if value.id in param_types:
return param_types[value.id]
if value.id in local_var_types:
return local_var_types[value.id]
return None
if isinstance(value, ast.BoolOp):
call_result = None
name_result = None
for operand in value.values:
ref = _extract_assign_owner(operand, local_var_types, param_types)
if ref is None:
continue
if isinstance(operand, ast.Call):
call_result = call_result or ref
else:
name_result = name_result or ref
return call_result or name_result
return None
def extract_local_var_types(
fn_node,
param_types: Optional[Dict[str, Tuple[Optional[str], str]]] = None,
) -> Dict[str, Tuple[Optional[str], str]]:
"""
Track local variable -> type assignments within a SINGLE function body,
free function or method alike:
h = Helper() -> {"h": (None, "Helper")}
r: routing.Router = ... -> {"r": ("routing", "Router")}
x = make_helper() -> chases factory functions the same way
attribute ownership tracking does
(resolution of the factory return type
itself happens later, in graph_builder)
This is the free-function counterpart to the self.attr tracking that
extract_attribute_ownerships does for class bodies. It exists because
a huge fraction of real code instantiates a class in a local variable
inside a plain function (not as a self.attr) and immediately calls a
method on it -- e.g.:
def run():
h = Handler()
return h.process()
Without this, `h.process()` can never resolve: there's nowhere that
records "h" has type "Handler". extract_attribute_ownerships alone
can't help, because it only ever looks inside ast.ClassDef bodies.
Returns: {local_var_name: (qualifier, bare_type_name)}
"""
if param_types is None:
param_types = {}
local_var_types: Dict[str, Tuple[Optional[str], str]] = {}
for stmt in _iter_statements(fn_node.body):
if isinstance(stmt, ast.AnnAssign):
target = stmt.target
if isinstance(target, ast.Name):
ref = _extract_annotation_name(stmt.annotation)
if ref:
local_var_types[target.id] = ref
continue
if isinstance(stmt, ast.Assign):
for target in stmt.targets:
if isinstance(target, ast.Name):
ref = _extract_assign_owner(
stmt.value, local_var_types, param_types
)
if ref:
local_var_types[target.id] = ref
continue
return local_var_types
def extract_param_types(fn_node) -> Dict[str, Tuple[Optional[str], str]]:
"""
Map annotated parameter names to their (qualifier, bare_name) type, e.g.
def f(router: routing.Router, name: str): ...
-> {"router": ("routing", "Router")} (unannotated/non-type params skipped)
"""
param_types: Dict[str, Tuple[Optional[str], str]] = {}
for arg in fn_node.args.args:
if arg.annotation is not None:
ref = _extract_annotation_name(arg.annotation)
if ref:
param_types[arg.arg] = ref
return param_types
def extract_attribute_ownerships(tree) -> Dict[str, Tuple[Optional[str], str]]:
"""
Extracts mappings like:
self.router: routing.Router = routing.Router()
self.router = Router()
self.router2 = router # typed constructor param
h = Helper(); self.helper = h # local var tracking
self.router3 = router or Router()
self.opt_router: Optional[Router] = None
Returns: {"FastAPI.router": (qualifier, type_name), ...}
"""
ownerships: Dict[str, Tuple[Optional[str], str]] = {}
for node in tree.body:
if not isinstance(node, ast.ClassDef):
continue
class_name = node.name
for item in node.body:
if not isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
param_types = extract_param_types(item)
local_var_types = {}
for stmt in _iter_statements(item.body):
if isinstance(stmt, ast.AnnAssign):
target = stmt.target
if (
isinstance(target, ast.Attribute)
and isinstance(target.value, ast.Name)
and target.value.id == "self"
):
ref = _extract_annotation_name(stmt.annotation)
if ref:
ownerships[f"{class_name}.{target.attr}"] = ref
continue
if isinstance(stmt, ast.Assign):
for target in stmt.targets:
if (
isinstance(target, ast.Attribute)
and isinstance(target.value, ast.Name)
and target.value.id == "self"
):
ref = _extract_assign_owner(
stmt.value, local_var_types, param_types
)
if ref:
ownerships[f"{class_name}.{target.attr}"] = ref
elif isinstance(target, ast.Name):
ref = _extract_assign_owner(
stmt.value, local_var_types, param_types
)
if ref:
local_var_types[target.id] = ref
continue
return ownerships