mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-01 05:27:02 +08:00
1054 lines
40 KiB
Python
1054 lines
40 KiB
Python
"""收集宿主直接网络传输、网络 SDK 与底层协议操作事实。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import ast
|
||
import hashlib
|
||
import json
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
from typing import Iterable
|
||
|
||
RAW_TRANSPORT_ROOTS = (
|
||
"requests",
|
||
"httpx",
|
||
"httpx2",
|
||
"aiohttp",
|
||
"urllib.request",
|
||
"urllib3",
|
||
"http.client",
|
||
"websocket",
|
||
"websockets",
|
||
"curl_cffi",
|
||
"cloudscraper",
|
||
"httplib2",
|
||
"pycurl",
|
||
)
|
||
NETWORK_SDK_ROOTS = (
|
||
"boto3",
|
||
"botocore",
|
||
"cloakbrowser",
|
||
"ddgs",
|
||
"discord",
|
||
"docker",
|
||
"google.genai",
|
||
"langchain_anthropic",
|
||
"langchain_aws",
|
||
"langchain_deepseek",
|
||
"langchain_google_genai",
|
||
"langchain_openai",
|
||
"lark_oapi",
|
||
"openai",
|
||
"oss2",
|
||
"plexapi",
|
||
"pywebpush",
|
||
"qbittorrentapi",
|
||
"redis",
|
||
"slack_bolt",
|
||
"slack_sdk",
|
||
"smbclient",
|
||
"smbprotocol",
|
||
"telebot",
|
||
"transmission_rpc",
|
||
)
|
||
PROTOCOL_OPERATIONS = {
|
||
"asyncio.open_connection",
|
||
"socket.create_connection",
|
||
"socket.getaddrinfo",
|
||
"socket.gethostbyname",
|
||
"xmlrpc.client.ServerProxy",
|
||
}
|
||
REEXPORTED_TARGETS = {
|
||
("app.adapters.network.http", "requests"): "requests",
|
||
}
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class _SymbolBinding:
|
||
"""记录表达式符号对应的导入来源与网络能力。"""
|
||
|
||
canonical: str
|
||
target: str | None
|
||
binding: str
|
||
|
||
|
||
_SymbolChoices = tuple[_SymbolBinding, ...]
|
||
|
||
|
||
def _unique_bindings(bindings: Iterable[_SymbolBinding]) -> _SymbolChoices:
|
||
"""把多个可证明来源去重并稳定排序。"""
|
||
return tuple(
|
||
sorted(
|
||
set(bindings),
|
||
key=lambda item: (item.target or "", item.canonical, item.binding),
|
||
)
|
||
)
|
||
|
||
|
||
def _is_type_checking_test(test: ast.expr) -> bool:
|
||
"""判断条件是否只在静态类型检查阶段成立。"""
|
||
return (
|
||
isinstance(test, ast.Name)
|
||
and test.id == "TYPE_CHECKING"
|
||
) or (
|
||
isinstance(test, ast.Attribute)
|
||
and isinstance(test.value, ast.Name)
|
||
and test.value.id == "typing"
|
||
and test.attr == "TYPE_CHECKING"
|
||
)
|
||
|
||
|
||
def _runtime_import_nodes(tree: ast.AST) -> Iterable[ast.Import | ast.ImportFrom]:
|
||
"""遍历运行期导入并排除 TYPE_CHECKING 分支。"""
|
||
parents: dict[ast.AST, ast.AST] = {}
|
||
for parent in ast.walk(tree):
|
||
for child in ast.iter_child_nodes(parent):
|
||
parents[child] = parent
|
||
for node in ast.walk(tree):
|
||
if not isinstance(node, (ast.Import, ast.ImportFrom)):
|
||
continue
|
||
child: ast.AST = node
|
||
parent = parents.get(child)
|
||
while parent is not None:
|
||
if (
|
||
isinstance(parent, ast.If)
|
||
and _is_type_checking_test(parent.test)
|
||
and child in parent.body
|
||
):
|
||
break
|
||
child = parent
|
||
parent = parents.get(parent)
|
||
else:
|
||
yield node
|
||
|
||
|
||
def _registered_target(imported: str) -> tuple[str, str] | None:
|
||
"""把导入模块归一到最长的 raw transport 或 network SDK 根。"""
|
||
candidates = sorted(
|
||
(*RAW_TRANSPORT_ROOTS, *NETWORK_SDK_ROOTS),
|
||
key=len,
|
||
reverse=True,
|
||
)
|
||
for target in candidates:
|
||
if imported == target or imported.startswith(f"{target}."):
|
||
kind = "raw_transport" if target in RAW_TRANSPORT_ROOTS else "network_sdk"
|
||
return target, kind
|
||
return None
|
||
|
||
|
||
def _attribute_parts(node: ast.AST) -> tuple[str, ...] | None:
|
||
"""把 Name/Attribute 表达式拆成稳定的点分片段。"""
|
||
if isinstance(node, ast.Name):
|
||
return (node.id,)
|
||
if not isinstance(node, ast.Attribute):
|
||
return None
|
||
prefix = _attribute_parts(node.value)
|
||
if prefix is None:
|
||
return None
|
||
return (*prefix, node.attr)
|
||
|
||
|
||
def _bound_name(node: ast.AST) -> tuple[str, ...] | None:
|
||
"""返回可进行有限传播的 Name 或 self/cls 属性名。"""
|
||
parts = _attribute_parts(node)
|
||
if not parts:
|
||
return None
|
||
if len(parts) == 1 or parts[0] in {"self", "cls"}:
|
||
return parts
|
||
return None
|
||
|
||
|
||
def _fact_fingerprint(fact: dict[str, object]) -> str:
|
||
"""为不含行号的完整事实生成稳定指纹。"""
|
||
payload = json.dumps(fact, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||
|
||
|
||
def _returns_network_capability(target: str, kind: str, operation: str) -> bool:
|
||
"""判断调用结果是否仍是应继续追踪的 client/capability。"""
|
||
name = operation.rsplit(".", maxsplit=1)[-1]
|
||
if kind == "raw_transport":
|
||
return name in {
|
||
"AsyncClient",
|
||
"Client",
|
||
"ClientSession",
|
||
"Session",
|
||
"WebSocketApp",
|
||
"__call__",
|
||
}
|
||
return (
|
||
name[:1].isupper()
|
||
or name in {"client", "connect", "register_session", "resource"}
|
||
or target in {"smbclient"}
|
||
)
|
||
|
||
|
||
class _EgressVisitor(ast.NodeVisitor):
|
||
"""根据 import provenance 收集稳定的网络能力使用点。"""
|
||
|
||
def __init__(
|
||
self,
|
||
source: str,
|
||
import_records: dict[ast.AST, dict[tuple[str, ...], _SymbolChoices]],
|
||
facts: dict[tuple[str, str, str], dict[str, object]],
|
||
*,
|
||
function_injections: dict[
|
||
ast.FunctionDef | ast.AsyncFunctionDef,
|
||
dict[str, _SymbolChoices],
|
||
] | None = None,
|
||
module_final_scope: dict[
|
||
tuple[str, ...], _SymbolChoices | None
|
||
] | None = None,
|
||
) -> None:
|
||
"""初始化单模块 visitor,并复用预收集的导入符号。"""
|
||
self._source = source
|
||
self._import_records = import_records
|
||
self._facts = facts
|
||
self._qualnames: list[str] = []
|
||
self._scopes: list[dict[tuple[str, ...], _SymbolChoices | None]] = [{}]
|
||
self._scope_kinds = ["module"]
|
||
self._call_results: dict[ast.Call, _SymbolChoices | None] = {}
|
||
self._function_injections = {
|
||
function: dict(injections)
|
||
for function, injections in (function_injections or {}).items()
|
||
}
|
||
self._functions: dict[str, ast.FunctionDef | ast.AsyncFunctionDef] = {}
|
||
self._module_final_scope = (
|
||
dict(module_final_scope) if module_final_scope is not None else None
|
||
)
|
||
self._collecting_class_bindings = False
|
||
|
||
def _qualname(self) -> str:
|
||
"""返回当前调用所在的稳定限定名。"""
|
||
return ".".join(self._qualnames) or "<module>"
|
||
|
||
def _fact(self, target: str, kind: str) -> dict[str, object]:
|
||
"""取得或创建当前 source/capability 的聚合事实。"""
|
||
key = (self._source, target, kind)
|
||
return self._facts.setdefault(
|
||
key,
|
||
{
|
||
"source": self._source,
|
||
"target": target,
|
||
"kind": kind,
|
||
"bindings": set(),
|
||
"uses": [],
|
||
},
|
||
)
|
||
|
||
def _resolve(self, node: ast.AST) -> tuple[_SymbolChoices, tuple[str, ...]] | None:
|
||
"""按最长符号前缀解析表达式及剩余操作片段。"""
|
||
parts = _attribute_parts(node)
|
||
if not parts:
|
||
return None
|
||
for scope in reversed(self._scopes):
|
||
for size in range(len(parts), 0, -1):
|
||
key = parts[:size]
|
||
if key not in scope:
|
||
continue
|
||
binding = scope[key]
|
||
if binding is None:
|
||
return None
|
||
return binding, parts[size:]
|
||
return None
|
||
|
||
def _set_symbol(
|
||
self,
|
||
name: tuple[str, ...],
|
||
binding: _SymbolChoices | None,
|
||
) -> None:
|
||
"""写入当前 lexical scope;self/cls 属性写入最近类作用域。"""
|
||
if name[0] in {"self", "cls"}:
|
||
for index in range(len(self._scope_kinds) - 1, -1, -1):
|
||
if self._scope_kinds[index] == "class":
|
||
if self._collecting_class_bindings:
|
||
if binding is not None:
|
||
existing = self._scopes[index].get(name) or ()
|
||
self._scopes[index][name] = _unique_bindings(
|
||
(*existing, *binding)
|
||
)
|
||
return
|
||
self._scopes[index][name] = binding
|
||
return
|
||
self._scopes[-1][name] = binding
|
||
|
||
def _push_scope(self, kind: str) -> None:
|
||
"""压入一个 lexical symbol scope。"""
|
||
self._scopes.append({})
|
||
self._scope_kinds.append(kind)
|
||
|
||
def _pop_scope(self) -> None:
|
||
"""弹出当前 lexical symbol scope。"""
|
||
self._scopes.pop()
|
||
self._scope_kinds.pop()
|
||
|
||
def seed_module(self, tree: ast.Module) -> None:
|
||
"""预发布模块级 import/alias,使函数分析不依赖定义先后。"""
|
||
parents: dict[ast.AST, ast.AST] = {}
|
||
for parent in ast.walk(tree):
|
||
for child in ast.iter_child_nodes(parent):
|
||
parents[child] = parent
|
||
|
||
def is_module_level(node: ast.AST) -> bool:
|
||
parent = parents.get(node)
|
||
while parent is not None and parent is not tree:
|
||
if isinstance(
|
||
parent,
|
||
(
|
||
ast.ClassDef,
|
||
ast.FunctionDef,
|
||
ast.AsyncFunctionDef,
|
||
ast.Lambda,
|
||
ast.ListComp,
|
||
ast.SetComp,
|
||
ast.DictComp,
|
||
ast.GeneratorExp,
|
||
),
|
||
):
|
||
return False
|
||
parent = parents.get(parent)
|
||
return True
|
||
|
||
for import_node, symbols in self._import_records.items():
|
||
if is_module_level(import_node):
|
||
self._scopes[0].update(symbols)
|
||
for node in ast.walk(tree):
|
||
if not is_module_level(node):
|
||
continue
|
||
if isinstance(node, ast.Assign):
|
||
targets = node.targets
|
||
value = node.value
|
||
elif isinstance(node, ast.AnnAssign) and node.value is not None:
|
||
targets = [node.target]
|
||
value = node.value
|
||
else:
|
||
continue
|
||
choices = self._static_bindings(value)
|
||
if not choices:
|
||
continue
|
||
for target_node in targets:
|
||
target = _bound_name(target_node)
|
||
if target and len(target) == 1:
|
||
self._scopes[0][target] = choices
|
||
self._functions = {
|
||
node.name: node
|
||
for node in tree.body
|
||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||
}
|
||
|
||
def function_injections(self) -> dict[
|
||
ast.FunctionDef | ast.AsyncFunctionDef,
|
||
dict[str, _SymbolChoices],
|
||
]:
|
||
"""返回当前调用点已证明的顶层函数参数来源。"""
|
||
return {
|
||
function: dict(injections)
|
||
for function, injections in self._function_injections.items()
|
||
}
|
||
|
||
def module_scope(self) -> dict[tuple[str, ...], _SymbolChoices | None]:
|
||
"""返回按模块执行顺序收敛后的符号状态。"""
|
||
return dict(self._scopes[0])
|
||
|
||
def _record_function_injection(self, node: ast.Call) -> None:
|
||
"""从调用方当前 scope 传播顶层包装函数的可证明参数来源。"""
|
||
if not isinstance(node.func, ast.Name):
|
||
return
|
||
function = self._functions.get(node.func.id)
|
||
if function is None:
|
||
return
|
||
positional = (*function.args.posonlyargs, *function.args.args)
|
||
arguments: list[tuple[str, ast.AST]] = [
|
||
(parameter.arg, argument)
|
||
for parameter, argument in zip(positional, node.args)
|
||
]
|
||
known_parameters = {
|
||
argument.arg
|
||
for argument in (*positional, *function.args.kwonlyargs)
|
||
}
|
||
arguments.extend(
|
||
(keyword.arg, keyword.value)
|
||
for keyword in node.keywords
|
||
if keyword.arg in known_parameters
|
||
)
|
||
injections = self._function_injections.setdefault(function, {})
|
||
for parameter, argument in arguments:
|
||
choices = self._static_bindings(argument)
|
||
if not choices:
|
||
continue
|
||
injections[parameter] = _unique_bindings(
|
||
(*injections.get(parameter, ()), *choices)
|
||
)
|
||
|
||
def _shadow_arguments(self, arguments: ast.arguments) -> None:
|
||
"""让函数参数遮蔽同名外层 import 或别名。"""
|
||
all_arguments = (
|
||
*arguments.posonlyargs,
|
||
*arguments.args,
|
||
*arguments.kwonlyargs,
|
||
)
|
||
for argument in all_arguments:
|
||
if argument.arg in {"self", "cls"}:
|
||
continue
|
||
self._set_symbol(
|
||
(argument.arg,),
|
||
self._annotation_bindings(argument.annotation),
|
||
)
|
||
if arguments.vararg:
|
||
self._set_symbol((arguments.vararg.arg,), None)
|
||
if arguments.kwarg:
|
||
self._set_symbol((arguments.kwarg.arg,), None)
|
||
|
||
def _annotation_bindings(self, annotation: ast.AST | None) -> _SymbolChoices | None:
|
||
"""从显式 Client/Session 参数注解恢复注入网络能力。"""
|
||
if annotation is None:
|
||
return None
|
||
bindings: list[_SymbolBinding] = []
|
||
for candidate in ast.walk(annotation):
|
||
if not isinstance(candidate, (ast.Name, ast.Attribute)):
|
||
continue
|
||
resolved = self._resolve(candidate)
|
||
if not resolved:
|
||
continue
|
||
choices, remainder = resolved
|
||
for binding in choices:
|
||
canonical = ".".join(
|
||
part for part in (binding.canonical, *remainder) if part
|
||
)
|
||
if canonical.rsplit(".", maxsplit=1)[-1] not in {
|
||
"AsyncClient",
|
||
"Client",
|
||
"ClientSession",
|
||
"Session",
|
||
}:
|
||
continue
|
||
bindings.append(
|
||
_SymbolBinding(
|
||
canonical=binding.target or canonical,
|
||
target=binding.target,
|
||
binding=binding.binding,
|
||
)
|
||
)
|
||
return _unique_bindings(bindings) or None
|
||
|
||
def _static_bindings(self, value: ast.AST) -> _SymbolChoices | None:
|
||
"""不记录 use,仅解析赋值表达式可能产生的网络能力集合。"""
|
||
if isinstance(value, ast.Call):
|
||
resolved = self._resolve(value.func)
|
||
elif isinstance(value, ast.IfExp):
|
||
return _unique_bindings(
|
||
binding
|
||
for branch in (value.body, value.orelse)
|
||
for binding in (self._static_bindings(branch) or ())
|
||
) or None
|
||
else:
|
||
resolved = self._resolve(value)
|
||
if not resolved:
|
||
return None
|
||
choices, remainder = resolved
|
||
if isinstance(value, ast.Call):
|
||
choices = tuple(
|
||
binding
|
||
for binding in choices
|
||
if binding.target is not None
|
||
and (registered := _registered_target(binding.target)) is not None
|
||
and _returns_network_capability(
|
||
registered[0],
|
||
registered[1],
|
||
".".join(
|
||
part for part in (binding.canonical, *remainder) if part
|
||
).removeprefix(registered[0]).lstrip(".")
|
||
or "__call__",
|
||
)
|
||
)
|
||
return _unique_bindings(
|
||
_SymbolBinding(
|
||
canonical=binding.target
|
||
or ".".join(
|
||
part for part in (binding.canonical, *remainder) if part
|
||
),
|
||
target=binding.target,
|
||
binding=binding.binding,
|
||
)
|
||
for binding in choices
|
||
if binding.target is not None
|
||
) or None
|
||
|
||
def _record_call(self, node: ast.Call) -> _SymbolChoices | None:
|
||
"""记录已知能力调用或精确底层协议操作。"""
|
||
if node in self._call_results:
|
||
return self._call_results[node]
|
||
if (
|
||
isinstance(node.func, ast.Name)
|
||
and node.func.id == "__import__"
|
||
and node.args
|
||
and isinstance(node.args[0], ast.Constant)
|
||
and isinstance(node.args[0].value, str)
|
||
and (registered := _registered_target(node.args[0].value))
|
||
):
|
||
target, kind = registered
|
||
binding_text = f"dynamic:__import__({node.args[0].value})"
|
||
fact = self._fact(target, kind)
|
||
fact["bindings"].add(binding_text) # type: ignore[union-attr]
|
||
fact["uses"].append(f"{self._qualname()}|call:dynamic-import") # type: ignore[union-attr]
|
||
result = (
|
||
_SymbolBinding(
|
||
canonical=target,
|
||
target=target,
|
||
binding=binding_text,
|
||
),
|
||
)
|
||
self._call_results[node] = result
|
||
return result
|
||
resolved = self._resolve(node.func)
|
||
if (
|
||
resolved is None
|
||
and isinstance(node.func, ast.Attribute)
|
||
and isinstance(node.func.value, ast.Call)
|
||
):
|
||
if parent_bindings := self._record_call(node.func.value):
|
||
resolved = (parent_bindings, (node.func.attr,))
|
||
if resolved is None:
|
||
self._call_results[node] = None
|
||
return None
|
||
bindings, remainder = resolved
|
||
results: list[_SymbolBinding] = []
|
||
for binding in bindings:
|
||
canonical = ".".join(
|
||
part for part in (binding.canonical, *remainder) if part
|
||
)
|
||
if (
|
||
canonical == "importlib.import_module"
|
||
and node.args
|
||
and isinstance(node.args[0], ast.Constant)
|
||
and isinstance(node.args[0].value, str)
|
||
and (registered := _registered_target(node.args[0].value))
|
||
):
|
||
target, kind = registered
|
||
binding_text = f"dynamic:importlib.import_module({node.args[0].value})"
|
||
fact = self._fact(target, kind)
|
||
fact["bindings"].add(binding_text) # type: ignore[union-attr]
|
||
fact["uses"].append(f"{self._qualname()}|call:dynamic-import") # type: ignore[union-attr]
|
||
results.append(
|
||
_SymbolBinding(
|
||
canonical=target,
|
||
target=target,
|
||
binding=binding_text,
|
||
)
|
||
)
|
||
continue
|
||
if canonical in PROTOCOL_OPERATIONS:
|
||
fact = self._fact(canonical, "protocol_operation")
|
||
fact["bindings"].add(binding.binding) # type: ignore[union-attr]
|
||
fact["uses"].append(f"{self._qualname()}|call:{canonical}") # type: ignore[union-attr]
|
||
results.append(
|
||
_SymbolBinding(
|
||
canonical=canonical,
|
||
target=canonical,
|
||
binding=binding.binding,
|
||
)
|
||
)
|
||
continue
|
||
if binding.target is None:
|
||
continue
|
||
registered = _registered_target(binding.target)
|
||
if registered is None:
|
||
continue
|
||
target, kind = registered
|
||
operation = canonical.removeprefix(target).lstrip(".") or "__call__"
|
||
fact = self._fact(target, kind)
|
||
fact["bindings"].add(binding.binding) # type: ignore[union-attr]
|
||
fact["uses"].append(f"{self._qualname()}|call:{operation}") # type: ignore[union-attr]
|
||
if _returns_network_capability(target, kind, operation):
|
||
results.append(
|
||
_SymbolBinding(
|
||
canonical=target,
|
||
target=target,
|
||
binding=binding.binding,
|
||
)
|
||
)
|
||
result = _unique_bindings(results) or None
|
||
self._call_results[node] = result
|
||
return result
|
||
|
||
def _propagate(self, target_node: ast.AST, value: ast.AST) -> None:
|
||
"""把简单别名或 Client/Session 获取结果传播到局部符号。"""
|
||
target_name = _bound_name(target_node)
|
||
if target_name is None:
|
||
return
|
||
source_bindings: _SymbolChoices | None = None
|
||
if isinstance(value, ast.Call):
|
||
source_bindings = self._record_call(value)
|
||
elif isinstance(value, ast.IfExp):
|
||
alternatives = [self._resolve(value.body), self._resolve(value.orelse)]
|
||
resolved_bindings = [binding for item in alternatives if item for binding in item[0]]
|
||
source_bindings = _unique_bindings(
|
||
_SymbolBinding(
|
||
canonical=binding.target or binding.canonical,
|
||
target=binding.target,
|
||
binding=binding.binding,
|
||
)
|
||
for binding in resolved_bindings
|
||
) or None
|
||
else:
|
||
resolved = self._resolve(value)
|
||
if resolved:
|
||
bindings, remainder = resolved
|
||
source_bindings = _unique_bindings(
|
||
_SymbolBinding(
|
||
canonical=".".join(
|
||
part for part in (binding.canonical, *remainder) if part
|
||
),
|
||
target=binding.target,
|
||
binding=binding.binding,
|
||
)
|
||
for binding in bindings
|
||
)
|
||
self._set_symbol(target_name, source_bindings)
|
||
|
||
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
||
"""在类限定名下访问定义体。"""
|
||
for expression in (*node.decorator_list, *node.bases, *node.keywords):
|
||
self.visit(expression.value if isinstance(expression, ast.keyword) else expression)
|
||
self._qualnames.append(node.name)
|
||
self._push_scope("class")
|
||
self._prebind_class_attributes(node)
|
||
for statement in node.body:
|
||
self.visit(statement)
|
||
self._pop_scope()
|
||
self._qualnames.pop()
|
||
|
||
def _prebind_class_attributes(self, node: ast.ClassDef) -> None:
|
||
"""无副作用预分析各方法,累积 self/cls 可证明网络能力。"""
|
||
original_facts = self._facts
|
||
original_results = self._call_results
|
||
original_flag = self._collecting_class_bindings
|
||
self._facts = {}
|
||
self._call_results = {}
|
||
self._collecting_class_bindings = True
|
||
try:
|
||
for method in node.body:
|
||
if isinstance(method, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||
self.visit(method)
|
||
finally:
|
||
self._collecting_class_bindings = original_flag
|
||
self._call_results = original_results
|
||
self._facts = original_facts
|
||
|
||
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
||
"""在函数限定名下访问定义体。"""
|
||
for expression in (
|
||
*node.decorator_list,
|
||
*node.args.defaults,
|
||
*(default for default in node.args.kw_defaults if default is not None),
|
||
):
|
||
self.visit(expression)
|
||
original_module_scope = self._scopes[0]
|
||
if self._module_final_scope is not None:
|
||
self._scopes[0] = dict(self._module_final_scope)
|
||
class_index = next(
|
||
(
|
||
index
|
||
for index in range(len(self._scope_kinds) - 1, -1, -1)
|
||
if self._scope_kinds[index] == "class"
|
||
),
|
||
None,
|
||
)
|
||
class_scope = (
|
||
dict(self._scopes[class_index])
|
||
if class_index is not None
|
||
else None
|
||
)
|
||
self._qualnames.append(node.name)
|
||
self._push_scope("function")
|
||
self._shadow_arguments(node.args)
|
||
for parameter, choices in self._function_injections.get(node, {}).items():
|
||
existing = self._scopes[-1].get((parameter,)) or ()
|
||
self._set_symbol(
|
||
(parameter,),
|
||
_unique_bindings((*existing, *choices)),
|
||
)
|
||
positional = (*node.args.posonlyargs, *node.args.args)
|
||
for argument, default in zip(
|
||
positional[-len(node.args.defaults):],
|
||
node.args.defaults,
|
||
):
|
||
if self._static_bindings(default):
|
||
self._propagate(ast.Name(id=argument.arg), default)
|
||
for argument, default in zip(node.args.kwonlyargs, node.args.kw_defaults):
|
||
if default is not None and self._static_bindings(default):
|
||
self._propagate(ast.Name(id=argument.arg), default)
|
||
for statement in node.body:
|
||
self.visit(statement)
|
||
self._pop_scope()
|
||
self._qualnames.pop()
|
||
if (
|
||
not self._collecting_class_bindings
|
||
and class_index is not None
|
||
and class_scope is not None
|
||
):
|
||
self._scopes[class_index] = class_scope
|
||
self._scopes[0] = original_module_scope
|
||
|
||
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
||
"""在异步函数限定名下访问定义体。"""
|
||
self.visit_FunctionDef(node)
|
||
|
||
def visit_Import(self, node: ast.Import) -> None:
|
||
"""在导入发生的 lexical scope 发布网络符号。"""
|
||
self._scopes[-1].update(self._import_records.get(node, {}))
|
||
|
||
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
||
"""在 from-import 发生的 lexical scope 发布网络符号。"""
|
||
self._scopes[-1].update(self._import_records.get(node, {}))
|
||
|
||
def visit_If(self, node: ast.If) -> None:
|
||
"""跳过 TYPE_CHECKING 专用分支,其余条件按源码顺序扫描。"""
|
||
self.visit(node.test)
|
||
if _is_type_checking_test(node.test):
|
||
for statement in node.orelse:
|
||
self.visit(statement)
|
||
return
|
||
|
||
original = [dict(scope) for scope in self._scopes]
|
||
for statement in node.body:
|
||
self.visit(statement)
|
||
body_scopes = [dict(scope) for scope in self._scopes]
|
||
|
||
self._scopes = [dict(scope) for scope in original]
|
||
for statement in node.orelse:
|
||
self.visit(statement)
|
||
else_scopes = [dict(scope) for scope in self._scopes]
|
||
|
||
merged_scopes: list[dict[tuple[str, ...], _SymbolChoices | None]] = []
|
||
for original_scope, body_scope, else_scope in zip(
|
||
original,
|
||
body_scopes,
|
||
else_scopes,
|
||
):
|
||
merged: dict[tuple[str, ...], _SymbolChoices | None] = {}
|
||
for name in original_scope.keys() | body_scope.keys() | else_scope.keys():
|
||
alternatives = _unique_bindings(
|
||
binding
|
||
for binding in (
|
||
body_scope.get(name, original_scope.get(name)),
|
||
else_scope.get(name, original_scope.get(name)),
|
||
)
|
||
if binding is not None
|
||
for binding in binding
|
||
)
|
||
merged[name] = alternatives or None
|
||
merged_scopes.append(merged)
|
||
self._scopes = merged_scopes
|
||
|
||
def visit_Try(self, node: ast.Try) -> None:
|
||
"""按成功/异常路径合并符号来源,finally 在合并结果上执行。"""
|
||
original = [dict(scope) for scope in self._scopes]
|
||
states: list[list[dict[tuple[str, ...], _SymbolChoices | None]]] = []
|
||
|
||
for statement in (*node.body, *node.orelse):
|
||
self.visit(statement)
|
||
states.append([dict(scope) for scope in self._scopes])
|
||
|
||
for handler in node.handlers:
|
||
self._scopes = [dict(scope) for scope in original]
|
||
if handler.type is not None:
|
||
self.visit(handler.type)
|
||
if handler.name:
|
||
self._set_symbol((handler.name,), None)
|
||
for statement in handler.body:
|
||
self.visit(statement)
|
||
states.append([dict(scope) for scope in self._scopes])
|
||
|
||
merged_scopes: list[dict[tuple[str, ...], _SymbolChoices | None]] = []
|
||
for index, original_scope in enumerate(original):
|
||
merged: dict[tuple[str, ...], _SymbolChoices | None] = {}
|
||
names = original_scope.keys()
|
||
for state in states:
|
||
names = names | state[index].keys()
|
||
for name in names:
|
||
alternatives = _unique_bindings(
|
||
binding
|
||
for state in states
|
||
for choices in (state[index].get(name, original_scope.get(name)),)
|
||
if choices is not None
|
||
for binding in choices
|
||
)
|
||
merged[name] = alternatives or None
|
||
merged_scopes.append(merged)
|
||
self._scopes = merged_scopes
|
||
for statement in node.finalbody:
|
||
self.visit(statement)
|
||
|
||
def visit_For(self, node: ast.For) -> None:
|
||
"""for 目标在循环体遮蔽同名符号,并保留循环零次路径。"""
|
||
self.visit(node.iter)
|
||
original = [dict(scope) for scope in self._scopes]
|
||
target = _bound_name(node.target)
|
||
if target:
|
||
self._set_symbol(target, None)
|
||
for statement in (*node.body, *node.orelse):
|
||
self.visit(statement)
|
||
body_scopes = [dict(scope) for scope in self._scopes]
|
||
merged_scopes: list[dict[tuple[str, ...], _SymbolChoices | None]] = []
|
||
for original_scope, body_scope in zip(original, body_scopes):
|
||
merged: dict[tuple[str, ...], _SymbolChoices | None] = {}
|
||
for name in original_scope.keys() | body_scope.keys():
|
||
alternatives = _unique_bindings(
|
||
binding
|
||
for choices in (
|
||
original_scope.get(name),
|
||
body_scope.get(name, original_scope.get(name)),
|
||
)
|
||
if choices is not None
|
||
for binding in choices
|
||
)
|
||
merged[name] = alternatives or None
|
||
merged_scopes.append(merged)
|
||
self._scopes = merged_scopes
|
||
|
||
def visit_AsyncFor(self, node: ast.AsyncFor) -> None:
|
||
"""异步 for 使用与同步 for 相同的遮蔽规则。"""
|
||
self.visit_For(node)
|
||
|
||
def visit_Lambda(self, node: ast.Lambda) -> None:
|
||
"""lambda 参数只在其表达式 scope 内遮蔽外层符号。"""
|
||
for default in (*node.args.defaults, *node.args.kw_defaults):
|
||
if default is not None:
|
||
self.visit(default)
|
||
self._push_scope("lambda")
|
||
self._shadow_arguments(node.args)
|
||
self.visit(node.body)
|
||
self._pop_scope()
|
||
|
||
def _visit_comprehension(
|
||
self,
|
||
generators: list[ast.comprehension],
|
||
expressions: tuple[ast.AST, ...],
|
||
) -> None:
|
||
"""在独立 comprehension scope 中处理迭代目标遮蔽。"""
|
||
self._push_scope("comprehension")
|
||
for generator in generators:
|
||
self.visit(generator.iter)
|
||
target = _bound_name(generator.target)
|
||
if target:
|
||
self._set_symbol(target, None)
|
||
for condition in generator.ifs:
|
||
self.visit(condition)
|
||
for expression in expressions:
|
||
self.visit(expression)
|
||
self._pop_scope()
|
||
|
||
def visit_ListComp(self, node: ast.ListComp) -> None:
|
||
"""访问列表推导式的独立 scope。"""
|
||
self._visit_comprehension(node.generators, (node.elt,))
|
||
|
||
def visit_SetComp(self, node: ast.SetComp) -> None:
|
||
"""访问集合推导式的独立 scope。"""
|
||
self._visit_comprehension(node.generators, (node.elt,))
|
||
|
||
def visit_GeneratorExp(self, node: ast.GeneratorExp) -> None:
|
||
"""访问生成器表达式的独立 scope。"""
|
||
self._visit_comprehension(node.generators, (node.elt,))
|
||
|
||
def visit_DictComp(self, node: ast.DictComp) -> None:
|
||
"""访问字典推导式的独立 scope。"""
|
||
self._visit_comprehension(node.generators, (node.key, node.value))
|
||
|
||
def visit_Assign(self, node: ast.Assign) -> None:
|
||
"""记录赋值右侧调用并传播简单目标。"""
|
||
self.visit(node.value)
|
||
for target in node.targets:
|
||
self._propagate(target, node.value)
|
||
|
||
def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
|
||
"""记录带注解赋值并传播简单目标。"""
|
||
if node.value is not None:
|
||
self.visit(node.value)
|
||
self._propagate(node.target, node.value)
|
||
|
||
def visit_NamedExpr(self, node: ast.NamedExpr) -> None:
|
||
"""记录海象赋值并传播简单目标。"""
|
||
self.visit(node.value)
|
||
self._propagate(node.target, node.value)
|
||
|
||
def visit_With(self, node: ast.With) -> None:
|
||
"""记录上下文能力获取,并把 as 目标传播到定义体。"""
|
||
for item in node.items:
|
||
self.visit(item.context_expr)
|
||
if item.optional_vars is not None:
|
||
self._propagate(item.optional_vars, item.context_expr)
|
||
for statement in node.body:
|
||
self.visit(statement)
|
||
|
||
def visit_AsyncWith(self, node: ast.AsyncWith) -> None:
|
||
"""记录异步上下文能力获取,并把 as 目标传播到定义体。"""
|
||
for item in node.items:
|
||
self.visit(item.context_expr)
|
||
if item.optional_vars is not None:
|
||
self._propagate(item.optional_vars, item.context_expr)
|
||
for statement in node.body:
|
||
self.visit(statement)
|
||
|
||
def visit_Call(self, node: ast.Call) -> None:
|
||
"""记录调用后继续访问参数中的嵌套调用。"""
|
||
self._record_call(node)
|
||
self._record_function_injection(node)
|
||
self.visit(node.func)
|
||
for argument in (*node.args, *node.keywords):
|
||
self.visit(argument.value if isinstance(argument, ast.keyword) else argument)
|
||
|
||
|
||
def _import_records(
|
||
source: str,
|
||
tree: ast.Module,
|
||
facts: dict[tuple[str, str, str], dict[str, object]],
|
||
) -> dict[ast.AST, dict[tuple[str, ...], _SymbolChoices]]:
|
||
"""预收集运行期 import 事实,并按 AST 节点保留 lexical 发布记录。"""
|
||
records: dict[ast.AST, dict[tuple[str, ...], _SymbolChoices]] = {}
|
||
for node in _runtime_import_nodes(tree):
|
||
symbols: dict[tuple[str, ...], _SymbolChoices] = {}
|
||
if isinstance(node, ast.Import):
|
||
for alias in node.names:
|
||
bound = alias.asname or alias.name.split(".", maxsplit=1)[0]
|
||
canonical = alias.name if alias.asname else bound
|
||
registered = _registered_target(alias.name)
|
||
target, kind = registered if registered else (None, None)
|
||
binding_text = f"import:{alias.name} as {bound}"
|
||
symbols[(bound,)] = (
|
||
_SymbolBinding(canonical, target, binding_text),
|
||
)
|
||
if registered:
|
||
fact = facts.setdefault(
|
||
(source, target, kind),
|
||
{
|
||
"source": source,
|
||
"target": target,
|
||
"kind": kind,
|
||
"bindings": set(),
|
||
"uses": [],
|
||
},
|
||
)
|
||
fact["bindings"].add(binding_text) # type: ignore[union-attr]
|
||
records[node] = symbols
|
||
continue
|
||
module = node.module or ""
|
||
for alias in node.names:
|
||
if alias.name == "*":
|
||
if registered := _registered_target(module):
|
||
target, kind = registered
|
||
binding_text = f"from:{module}.* as *"
|
||
fact = facts.setdefault(
|
||
(source, target, kind),
|
||
{
|
||
"source": source,
|
||
"target": target,
|
||
"kind": kind,
|
||
"bindings": set(),
|
||
"uses": [],
|
||
},
|
||
)
|
||
fact["bindings"].add(binding_text) # type: ignore[union-attr]
|
||
continue
|
||
bound = alias.asname or alias.name
|
||
reexport = REEXPORTED_TARGETS.get((module, alias.name))
|
||
imported = reexport or f"{module}.{alias.name}"
|
||
registered = _registered_target(imported) or _registered_target(module)
|
||
target, kind = registered if registered else (None, None)
|
||
if reexport:
|
||
canonical = reexport
|
||
binding_text = f"reexport:{module}.{alias.name} as {bound}"
|
||
elif registered and imported == target:
|
||
canonical = imported
|
||
binding_text = f"from:{module}.{alias.name} as {bound}"
|
||
else:
|
||
canonical = imported
|
||
binding_text = f"from:{module}.{alias.name} as {bound}"
|
||
symbols[(bound,)] = (
|
||
_SymbolBinding(canonical, target, binding_text),
|
||
)
|
||
if registered:
|
||
fact = facts.setdefault(
|
||
(source, target, kind),
|
||
{
|
||
"source": source,
|
||
"target": target,
|
||
"kind": kind,
|
||
"bindings": set(),
|
||
"uses": [],
|
||
},
|
||
)
|
||
fact["bindings"].add(binding_text) # type: ignore[union-attr]
|
||
records[node] = symbols
|
||
return records
|
||
|
||
|
||
def collect_direct_egress(modules: dict[str, Path]) -> dict[str, object]:
|
||
"""收集宿主 direct egress 事实、注册表和稳定统计。"""
|
||
facts: dict[tuple[str, str, str], dict[str, object]] = {}
|
||
for source, path in sorted(modules.items()):
|
||
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
|
||
discovery_facts: dict[tuple[str, str, str], dict[str, object]] = {}
|
||
discovery_records = _import_records(source, tree, discovery_facts)
|
||
discovery = _EgressVisitor(source, discovery_records, discovery_facts)
|
||
discovery.seed_module(tree)
|
||
discovery.visit(tree)
|
||
import_records = _import_records(source, tree, facts)
|
||
visitor = _EgressVisitor(
|
||
source,
|
||
import_records,
|
||
facts,
|
||
function_injections=discovery.function_injections(),
|
||
module_final_scope=discovery.module_scope(),
|
||
)
|
||
visitor.seed_module(tree)
|
||
visitor.visit(tree)
|
||
|
||
entries: list[dict[str, object]] = []
|
||
for fact in facts.values():
|
||
normalized = {
|
||
"source": fact["source"],
|
||
"target": fact["target"],
|
||
"kind": fact["kind"],
|
||
"bindings": sorted(fact["bindings"]),
|
||
"uses": sorted(fact["uses"]),
|
||
}
|
||
entries.append({**normalized, "fingerprint": _fact_fingerprint(normalized)})
|
||
entries.sort(key=lambda item: (item["source"], item["target"], item["kind"]))
|
||
|
||
counts_by_kind = {
|
||
kind: sum(entry["kind"] == kind for entry in entries)
|
||
for kind in ("raw_transport", "network_sdk", "protocol_operation")
|
||
}
|
||
source_roots = {
|
||
root: sum(
|
||
entry["source"] == root or str(entry["source"]).startswith(f"{root}.")
|
||
for entry in entries
|
||
)
|
||
for root in ("app.application", "app.chain")
|
||
}
|
||
return {
|
||
"scope": {
|
||
"root": "app",
|
||
"excluded_roots": ["app.plugins"],
|
||
"runtime_only": True,
|
||
"line_numbers": False,
|
||
},
|
||
"registry": {
|
||
"raw_transports": list(RAW_TRANSPORT_ROOTS),
|
||
"network_sdks": list(NETWORK_SDK_ROOTS),
|
||
"protocol_operations": sorted(PROTOCOL_OPERATIONS),
|
||
"reexports": [
|
||
{
|
||
"source": f"{module}.{member}",
|
||
"target": target,
|
||
}
|
||
for (module, member), target in sorted(REEXPORTED_TARGETS.items())
|
||
],
|
||
},
|
||
"count": len(entries),
|
||
"counts_by_kind": counts_by_kind,
|
||
"application_chain_counts": source_roots,
|
||
"entries": entries,
|
||
}
|