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
|
File without changes
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Django settings, admin, signals, Celery, Channels dispatch and management commands (functions taking the
|
|
2
|
+
DjangoPlugin instance `p`; split out of plugin.py to keep the url/route code readable)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
|
|
7
|
+
from ...core.model import EXACT, HEURISTIC, RESOLVED
|
|
8
|
+
from ..python.plugin import Ctx, PythonPlugin, ann_text, const_str, dotted, kwarg, walk_body
|
|
9
|
+
|
|
10
|
+
MODEL_SIGNALS = {"pre_save": "save", "post_save": "save", "pre_delete": "delete", "post_delete": "delete"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def mod_of(path):
|
|
14
|
+
return ("/".join(path.split("/")[:-1]) or None) if path else None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ------------------------------------------------------------------ settings
|
|
18
|
+
def settings_modules(p):
|
|
19
|
+
prog = p.prog
|
|
20
|
+
names = []
|
|
21
|
+
for m in prog.modules.values():
|
|
22
|
+
for sub in ast.walk(m.tree):
|
|
23
|
+
if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute) and sub.func.attr == "setdefault" \
|
|
24
|
+
and len(sub.args) >= 2 and const_str(sub.args[0]) == "DJANGO_SETTINGS_MODULE" and const_str(sub.args[1]):
|
|
25
|
+
names.append(const_str(sub.args[1]))
|
|
26
|
+
mods = []
|
|
27
|
+
for n in names:
|
|
28
|
+
m = prog.module(n)
|
|
29
|
+
if m and m not in mods:
|
|
30
|
+
mods.append(m)
|
|
31
|
+
if not mods:
|
|
32
|
+
mods = [m for m in prog.modules.values() if m.name.split(".")[-1] == "settings" or
|
|
33
|
+
(len(m.name.split(".")) > 1 and m.name.split(".")[-2] == "settings")]
|
|
34
|
+
out, stack = [], list(mods)
|
|
35
|
+
while stack:
|
|
36
|
+
m = stack.pop(0)
|
|
37
|
+
if m in out:
|
|
38
|
+
continue
|
|
39
|
+
out.append(m)
|
|
40
|
+
for s in m.star:
|
|
41
|
+
sm = prog.module(s)
|
|
42
|
+
if sm:
|
|
43
|
+
stack.append(sm)
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def settings(p) -> dict:
|
|
48
|
+
prog, b = p.prog, p.b
|
|
49
|
+
p.settings_mods = settings_modules(p)
|
|
50
|
+
p.settings_values = {}
|
|
51
|
+
n = 0
|
|
52
|
+
for m in p.settings_mods:
|
|
53
|
+
for stt in m.tree.body:
|
|
54
|
+
tgts = []
|
|
55
|
+
if isinstance(stt, ast.Assign):
|
|
56
|
+
tgts = [t.id for t in stt.targets if isinstance(t, ast.Name)]
|
|
57
|
+
elif isinstance(stt, ast.AnnAssign) and isinstance(stt.target, ast.Name):
|
|
58
|
+
tgts = [stt.target.id]
|
|
59
|
+
for k in tgts:
|
|
60
|
+
if not k.isupper():
|
|
61
|
+
continue
|
|
62
|
+
cid = b.add_node("config", f"settings.{k}", name=f"settings.{k}", fqn=f"settings.{k}", file=m.file,
|
|
63
|
+
line=stt.lineno, module="settings", lang="python")
|
|
64
|
+
p.settings_values.setdefault(k, []).append((m, stt.value))
|
|
65
|
+
n += 1
|
|
66
|
+
ctx = Ctx(m, None, None)
|
|
67
|
+
for sub in (ast.walk(stt.value) if stt.value is not None else []):
|
|
68
|
+
if isinstance(sub, ast.Call):
|
|
69
|
+
r = PythonPlugin.env_key(prog, sub, ctx)
|
|
70
|
+
if r:
|
|
71
|
+
b.add_edge(cid, b.add_node("env", r[0], lang="env"), "READS_ENV", m.file, sub.lineno, EXACT, via=r[1])
|
|
72
|
+
elif isinstance(sub, ast.Subscript) and const_str(sub.slice):
|
|
73
|
+
t = prog.infer(sub.value, ctx)
|
|
74
|
+
if t and t[0] == "ext" and t[1] == "os.environ":
|
|
75
|
+
b.add_edge(cid, b.add_node("env", const_str(sub.slice), lang="env"), "READS_ENV", m.file, sub.lineno, EXACT, via="os.environ[]")
|
|
76
|
+
elif isinstance(sub, ast.Name) and sub.id.isupper() and sub.id != k:
|
|
77
|
+
b.add_edge(cid, f"config:settings.{sub.id}", "REFERS_TO", m.file, sub.lineno, RESOLVED)
|
|
78
|
+
return {"modules": [m.name for m in p.settings_mods], "keys": n}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def settings_reads(p) -> int:
|
|
82
|
+
prog, b = p.prog, p.b
|
|
83
|
+
n = 0
|
|
84
|
+
smods = {m.name for m in p.settings_mods}
|
|
85
|
+
for f in prog.funcs.values():
|
|
86
|
+
ctx = Ctx(f.module, f, f.cls)
|
|
87
|
+
for sub in walk_body(f.node):
|
|
88
|
+
key = None
|
|
89
|
+
if isinstance(sub, ast.Attribute) and sub.attr.isupper() and isinstance(sub.value, (ast.Name, ast.Attribute)):
|
|
90
|
+
t = prog.infer(sub.value, ctx)
|
|
91
|
+
if t and ((t[0] == "ext" and t[1] == "django.conf.settings") or (t[0] == "mod" and t[1].name in smods)):
|
|
92
|
+
key = sub.attr
|
|
93
|
+
elif isinstance(sub, ast.Call) and isinstance(sub.func, ast.Name) and sub.func.id in ("getattr", "hasattr") and len(sub.args) >= 2:
|
|
94
|
+
t = prog.infer(sub.args[0], ctx)
|
|
95
|
+
if t and t[0] == "ext" and t[1] == "django.conf.settings" and const_str(sub.args[1]):
|
|
96
|
+
key = const_str(sub.args[1])
|
|
97
|
+
if key:
|
|
98
|
+
cid = f"config:settings.{key}"
|
|
99
|
+
if cid not in b.nodes:
|
|
100
|
+
b.add_node("config", f"settings.{key}", name=f"settings.{key}", lang="python", attrs={"not_in_settings": True})
|
|
101
|
+
b.add_edge(f.id, cid, "READS_CONFIG", f.file, sub.lineno, EXACT)
|
|
102
|
+
n += 1
|
|
103
|
+
return n
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ------------------------------------------------------------------ admin
|
|
107
|
+
def admin(p) -> dict:
|
|
108
|
+
prog, b = p.prog, p.b
|
|
109
|
+
regs = []
|
|
110
|
+
for m in prog.modules.values():
|
|
111
|
+
for sub in ast.walk(m.tree):
|
|
112
|
+
if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute) and sub.func.attr == "register" and sub.args:
|
|
113
|
+
base = ann_text(sub.func.value) or ""
|
|
114
|
+
t = prog.infer(sub.func.value, Ctx(m, None, None))
|
|
115
|
+
is_site = base.endswith("site") or (t and t[0] in ("ext", "einst") and "admin" in t[1]) or \
|
|
116
|
+
(t and t[0] == "inst" and prog.subclass_of(t[1], "AdminSite"))
|
|
117
|
+
if not is_site:
|
|
118
|
+
continue
|
|
119
|
+
targets = sub.args[0].elts if isinstance(sub.args[0], (ast.List, ast.Tuple)) else [sub.args[0]]
|
|
120
|
+
for tg in targets:
|
|
121
|
+
regs.append((m, tg, sub.args[1] if len(sub.args) > 1 else None, sub.lineno))
|
|
122
|
+
for c in m.all_classes:
|
|
123
|
+
for d in c.decorators:
|
|
124
|
+
dn = dotted(d.func) if isinstance(d, ast.Call) else None
|
|
125
|
+
if dn and dn.split(".")[-1] == "register" and ("admin" in dn or dn == "register"):
|
|
126
|
+
for tg in d.args:
|
|
127
|
+
regs.append((m, tg, ("cls", c), d.lineno))
|
|
128
|
+
n = 0
|
|
129
|
+
for m, tg, adm, line in regs:
|
|
130
|
+
mt = prog.infer(tg, Ctx(m, None, None))
|
|
131
|
+
model = mt[1] if mt and mt[0] == "type" else None
|
|
132
|
+
ac = adm[1] if isinstance(adm, tuple) else None
|
|
133
|
+
if adm is not None and ac is None:
|
|
134
|
+
at = prog.infer(adm, Ctx(m, None, None))
|
|
135
|
+
ac = at[1] if at and at[0] == "type" else None
|
|
136
|
+
key = ac.qual if ac else (model.qual if model else ann_text(tg))
|
|
137
|
+
aid = b.add_node("admin", key, name=(ac.name if ac else f"admin:{model.name if model else key}"), fqn=key,
|
|
138
|
+
file=m.file, line=line, module=mod_of(m.file), lang="python", entry_kind="admin_panel",
|
|
139
|
+
attrs={"model": model.qual if model else None, "framework": "django-admin"})
|
|
140
|
+
n += 1
|
|
141
|
+
tbl = p.models.table(model.qual) if model else None
|
|
142
|
+
if tbl:
|
|
143
|
+
b.add_edge(aid, f"table:{tbl}", "READS_TABLE", m.file, line, RESOLVED, via="django-admin")
|
|
144
|
+
b.add_edge(aid, f"table:{tbl}", "WRITES_TABLE", m.file, line, RESOLVED, via="django-admin")
|
|
145
|
+
if not ac:
|
|
146
|
+
continue
|
|
147
|
+
for f in ac.methods.values():
|
|
148
|
+
b.add_edge(aid, f.id, "HANDLED_BY", f.file, f.line, RESOLVED, via="admin-surface")
|
|
149
|
+
acts = ac.attrs.get("actions")
|
|
150
|
+
if acts and isinstance(acts[0], (ast.List, ast.Tuple)):
|
|
151
|
+
for x in acts[0].elts:
|
|
152
|
+
t = prog.infer(x, Ctx(ac.module, None, ac))
|
|
153
|
+
if t and t[0] in ("func", "bound"):
|
|
154
|
+
b.add_edge(aid, t[1].id, "HANDLED_BY", ac.file, acts[1], RESOLVED, via="admin-action")
|
|
155
|
+
inl = ac.attrs.get("inlines")
|
|
156
|
+
if inl and isinstance(inl[0], (ast.List, ast.Tuple)):
|
|
157
|
+
for x in inl[0].elts:
|
|
158
|
+
t = prog.infer(x, Ctx(ac.module, None, ac))
|
|
159
|
+
if t and t[0] == "type":
|
|
160
|
+
mm = p.class_attr_expr(t[1], "model")
|
|
161
|
+
if mm:
|
|
162
|
+
t2 = prog.infer(mm[0], Ctx(mm[1].module, None, mm[1]))
|
|
163
|
+
if t2 and t2[0] == "type" and p.models.table(t2[1].qual):
|
|
164
|
+
b.add_edge(aid, f"table:{p.models.table(t2[1].qual)}", "WRITES_TABLE", ac.file, inl[1], RESOLVED, via="admin-inline")
|
|
165
|
+
return {"registrations": n}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ------------------------------------------------------------------ signals
|
|
169
|
+
def signal_key(p, m, e, custom, only_custom=False):
|
|
170
|
+
prog = p.prog
|
|
171
|
+
if isinstance(e, ast.Name):
|
|
172
|
+
r = prog.resolve_name(m, e.id)
|
|
173
|
+
if r and r[0] == "var" and (r[1].name, r[2]) in custom:
|
|
174
|
+
return f"{r[1].name}.{r[2]}"
|
|
175
|
+
if isinstance(e, ast.Attribute):
|
|
176
|
+
t0 = prog.infer(e.value, Ctx(m, None, None))
|
|
177
|
+
if t0 and t0[0] == "mod" and (t0[1].name, e.attr) in custom:
|
|
178
|
+
return f"{t0[1].name}.{e.attr}"
|
|
179
|
+
if only_custom:
|
|
180
|
+
return None
|
|
181
|
+
t = prog.infer(e, Ctx(m, None, None))
|
|
182
|
+
if t and t[0] == "ext":
|
|
183
|
+
return t[1]
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def signals(p) -> dict:
|
|
188
|
+
prog, b = p.prog, p.b
|
|
189
|
+
custom = {}
|
|
190
|
+
for m in prog.modules.values():
|
|
191
|
+
for name, vals in m.vars.items():
|
|
192
|
+
for v, line, _ in vals:
|
|
193
|
+
if isinstance(v, ast.Call):
|
|
194
|
+
t = prog.infer(v.func, Ctx(m, None, None))
|
|
195
|
+
if t and t[0] == "ext" and t[1].split(".")[-1] in ("Signal", "ModelSignal"):
|
|
196
|
+
custom[(m.name, name)] = (m, line)
|
|
197
|
+
conns = []
|
|
198
|
+
for f in prog.funcs.values():
|
|
199
|
+
for d in f.decorators:
|
|
200
|
+
if isinstance(d, ast.Call) and (dotted(d.func) or "").split(".")[-1] == "receiver" and d.args:
|
|
201
|
+
sigs = d.args[0].elts if isinstance(d.args[0], (ast.List, ast.Tuple)) else [d.args[0]]
|
|
202
|
+
for s in sigs:
|
|
203
|
+
conns.append((f.module, s, f, kwarg(d, "sender"), d.lineno, EXACT))
|
|
204
|
+
for m in prog.modules.values():
|
|
205
|
+
for sub in ast.walk(m.tree):
|
|
206
|
+
if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute) and sub.func.attr == "connect" and sub.args:
|
|
207
|
+
ht = prog.infer(sub.args[0], Ctx(m, None, None))
|
|
208
|
+
if ht and ht[0] in ("func", "bound"):
|
|
209
|
+
conns.append((m, sub.func.value, ht[1], kwarg(sub, "sender"), sub.lineno, RESOLVED))
|
|
210
|
+
n = 0
|
|
211
|
+
writes = [e for e in b.edges.values() if e.kind == "WRITES_TABLE"]
|
|
212
|
+
for m, sig, h, sender, line, conf in conns:
|
|
213
|
+
sk = signal_key(p, m, sig, custom)
|
|
214
|
+
if not sk or ("signal" not in sk and not any(sk == f"{a}.{c}" for a, c in custom)):
|
|
215
|
+
continue
|
|
216
|
+
sname, sender_q = sk, None
|
|
217
|
+
if sender is not None:
|
|
218
|
+
st = prog.infer(sender, Ctx(m, None, None))
|
|
219
|
+
if st and st[0] == "type":
|
|
220
|
+
sender_q = st[1].qual
|
|
221
|
+
sname = f"{sk}[{st[1].name}]"
|
|
222
|
+
elif const_str(sender):
|
|
223
|
+
sname = f"{sk}[{const_str(sender)}]"
|
|
224
|
+
eid = b.add_node("event", sname, name=sname, fqn=sname, lang="python", attrs={"signal": sk, "sender": sender_q})
|
|
225
|
+
b.add_edge(eid, h.id, "LISTENED_BY", m.file, line, conf)
|
|
226
|
+
lid = b.add_node("listener", h.qual, name=h.name, fqn=h.qual, file=h.file, line=h.line, module=mod_of(h.file),
|
|
227
|
+
lang="python", entry_kind="listener", attrs={"signal": sname})
|
|
228
|
+
b.add_edge(lid, h.id, "HANDLED_BY", h.file, h.line, EXACT)
|
|
229
|
+
n += 1
|
|
230
|
+
short = sk.split(".")[-1]
|
|
231
|
+
if sender_q and short in MODEL_SIGNALS and p.models.table(sender_q):
|
|
232
|
+
tid = f"table:{p.models.table(sender_q)}"
|
|
233
|
+
want = MODEL_SIGNALS[short]
|
|
234
|
+
fire = {"save", "create", "get_or_create", "update_or_create"} if want == "save" else {"delete"}
|
|
235
|
+
for e in writes:
|
|
236
|
+
via = (e.attrs.get("via") or "").split(".")[-1]
|
|
237
|
+
if e.dst == tid and (via in fire or (via.startswith("a") and via[1:] in fire)):
|
|
238
|
+
b.add_edge(e.src, eid, "DISPATCHES", e.file, e.line, RESOLVED, via=f"{e.attrs.get('via')} fires {short}")
|
|
239
|
+
for f in prog.funcs.values():
|
|
240
|
+
for sub in walk_body(f.node):
|
|
241
|
+
if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute) and sub.func.attr in ("send", "send_robust", "asend", "asend_robust"):
|
|
242
|
+
sk = signal_key(p, f.module, sub.func.value, custom, only_custom=True)
|
|
243
|
+
if sk:
|
|
244
|
+
for eid in [x for x in b.nodes if x == f"event:{sk}" or x.startswith(f"event:{sk}[")]:
|
|
245
|
+
b.add_edge(f.id, eid, "DISPATCHES", f.file, sub.lineno, RESOLVED, via="Signal.send")
|
|
246
|
+
return {"receivers": n, "custom_signals": len(custom)}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ------------------------------------------------------------------ celery
|
|
250
|
+
def celery(p) -> dict:
|
|
251
|
+
prog, b = p.prog, p.b
|
|
252
|
+
tasks, by_name = {}, {}
|
|
253
|
+
for f in prog.funcs.values():
|
|
254
|
+
for d in f.decorators:
|
|
255
|
+
dn = dotted(d.func if isinstance(d, ast.Call) else d) or ""
|
|
256
|
+
last = dn.split(".")[-1]
|
|
257
|
+
if last in ("shared_task", "periodic_task", "db_task", "db_periodic_task") or (last == "task" and dn != "task") or dn == "task" and "celery" in str(f.module.imports.get("task", "")):
|
|
258
|
+
tname = (const_str(kwarg(d, "name")) if isinstance(d, ast.Call) else None) or f.qual
|
|
259
|
+
jid = b.add_node("job", f.qual, name=f.name, fqn=f.qual, file=f.file, line=f.line, module=mod_of(f.file),
|
|
260
|
+
lang="python", entry_kind="queue_job", attrs={"task_name": tname, "decorator": dn})
|
|
261
|
+
b.add_edge(jid, f.id, "HANDLED_BY", f.file, f.line, EXACT)
|
|
262
|
+
tasks[f.qual] = jid
|
|
263
|
+
by_name[tname] = jid
|
|
264
|
+
if last in ("periodic_task", "db_periodic_task"):
|
|
265
|
+
sid = b.add_node("schedule", f"{f.qual}@{f.file}:{d.lineno}", name=f"schedule {f.name}", file=f.file,
|
|
266
|
+
line=d.lineno, lang="python", entry_kind="scheduled")
|
|
267
|
+
b.add_edge(sid, jid, "SCHEDULES", f.file, d.lineno, EXACT)
|
|
268
|
+
break
|
|
269
|
+
for c in prog.classes.values():
|
|
270
|
+
if prog.subclass_of(c, "celery.Task", "celery.app.task.Task") and "run" in c.methods:
|
|
271
|
+
f = c.methods["run"]
|
|
272
|
+
jid = b.add_node("job", c.qual, name=c.name, fqn=c.qual, file=c.file, line=c.line, lang="python", entry_kind="queue_job")
|
|
273
|
+
b.add_edge(jid, f.id, "HANDLED_BY", f.file, f.line, EXACT)
|
|
274
|
+
tasks[c.qual] = jid
|
|
275
|
+
n_d = 0
|
|
276
|
+
for f in prog.funcs.values():
|
|
277
|
+
ctx = Ctx(f.module, f, f.cls)
|
|
278
|
+
for sub in walk_body(f.node):
|
|
279
|
+
if not (isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute)):
|
|
280
|
+
continue
|
|
281
|
+
a = sub.func.attr
|
|
282
|
+
if a in ("delay", "apply_async", "s", "si", "signature", "apply", "map", "starmap", "chunks", "delay_on_commit"):
|
|
283
|
+
t = prog.infer(sub.func.value, ctx)
|
|
284
|
+
q = t[1].qual if t and t[0] in ("func", "type") else None
|
|
285
|
+
if q in tasks:
|
|
286
|
+
b.add_edge(f.id, tasks[q], "DISPATCHES", f.file, sub.lineno, EXACT if t[0] == "func" else RESOLVED, via=f".{a}()")
|
|
287
|
+
n_d += 1
|
|
288
|
+
elif a == "send_task" and sub.args and const_str(sub.args[0]) in by_name:
|
|
289
|
+
b.add_edge(f.id, by_name[const_str(sub.args[0])], "DISPATCHES", f.file, sub.lineno, RESOLVED, via="send_task")
|
|
290
|
+
n_d += 1
|
|
291
|
+
n_s = 0
|
|
292
|
+
for m in prog.modules.values():
|
|
293
|
+
for sub in ast.walk(m.tree):
|
|
294
|
+
dct = None
|
|
295
|
+
if isinstance(sub, ast.Assign):
|
|
296
|
+
tn = ann_text(sub.targets[0]) or ""
|
|
297
|
+
if tn in ("CELERY_BEAT_SCHEDULE", "CELERYBEAT_SCHEDULE", "beat_schedule") or tn.endswith(".beat_schedule"):
|
|
298
|
+
dct = sub.value
|
|
299
|
+
elif isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute) and sub.func.attr == "update":
|
|
300
|
+
dct = kwarg(sub, "beat_schedule")
|
|
301
|
+
if isinstance(dct, ast.Dict):
|
|
302
|
+
for k, v in zip(dct.keys, dct.values):
|
|
303
|
+
if not isinstance(v, ast.Dict):
|
|
304
|
+
continue
|
|
305
|
+
for k2, v2 in zip(v.keys, v.values):
|
|
306
|
+
if const_str(k2) == "task" and const_str(v2):
|
|
307
|
+
tn2 = const_str(v2)
|
|
308
|
+
jid = by_name.get(tn2) or tasks.get(tn2) or b.add_node("job", tn2, lang="python", attrs={"unresolved_task": True})
|
|
309
|
+
sid = b.add_node("schedule", f"{const_str(k) or tn2}@{m.file}:{v.lineno}", name=f"beat {const_str(k) or tn2}",
|
|
310
|
+
file=m.file, line=v.lineno, lang="python", entry_kind="scheduled")
|
|
311
|
+
b.add_edge(sid, jid, "SCHEDULES", m.file, v2.lineno, EXACT if (tn2 in by_name or tn2 in tasks) else HEURISTIC)
|
|
312
|
+
n_s += 1
|
|
313
|
+
if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute) and sub.func.attr == "add_periodic_task" and len(sub.args) >= 2:
|
|
314
|
+
sig = sub.args[1]
|
|
315
|
+
if isinstance(sig, ast.Call) and isinstance(sig.func, ast.Attribute):
|
|
316
|
+
t = prog.infer(sig.func.value, Ctx(m, None, None))
|
|
317
|
+
if t and t[0] == "func" and t[1].qual in tasks:
|
|
318
|
+
sid = b.add_node("schedule", f"{t[1].qual}@{m.file}:{sub.lineno}", name=f"periodic {t[1].name}", file=m.file,
|
|
319
|
+
line=sub.lineno, lang="python", entry_kind="scheduled")
|
|
320
|
+
b.add_edge(sid, tasks[t[1].qual], "SCHEDULES", m.file, sub.lineno, EXACT)
|
|
321
|
+
n_s += 1
|
|
322
|
+
return {"tasks": len(tasks), "dispatches": n_d, "schedules": n_s}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def group_sends(p) -> int:
|
|
326
|
+
"""channel_layer.group_send(group, {"type": "chat.message"}) -> consumer method chat_message."""
|
|
327
|
+
from .plugin import CONSUMER_BASES
|
|
328
|
+
prog, b = p.prog, p.b
|
|
329
|
+
cons = {}
|
|
330
|
+
for c in prog.classes.values():
|
|
331
|
+
if prog.subclass_of(c, *CONSUMER_BASES):
|
|
332
|
+
for mn, mf in c.methods.items():
|
|
333
|
+
cons.setdefault(mn, []).append(mf)
|
|
334
|
+
n = 0
|
|
335
|
+
for f in prog.funcs.values():
|
|
336
|
+
for sub in walk_body(f.node):
|
|
337
|
+
if isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute) and sub.func.attr in ("group_send", "send") \
|
|
338
|
+
and len(sub.args) >= 2 and isinstance(sub.args[1], ast.Dict):
|
|
339
|
+
for k, v in zip(sub.args[1].keys, sub.args[1].values):
|
|
340
|
+
if const_str(k) == "type" and const_str(v):
|
|
341
|
+
hn = const_str(v).replace(".", "_")
|
|
342
|
+
for hf in cons.get(hn, []):
|
|
343
|
+
b.add_edge(f.id, hf.id, "DISPATCHES", f.file, sub.lineno, RESOLVED if len(cons[hn]) == 1 else HEURISTIC,
|
|
344
|
+
via=f"channel_layer.{sub.func.attr} type={const_str(v)}")
|
|
345
|
+
n += 1
|
|
346
|
+
return n
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ------------------------------------------------------------------ management commands
|
|
350
|
+
def commands(p) -> dict:
|
|
351
|
+
prog, b = p.prog, p.b
|
|
352
|
+
cmds = {}
|
|
353
|
+
for m in prog.modules.values():
|
|
354
|
+
parts = m.file.split("/")
|
|
355
|
+
if len(parts) >= 3 and parts[-2] == "commands" and parts[-3] == "management" and not parts[-1].startswith("_"):
|
|
356
|
+
c = m.classes.get("Command")
|
|
357
|
+
if not c:
|
|
358
|
+
continue
|
|
359
|
+
name = parts[-1][:-3]
|
|
360
|
+
cid = b.add_node("command", name, name=name, fqn=c.qual, file=m.file, line=c.line, module=mod_of(m.file),
|
|
361
|
+
lang="python", entry_kind="management_command",
|
|
362
|
+
doc=const_str((c.attrs.get("help") or (None,))[0]), attrs={"framework": "django"})
|
|
363
|
+
h = prog.find_method(c, "handle")
|
|
364
|
+
if h:
|
|
365
|
+
b.add_edge(cid, h.id, "HANDLED_BY", h.file, h.line, EXACT)
|
|
366
|
+
cmds[name] = cid
|
|
367
|
+
n = 0
|
|
368
|
+
for f in prog.funcs.values():
|
|
369
|
+
ctx = Ctx(f.module, f, f.cls)
|
|
370
|
+
for sub in walk_body(f.node):
|
|
371
|
+
if isinstance(sub, ast.Call) and sub.args:
|
|
372
|
+
t = prog.infer(sub.func, ctx)
|
|
373
|
+
if t and t[0] == "ext" and t[1].endswith("call_command") and const_str(sub.args[0]):
|
|
374
|
+
nm = const_str(sub.args[0])
|
|
375
|
+
b.add_edge(f.id, cmds.get(nm) or b.add_node("command", nm, lang="python"), "DISPATCHES", f.file, sub.lineno,
|
|
376
|
+
EXACT if nm in cmds else HEURISTIC, via="call_command")
|
|
377
|
+
n += 1
|
|
378
|
+
return {"commands": len(cmds), "call_command": n}
|