cg-code-graph 0.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- codegraph/xcode.py +251 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""Python entry points and function references (stdlib `ast` + `tomllib`, no code execution).
|
|
2
|
+
|
|
3
|
+
Entry points (`script` nodes, CALLS to what they run):
|
|
4
|
+
* `if __name__ == "__main__":` blocks -> `script:<module>` (entry `main`, "python -m pkg.mod");
|
|
5
|
+
* `pkg/__main__.py` -> `script:pkg.__main__` (entry `main`, "python -m pkg"), its whole module body;
|
|
6
|
+
* packaging entry points: PEP 621 `[project.scripts]` / `[project.gui-scripts]` / `[project.entry-points.<group>]`,
|
|
7
|
+
Poetry `[tool.poetry.scripts]` / `[tool.poetry.plugins.<group>]`, `setup.cfg [options.entry_points]` and a
|
|
8
|
+
literal `setup(entry_points=...)` -> `script:<group>:<name>` (entry `main` for console / GUI scripts,
|
|
9
|
+
`public_api` for plugin groups such as `pytest11`), resolved through the detected source roots.
|
|
10
|
+
* registrations with well-known frameworks: MCP servers (`@mcp.tool()`, `mcp.tool()(fn)`, `add_tool`) ->
|
|
11
|
+
`message_handler`; click / typer / Flask CLI commands -> `cli_command`. A local decorator that wraps its argument
|
|
12
|
+
and registers the wrapper passes the registration on to every function it decorates.
|
|
13
|
+
|
|
14
|
+
Function references (REFERENCES_FN, propagating, attrs.how):
|
|
15
|
+
* collection - an element of a tuple / list / set / dict literal (dispatch tables, module constants);
|
|
16
|
+
* callback - an argument to a call (`map(f, xs)`, `Thread(target=f)`, `atexit.register(f)`, `run(f, x)`);
|
|
17
|
+
* assignment - assigned, returned or used as a default value (`handler = f`, `return f`);
|
|
18
|
+
* decorator - the decorated function, from the decorator that receives it (a local decorator function, a
|
|
19
|
+
local registry method, `@group.command()` on a function object) or, for an external registering
|
|
20
|
+
decorator (`@app.route`, `@registry.register`), from the module (heuristic).
|
|
21
|
+
Decorator applications are calls too: `@deco` adds CALLS module -> deco (attrs.via = "decorator").
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import ast
|
|
26
|
+
import configparser
|
|
27
|
+
import re
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from ...blindspots import REGISTER_DECOS, ROUTE_DECOS
|
|
31
|
+
from ...core.model import EXACT, HEURISTIC, RESOLVED
|
|
32
|
+
|
|
33
|
+
MCP_SERVERS = ("FastMCP", "MCPServer")
|
|
34
|
+
MCP_DECOS = {"tool", "resource", "prompt"}
|
|
35
|
+
MCP_ADDERS = {"add_tool": "tool", "add_resource": "resource", "add_prompt": "prompt"}
|
|
36
|
+
CLICK_LIBS = ("click", "asyncclick", "rich_click", "cloup")
|
|
37
|
+
COLLECTION_ADDS = {"append", "appendleft", "add", "insert", "extend", "setdefault"}
|
|
38
|
+
SCRIPT_GROUPS = {"console_scripts": "console script", "gui_scripts": "GUI script"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_main_guard(test) -> bool:
|
|
42
|
+
"""`__name__ == "__main__"` (either side)."""
|
|
43
|
+
if not (isinstance(test, ast.Compare) and len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq)):
|
|
44
|
+
return False
|
|
45
|
+
sides = [test.left, test.comparators[0]]
|
|
46
|
+
return any(isinstance(x, ast.Name) and x.id == "__name__" for x in sides) and \
|
|
47
|
+
any(isinstance(x, ast.Constant) and x.value == "__main__" for x in sides)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# --------------------------------------------------------------------------- packaging entry points
|
|
51
|
+
|
|
52
|
+
def _line_of(text: str, name: str) -> int:
|
|
53
|
+
pat = re.compile(r"^\s*['\"]?" + re.escape(name) + r"['\"]?\s*[=:]", re.M)
|
|
54
|
+
m = pat.search(text)
|
|
55
|
+
return text.count("\n", 0, m.start()) + 1 if m else 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _ini_entries(text: str) -> list[tuple[str, str]]:
|
|
59
|
+
out = []
|
|
60
|
+
for ln in text.splitlines():
|
|
61
|
+
ln = ln.strip()
|
|
62
|
+
if "=" in ln and not ln.startswith(("#", ";", "[")):
|
|
63
|
+
k, v = ln.split("=", 1)
|
|
64
|
+
out.append((k.strip(), v.strip()))
|
|
65
|
+
return out
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _pyproject_entries(text: str) -> list[tuple[str, str, str, str]]:
|
|
69
|
+
"""(group, name, target, declared_in)"""
|
|
70
|
+
try:
|
|
71
|
+
import tomllib
|
|
72
|
+
d = tomllib.loads(text)
|
|
73
|
+
except Exception: # noqa: BLE001 (invalid TOML: no entry points from this file)
|
|
74
|
+
return []
|
|
75
|
+
out = []
|
|
76
|
+
proj = d.get("project") if isinstance(d.get("project"), dict) else {}
|
|
77
|
+
for key, group in (("scripts", "console_scripts"), ("gui-scripts", "gui_scripts")):
|
|
78
|
+
for k, v in (proj.get(key) or {}).items() if isinstance(proj.get(key), dict) else ():
|
|
79
|
+
if isinstance(v, str):
|
|
80
|
+
out.append((group, k, v, f"[project.{key}]"))
|
|
81
|
+
eps = proj.get("entry-points") if isinstance(proj.get("entry-points"), dict) else {}
|
|
82
|
+
for group, items in eps.items():
|
|
83
|
+
for k, v in (items or {}).items() if isinstance(items, dict) else ():
|
|
84
|
+
if isinstance(v, str):
|
|
85
|
+
out.append((group, k, v, f"[project.entry-points.{group}]"))
|
|
86
|
+
tool = d.get("tool") if isinstance(d.get("tool"), dict) else {}
|
|
87
|
+
poetry = tool.get("poetry") if isinstance(tool.get("poetry"), dict) else {}
|
|
88
|
+
for k, v in (poetry.get("scripts") or {}).items() if isinstance(poetry.get("scripts"), dict) else ():
|
|
89
|
+
if isinstance(v, dict):
|
|
90
|
+
v = v.get("reference") or v.get("callable") if v.get("type", "console") == "console" else None
|
|
91
|
+
if isinstance(v, str):
|
|
92
|
+
out.append(("console_scripts", k, v, "[tool.poetry.scripts]"))
|
|
93
|
+
for group, items in (poetry.get("plugins") or {}).items() if isinstance(poetry.get("plugins"), dict) else ():
|
|
94
|
+
for k, v in (items or {}).items() if isinstance(items, dict) else ():
|
|
95
|
+
if isinstance(v, str):
|
|
96
|
+
out.append((group, k, v, f"[tool.poetry.plugins.{group}]"))
|
|
97
|
+
flit = tool.get("flit") if isinstance(tool.get("flit"), dict) else {}
|
|
98
|
+
for k, v in (flit.get("scripts") or {}).items() if isinstance(flit.get("scripts"), dict) else ():
|
|
99
|
+
if isinstance(v, str):
|
|
100
|
+
out.append(("console_scripts", k, v, "[tool.flit.scripts]"))
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _setup_cfg_entries(text: str) -> list[tuple[str, str, str, str]]:
|
|
105
|
+
cp = configparser.ConfigParser(interpolation=None)
|
|
106
|
+
try:
|
|
107
|
+
cp.read_string(text)
|
|
108
|
+
except configparser.Error:
|
|
109
|
+
return []
|
|
110
|
+
if not cp.has_section("options.entry_points"):
|
|
111
|
+
return []
|
|
112
|
+
out = []
|
|
113
|
+
for group, val in cp.items("options.entry_points"):
|
|
114
|
+
for k, v in _ini_entries(val):
|
|
115
|
+
out.append((group, k, v, "[options.entry_points]"))
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _setup_py_entries(text: str) -> list[tuple[str, str, str, str]]:
|
|
120
|
+
try:
|
|
121
|
+
tree = ast.parse(text)
|
|
122
|
+
except (SyntaxError, ValueError, RecursionError):
|
|
123
|
+
return []
|
|
124
|
+
consts = {}
|
|
125
|
+
for st in tree.body:
|
|
126
|
+
if isinstance(st, ast.Assign) and len(st.targets) == 1 and isinstance(st.targets[0], ast.Name):
|
|
127
|
+
consts[st.targets[0].id] = st.value
|
|
128
|
+
out = []
|
|
129
|
+
|
|
130
|
+
def strs(v):
|
|
131
|
+
if isinstance(v, ast.Name):
|
|
132
|
+
v = consts.get(v.id)
|
|
133
|
+
if isinstance(v, ast.Constant) and isinstance(v.value, str):
|
|
134
|
+
return [x for x in v.value.splitlines() if x.strip()]
|
|
135
|
+
if isinstance(v, (ast.List, ast.Tuple)):
|
|
136
|
+
return [x.value for x in v.elts if isinstance(x, ast.Constant) and isinstance(x.value, str)]
|
|
137
|
+
return []
|
|
138
|
+
for n in ast.walk(tree):
|
|
139
|
+
if not isinstance(n, ast.Call):
|
|
140
|
+
continue
|
|
141
|
+
fname = n.func.attr if isinstance(n.func, ast.Attribute) else n.func.id if isinstance(n.func, ast.Name) else ""
|
|
142
|
+
if fname != "setup":
|
|
143
|
+
continue
|
|
144
|
+
for kw in n.keywords:
|
|
145
|
+
if kw.arg != "entry_points":
|
|
146
|
+
continue
|
|
147
|
+
v = consts.get(kw.value.id) if isinstance(kw.value, ast.Name) else kw.value
|
|
148
|
+
if isinstance(v, ast.Dict):
|
|
149
|
+
for k, items in zip(v.keys, v.values):
|
|
150
|
+
if isinstance(k, ast.Constant) and isinstance(k.value, str):
|
|
151
|
+
for line in strs(items):
|
|
152
|
+
if "=" in line:
|
|
153
|
+
a, b = line.split("=", 1)
|
|
154
|
+
out.append((k.value, a.strip(), b.strip(), "setup(entry_points=...)"))
|
|
155
|
+
elif isinstance(v, ast.Constant) and isinstance(v.value, str): # ini-style string
|
|
156
|
+
group = None
|
|
157
|
+
for line in v.value.splitlines():
|
|
158
|
+
line = line.strip()
|
|
159
|
+
if line.startswith("[") and line.endswith("]"):
|
|
160
|
+
group = line[1:-1].strip()
|
|
161
|
+
elif group and "=" in line:
|
|
162
|
+
a, b = line.split("=", 1)
|
|
163
|
+
out.append((group, a.strip(), b.strip(), "setup(entry_points=...)"))
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def packaging_entry_points(root: Path, project_dirs: list[str]) -> list[dict]:
|
|
168
|
+
out = []
|
|
169
|
+
for pd in project_dirs:
|
|
170
|
+
base = root / pd if pd else root
|
|
171
|
+
for fn, parse in (("pyproject.toml", _pyproject_entries), ("setup.cfg", _setup_cfg_entries),
|
|
172
|
+
("setup.py", _setup_py_entries)):
|
|
173
|
+
p = base / fn
|
|
174
|
+
if not p.is_file():
|
|
175
|
+
continue
|
|
176
|
+
try:
|
|
177
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
178
|
+
except OSError:
|
|
179
|
+
continue
|
|
180
|
+
rel = f"{pd}/{fn}" if pd else fn
|
|
181
|
+
for group, name, target, where in parse(text):
|
|
182
|
+
out.append({"project": pd, "file": rel, "line": _line_of(text, name), "group": group, "name": name,
|
|
183
|
+
"target": target, "declared_in": f"{rel} {where}"})
|
|
184
|
+
return out
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# --------------------------------------------------------------------------- the pass
|
|
188
|
+
|
|
189
|
+
class RefPass:
|
|
190
|
+
def __init__(self, prog, b, ctx_cls):
|
|
191
|
+
self.prog, self.b, self.Ctx = prog, b, ctx_cls
|
|
192
|
+
self.fn_by_node = {id(f.node): f for f in prog.funcs.values()}
|
|
193
|
+
self.cls_by_node = {id(c.node): c for c in prog.classes.values()}
|
|
194
|
+
self.names = {f.name for f in prog.funcs.values()}
|
|
195
|
+
self.stats = {"references": {}, "decorator_calls": 0, "main_blocks": 0, "entry_points": 0,
|
|
196
|
+
"entry_points_unresolved": [], "registrations": {}}
|
|
197
|
+
self.registrars: dict = {} # FuncInfo.id -> (FuncInfo, kind, attrs): wraps and registers its argument
|
|
198
|
+
self.decorated_by: dict = {} # FuncInfo.id -> [FuncInfo] decorated with it
|
|
199
|
+
self.passed: list = [] # (call, ctx, FuncInfo) first argument of a call is a known function
|
|
200
|
+
self.var_regs: dict = {} # (module name, var) -> [FuncInfo] registered on that object (typer app)
|
|
201
|
+
self.obj_calls: list = [] # (script id, (module name, var), file, line): a script calls a CLI object
|
|
202
|
+
|
|
203
|
+
# ---- helpers
|
|
204
|
+
def ref(self, src, f, how, file, line, conf=RESOLVED, **attrs):
|
|
205
|
+
if src == f.id:
|
|
206
|
+
return
|
|
207
|
+
self.b.add_edge(src, f.id, "REFERENCES_FN", file, line, conf, how=how, **attrs)
|
|
208
|
+
self.stats["references"][how] = self.stats["references"].get(how, 0) + 1
|
|
209
|
+
|
|
210
|
+
def func_of(self, t):
|
|
211
|
+
if t and t[0] in ("func", "bound"):
|
|
212
|
+
return t[1]
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
def value_ref(self, t):
|
|
216
|
+
"""The function a value expression refers to; None for a property (reading it runs the getter)."""
|
|
217
|
+
f = self.func_of(t)
|
|
218
|
+
if f is None or (t[0] == "bound" and is_property(f)):
|
|
219
|
+
return None
|
|
220
|
+
return f
|
|
221
|
+
|
|
222
|
+
def set_entry(self, f, kind, attrs):
|
|
223
|
+
n = self.b.nodes.get(f.id)
|
|
224
|
+
if n is None:
|
|
225
|
+
return
|
|
226
|
+
reg = dict(attrs)
|
|
227
|
+
if n.attrs is None:
|
|
228
|
+
n.attrs = {}
|
|
229
|
+
n.attrs.setdefault("registrations", [])
|
|
230
|
+
if reg not in n.attrs["registrations"]:
|
|
231
|
+
n.attrs["registrations"].append(reg)
|
|
232
|
+
if n.entry_kind is None:
|
|
233
|
+
n.entry_kind = kind
|
|
234
|
+
elif n.entry_kind != kind:
|
|
235
|
+
n.attrs["entry_kinds"] = sorted(set(n.attrs.get("entry_kinds", [n.entry_kind])) | {kind})
|
|
236
|
+
key = f"{attrs.get('framework')}:{kind}"
|
|
237
|
+
self.stats["registrations"][key] = self.stats["registrations"].get(key, 0) + 1
|
|
238
|
+
|
|
239
|
+
# ---- framework registrations
|
|
240
|
+
def registration(self, e, ctx):
|
|
241
|
+
"""`e` is a decorator expression (without its call) or the callee of a registering call: (kind, attrs)."""
|
|
242
|
+
p = self.prog
|
|
243
|
+
if not isinstance(e, ast.Attribute):
|
|
244
|
+
return None
|
|
245
|
+
attr = e.attr
|
|
246
|
+
recv = p.infer(e.value, ctx)
|
|
247
|
+
rname = recv[1] if recv and recv[0] in ("einst", "ext") and isinstance(recv[1], str) else ""
|
|
248
|
+
last = rname.rsplit(".", 1)[-1]
|
|
249
|
+
if recv and recv[0] == "einst" and last in MCP_SERVERS and attr in (MCP_DECOS | set(MCP_ADDERS)):
|
|
250
|
+
return "message_handler", {"framework": "mcp", "registration": MCP_ADDERS.get(attr, attr)}
|
|
251
|
+
full = p.infer(e, ctx)
|
|
252
|
+
if full and full[0] == "ext" and attr in ("command", "group") and full[1].split(".")[0] in CLICK_LIBS:
|
|
253
|
+
return "cli_command", {"framework": full[1].split(".")[0], "registration": attr}
|
|
254
|
+
if attr in ("command", "group"):
|
|
255
|
+
if recv and recv[0] == "einst" and rname.split(".")[0] in CLICK_LIBS:
|
|
256
|
+
return "cli_command", {"framework": rname.split(".")[0], "registration": attr}
|
|
257
|
+
g = self.func_of(recv)
|
|
258
|
+
if g is not None and any(self._is_click(d, g) for d in g.decorators):
|
|
259
|
+
return "cli_command", {"framework": "click", "registration": attr}
|
|
260
|
+
if recv and recv[0] == "einst" and rname.split(".")[0] == "typer" and attr in ("command", "callback"):
|
|
261
|
+
return "cli_command", {"framework": "typer", "registration": attr}
|
|
262
|
+
if attr == "command" and (dotted(e.value) or "").endswith(".cli"):
|
|
263
|
+
return "cli_command", {"framework": "flask", "registration": "cli.command"}
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
def _is_click(self, d, g) -> bool:
|
|
267
|
+
de = d.func if isinstance(d, ast.Call) else d
|
|
268
|
+
t = self.prog.infer(de, self.Ctx(g.module, None, None))
|
|
269
|
+
if t and t[0] == "ext" and t[1].split(".")[0] in CLICK_LIBS and t[1].rsplit(".", 1)[-1] in ("group", "command"):
|
|
270
|
+
return True
|
|
271
|
+
return bool(isinstance(de, ast.Attribute) and de.attr in ("group", "command") and
|
|
272
|
+
self.func_of(self.prog.infer(de.value, self.Ctx(g.module, None, None))) is not None)
|
|
273
|
+
|
|
274
|
+
def _recv_var(self, e, ctx):
|
|
275
|
+
"""(module, var) of a module-level object a decorator registers on (`app` in `@app.command()`)."""
|
|
276
|
+
if isinstance(e, ast.Attribute) and isinstance(e.value, ast.Name):
|
|
277
|
+
r = self.prog.resolve_name(ctx.mod, e.value.id)
|
|
278
|
+
if r and r[0] == "var":
|
|
279
|
+
return (r[1].name, r[2])
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
# ---- decorators
|
|
283
|
+
def decorator(self, d, target, owner_id, ctx, file):
|
|
284
|
+
"""`target` is the decorated FuncInfo, or None for a nested def (it collapses into the owner)."""
|
|
285
|
+
p = self.prog
|
|
286
|
+
de = d.func if isinstance(d, ast.Call) else d
|
|
287
|
+
reg = self.registration(de, ctx)
|
|
288
|
+
if reg:
|
|
289
|
+
if target is not None:
|
|
290
|
+
self.set_entry(target, reg[0], {**reg[1], "decorator": ann(d), "at": f"{file}:{d.lineno}"})
|
|
291
|
+
rv = self._recv_var(de, ctx)
|
|
292
|
+
if rv:
|
|
293
|
+
self.var_regs.setdefault(rv, []).append(target)
|
|
294
|
+
elif ctx.func is not None:
|
|
295
|
+
self.registrars.setdefault(ctx.func.id, (ctx.func, reg[0], {**reg[1], "decorator": ann(d)}))
|
|
296
|
+
t = p.infer(de, ctx)
|
|
297
|
+
dfn = self.func_of(t)
|
|
298
|
+
if dfn is not None:
|
|
299
|
+
self.b.add_edge(owner_id, dfn.id, "CALLS", file, d.lineno, EXACT if t[0] == "func" else RESOLVED, via="decorator")
|
|
300
|
+
self.stats["decorator_calls"] += 1
|
|
301
|
+
if target is not None:
|
|
302
|
+
self.ref(dfn.id, target, "decorator", file, d.lineno)
|
|
303
|
+
self.decorated_by.setdefault(dfn.id, []).append(target)
|
|
304
|
+
return
|
|
305
|
+
if target is None:
|
|
306
|
+
return
|
|
307
|
+
if isinstance(de, ast.Attribute):
|
|
308
|
+
g = self.func_of(p.infer(de.value, ctx))
|
|
309
|
+
if g is not None: # @group.command() / @dispatcher.register: the function object holds the target
|
|
310
|
+
self.ref(g.id, target, "decorator", file, d.lineno, via=de.attr)
|
|
311
|
+
return
|
|
312
|
+
if reg:
|
|
313
|
+
return
|
|
314
|
+
name = de.attr if isinstance(de, ast.Attribute) else de.id if isinstance(de, ast.Name) else ""
|
|
315
|
+
has_recv = isinstance(de, ast.Attribute)
|
|
316
|
+
if (has_recv and (name in ROUTE_DECOS or name in REGISTER_DECOS or name.startswith("register"))) or \
|
|
317
|
+
(not has_recv and name in REGISTER_DECOS and t is not None):
|
|
318
|
+
if "mock" in (dotted(de) or "").split(".") or (dotted(de) or "").startswith("patch"):
|
|
319
|
+
return
|
|
320
|
+
self.ref(owner_id, target, "decorator", file, d.lineno, HEURISTIC, decorator=ann(d), registry="external")
|
|
321
|
+
|
|
322
|
+
# ---- scope walk
|
|
323
|
+
def scan(self, owner_id, roots, ctx, file, module_scope: bool, calls: list | None = None):
|
|
324
|
+
"""roots: [(node, parent)] or [(node, parent, refs)]. Module scope stops at def bodies (decorators and defaults
|
|
325
|
+
stay); function scope walks like `walk_body` (nested defs collapse into the owner, nested classes do not), and
|
|
326
|
+
appends every Call to `calls` in walk_body order. refs=False: walk for calls only (a def's own decorators and
|
|
327
|
+
defaults belong to the enclosing scope)."""
|
|
328
|
+
p, names = self.prog, self.names
|
|
329
|
+
Name, Attribute, Load, Call = ast.Name, ast.Attribute, ast.Load, ast.Call
|
|
330
|
+
defs = (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
331
|
+
stack = [r if len(r) == 3 else (r[0], r[1], True) for r in roots]
|
|
332
|
+
while stack:
|
|
333
|
+
n, par, on = stack.pop()
|
|
334
|
+
t = type(n)
|
|
335
|
+
if t is Name or t is Attribute:
|
|
336
|
+
if t is Attribute:
|
|
337
|
+
stack.append((n.value, n, on))
|
|
338
|
+
if not on or type(n.ctx) is not Load or (n.id if t is Name else n.attr) not in names:
|
|
339
|
+
continue
|
|
340
|
+
how = self._how(n, par)
|
|
341
|
+
if how:
|
|
342
|
+
f = self.value_ref(p.infer(n, ctx))
|
|
343
|
+
if f is not None:
|
|
344
|
+
self.ref(owner_id, f, how, file, n.lineno)
|
|
345
|
+
if how == "callback" and type(par) is Call and par.args and par.args[0] is n:
|
|
346
|
+
self.passed.append((par, ctx, f))
|
|
347
|
+
continue
|
|
348
|
+
if t in defs:
|
|
349
|
+
if module_scope:
|
|
350
|
+
tgt = self.fn_by_node.get(id(n))
|
|
351
|
+
for dd in n.decorator_list:
|
|
352
|
+
self.decorator(dd, tgt, owner_id, ctx, file)
|
|
353
|
+
stack.append((dd, n, on))
|
|
354
|
+
stack.extend((x, n.args, on) for x in n.args.defaults + n.args.kw_defaults if x is not None)
|
|
355
|
+
continue
|
|
356
|
+
if on:
|
|
357
|
+
for dd in n.decorator_list:
|
|
358
|
+
self.decorator(dd, None, owner_id, ctx, file)
|
|
359
|
+
stack.extend((c, n, on) for c in ast.iter_child_nodes(n))
|
|
360
|
+
continue
|
|
361
|
+
if t is ast.ClassDef:
|
|
362
|
+
if module_scope:
|
|
363
|
+
cctx = self.Ctx(ctx.mod, None, self.cls_by_node.get(id(n)))
|
|
364
|
+
inner = n.decorator_list + n.bases + [k.value for k in n.keywords] + n.body
|
|
365
|
+
self.scan(owner_id, [(x, n) for x in inner], cctx, file, True)
|
|
366
|
+
continue
|
|
367
|
+
if t is Call:
|
|
368
|
+
if calls is not None:
|
|
369
|
+
calls.append(n)
|
|
370
|
+
if on:
|
|
371
|
+
self._registration_call(n, owner_id, ctx, file)
|
|
372
|
+
stack.extend((c, n, on) for c in ast.iter_child_nodes(n))
|
|
373
|
+
|
|
374
|
+
@staticmethod
|
|
375
|
+
def _how(n, par):
|
|
376
|
+
"""How the function-valued expression `n` is used by its parent node (None: not a reference)."""
|
|
377
|
+
tp = type(par)
|
|
378
|
+
if tp in (ast.List, ast.Tuple, ast.Set, ast.Dict, ast.Starred):
|
|
379
|
+
return "collection"
|
|
380
|
+
if tp is ast.Call:
|
|
381
|
+
if par.func is n:
|
|
382
|
+
return None
|
|
383
|
+
if isinstance(par.func, ast.Attribute) and par.func.attr in COLLECTION_ADDS:
|
|
384
|
+
return "collection" # HANDLERS.append(f), REGISTRY.setdefault(k, f)
|
|
385
|
+
return "callback"
|
|
386
|
+
if tp is ast.keyword:
|
|
387
|
+
return "callback"
|
|
388
|
+
if tp in (ast.Assign, ast.AnnAssign, ast.NamedExpr, ast.Return, ast.Yield):
|
|
389
|
+
return "assignment" if par.value is n else None
|
|
390
|
+
if tp is ast.Lambda:
|
|
391
|
+
return "assignment" if par.body is n else None
|
|
392
|
+
if tp is ast.arguments:
|
|
393
|
+
return "assignment"
|
|
394
|
+
if tp is ast.IfExp:
|
|
395
|
+
return "assignment" if par.test is not n else None
|
|
396
|
+
if tp is ast.BoolOp:
|
|
397
|
+
return "assignment"
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
def _registration_call(self, call, owner_id, ctx, file):
|
|
401
|
+
"""`mcp.tool()(fn)`, `mcp.add_tool(fn)`, `app.command()(fn)`: fn (or the closure that wraps the owner's
|
|
402
|
+
argument) becomes an entry point."""
|
|
403
|
+
fn = call.func
|
|
404
|
+
inner = fn.func if isinstance(fn, ast.Call) else fn if isinstance(fn, ast.Attribute) and fn.attr in MCP_ADDERS else None
|
|
405
|
+
if inner is None or not call.args:
|
|
406
|
+
return
|
|
407
|
+
reg = self.registration(inner, ctx)
|
|
408
|
+
if not reg:
|
|
409
|
+
return
|
|
410
|
+
a = call.args[0]
|
|
411
|
+
f = self.func_of(self.prog.infer(a, ctx))
|
|
412
|
+
attrs = {**reg[1], "call": ann(call)[:120], "at": f"{file}:{call.lineno}"}
|
|
413
|
+
if f is not None:
|
|
414
|
+
self.set_entry(f, reg[0], attrs)
|
|
415
|
+
elif ctx.func is not None and isinstance(a, (ast.Name, ast.Lambda)):
|
|
416
|
+
self.registrars.setdefault(ctx.func.id, (ctx.func, reg[0], attrs))
|
|
417
|
+
|
|
418
|
+
def object_calls(self, sid, stmts, ctx, file):
|
|
419
|
+
"""`app()` / `cli()` in a script body where `app` is a module-level object (typer app, click group object)."""
|
|
420
|
+
for st in stmts:
|
|
421
|
+
if isinstance(st, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
422
|
+
continue
|
|
423
|
+
for sub in ast.walk(st):
|
|
424
|
+
if isinstance(sub, ast.Call) and isinstance(sub.func, (ast.Name, ast.Attribute)):
|
|
425
|
+
e = sub.func
|
|
426
|
+
if isinstance(e, ast.Name):
|
|
427
|
+
r = self.prog.resolve_name(ctx.mod, e.id)
|
|
428
|
+
else:
|
|
429
|
+
base = self.prog.infer(e.value, ctx)
|
|
430
|
+
r = self.prog.lookup(base[1].name, e.attr) if base and base[0] == "mod" else None
|
|
431
|
+
if r and r[0] == "var":
|
|
432
|
+
self.obj_calls.append((sid, (r[1].name, r[2]), file, sub.lineno))
|
|
433
|
+
|
|
434
|
+
def finish(self):
|
|
435
|
+
"""Registering wrappers: every function decorated with (or passed to) one is registered; a wrapper with no
|
|
436
|
+
such use is itself the registered handler (its closure collapses into it)."""
|
|
437
|
+
p = self.prog
|
|
438
|
+
for sid, key, file, line in self.obj_calls:
|
|
439
|
+
for g in self.var_regs.get(key, []):
|
|
440
|
+
self.ref(sid, g, "callback", file, line, via="registered command")
|
|
441
|
+
for fid, (F, kind, attrs) in self.registrars.items():
|
|
442
|
+
targets = list(self.decorated_by.get(fid, []))
|
|
443
|
+
for call, ctx, g in self.passed:
|
|
444
|
+
if any(t is F for t, _c, _v in p.resolve_call(call, ctx)):
|
|
445
|
+
targets.append(g)
|
|
446
|
+
if targets:
|
|
447
|
+
for g in targets:
|
|
448
|
+
self.set_entry(g, kind, {**attrs, "registered_by": F.qual})
|
|
449
|
+
else:
|
|
450
|
+
self.set_entry(F, kind, {**attrs, "closure": True})
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
PROPERTY_DECOS = {"property", "cached_property", "setter", "getter", "deleter", "abstractproperty", "hybrid_property",
|
|
454
|
+
"classproperty", "lazy_property", "reify", "computed_field"}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def is_property(f) -> bool:
|
|
458
|
+
cached = getattr(f, "_is_property", None)
|
|
459
|
+
if cached is None:
|
|
460
|
+
cached = False
|
|
461
|
+
for d in f.decorators:
|
|
462
|
+
name = (dotted(d.func if isinstance(d, ast.Call) else d) or "").rsplit(".", 1)[-1]
|
|
463
|
+
if name in PROPERTY_DECOS or name.endswith("_property"):
|
|
464
|
+
cached = True
|
|
465
|
+
break
|
|
466
|
+
f._is_property = cached
|
|
467
|
+
return cached
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def ann(e) -> str:
|
|
471
|
+
try:
|
|
472
|
+
return ast.unparse(e)
|
|
473
|
+
except Exception: # noqa: BLE001
|
|
474
|
+
return ""
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def dotted(e) -> str | None:
|
|
478
|
+
parts = []
|
|
479
|
+
while isinstance(e, ast.Attribute):
|
|
480
|
+
parts.append(e.attr)
|
|
481
|
+
e = e.value
|
|
482
|
+
if isinstance(e, ast.Name):
|
|
483
|
+
parts.append(e.id)
|
|
484
|
+
return ".".join(reversed(parts))
|
|
485
|
+
return None
|