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.
Files changed (174) hide show
  1. cg_code_graph-0.10.1.dist-info/METADATA +678 -0
  2. cg_code_graph-0.10.1.dist-info/RECORD +174 -0
  3. cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
  4. cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
  5. cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
  6. cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
  7. codegraph/__init__.py +2 -0
  8. codegraph/aitools.py +129 -0
  9. codegraph/apps.py +76 -0
  10. codegraph/blindspots.py +428 -0
  11. codegraph/bridges.py +1701 -0
  12. codegraph/cli.py +725 -0
  13. codegraph/concepts.py +362 -0
  14. codegraph/config.py +559 -0
  15. codegraph/core/__init__.py +0 -0
  16. codegraph/core/cache.py +375 -0
  17. codegraph/core/detect.py +80 -0
  18. codegraph/core/extractors.py +187 -0
  19. codegraph/core/fsutil.py +61 -0
  20. codegraph/core/generated.py +575 -0
  21. codegraph/core/model.py +174 -0
  22. codegraph/core/paths.py +175 -0
  23. codegraph/core/plugin.py +160 -0
  24. codegraph/core/store.py +80 -0
  25. codegraph/core/syntax_errors.py +132 -0
  26. codegraph/coverage.py +928 -0
  27. codegraph/doctor.py +453 -0
  28. codegraph/external.py +613 -0
  29. codegraph/indexer.py +336 -0
  30. codegraph/link.py +434 -0
  31. codegraph/lint_async.py +524 -0
  32. codegraph/mcp_server.py +1303 -0
  33. codegraph/parity.py +473 -0
  34. codegraph/parity_structure.py +307 -0
  35. codegraph/payload.py +321 -0
  36. codegraph/plans.py +1285 -0
  37. codegraph/platform_scan.py +643 -0
  38. codegraph/platforms.py +1369 -0
  39. codegraph/plugins/__init__.py +0 -0
  40. codegraph/plugins/cfamily/__init__.py +0 -0
  41. codegraph/plugins/cfamily/plugin.py +930 -0
  42. codegraph/plugins/cfamily/syntax.py +881 -0
  43. codegraph/plugins/dart/__init__.py +0 -0
  44. codegraph/plugins/dart/bridges.py +345 -0
  45. codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
  46. codegraph/plugins/dart/extractor/pubspec.lock +149 -0
  47. codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
  48. codegraph/plugins/dart/http.py +904 -0
  49. codegraph/plugins/dart/models.py +308 -0
  50. codegraph/plugins/dart/plugin.py +625 -0
  51. codegraph/plugins/dart/program.py +907 -0
  52. codegraph/plugins/django/__init__.py +0 -0
  53. codegraph/plugins/django/extras.py +378 -0
  54. codegraph/plugins/django/models.py +508 -0
  55. codegraph/plugins/django/plugin.py +728 -0
  56. codegraph/plugins/django/schemas.py +339 -0
  57. codegraph/plugins/django/shapes.py +216 -0
  58. codegraph/plugins/django/urls.py +603 -0
  59. codegraph/plugins/express/__init__.py +0 -0
  60. codegraph/plugins/express/plugin.py +428 -0
  61. codegraph/plugins/flutter/__init__.py +0 -0
  62. codegraph/plugins/flutter/plugin.py +538 -0
  63. codegraph/plugins/kotlin/__init__.py +0 -0
  64. codegraph/plugins/kotlin/exact.py +457 -0
  65. codegraph/plugins/kotlin/plugin.py +1961 -0
  66. codegraph/plugins/kotlin/reparse.py +234 -0
  67. codegraph/plugins/laravel/__init__.py +0 -0
  68. codegraph/plugins/laravel/broadcast.py +351 -0
  69. codegraph/plugins/laravel/plugin.py +863 -0
  70. codegraph/plugins/laravel/tests.py +262 -0
  71. codegraph/plugins/laravel/values.py +728 -0
  72. codegraph/plugins/native/__init__.py +0 -0
  73. codegraph/plugins/native/gates.py +286 -0
  74. codegraph/plugins/native/runner.py +183 -0
  75. codegraph/plugins/native/scipread.py +194 -0
  76. codegraph/plugins/native/ts.py +54 -0
  77. codegraph/plugins/nest/__init__.py +0 -0
  78. codegraph/plugins/nest/plugin.py +654 -0
  79. codegraph/plugins/nextjs/__init__.py +0 -0
  80. codegraph/plugins/nextjs/plugin.py +336 -0
  81. codegraph/plugins/nuxt/__init__.py +0 -0
  82. codegraph/plugins/nuxt/plugin.py +308 -0
  83. codegraph/plugins/php/__init__.py +0 -0
  84. codegraph/plugins/php/extractor/composer.json +5 -0
  85. codegraph/plugins/php/extractor/composer.lock +76 -0
  86. codegraph/plugins/php/extractor/extract.php +743 -0
  87. codegraph/plugins/php/gating.py +573 -0
  88. codegraph/plugins/php/plugin.py +668 -0
  89. codegraph/plugins/php/strings.py +197 -0
  90. codegraph/plugins/python/__init__.py +0 -0
  91. codegraph/plugins/python/aitools.py +664 -0
  92. codegraph/plugins/python/external.py +245 -0
  93. codegraph/plugins/python/fields.py +107 -0
  94. codegraph/plugins/python/plugin.py +1733 -0
  95. codegraph/plugins/python/refs.py +485 -0
  96. codegraph/plugins/python/roots.py +412 -0
  97. codegraph/plugins/python/socketio.py +210 -0
  98. codegraph/plugins/python/subproc.py +864 -0
  99. codegraph/plugins/python/tests.py +1040 -0
  100. codegraph/plugins/python/values.py +179 -0
  101. codegraph/plugins/pyweb/__init__.py +0 -0
  102. codegraph/plugins/pyweb/plugin.py +1334 -0
  103. codegraph/plugins/pyweb/values.py +68 -0
  104. codegraph/plugins/rust/__init__.py +0 -0
  105. codegraph/plugins/rust/cargo.py +226 -0
  106. codegraph/plugins/rust/plugin.py +980 -0
  107. codegraph/plugins/rust/syntax.py +678 -0
  108. codegraph/plugins/scip/__init__.py +0 -0
  109. codegraph/plugins/scip/importer.py +129 -0
  110. codegraph/plugins/scip/scip.proto +962 -0
  111. codegraph/plugins/scip/scip_pb2.py +97 -0
  112. codegraph/plugins/stubs/__init__.py +0 -0
  113. codegraph/plugins/stubs/plugins.py +38 -0
  114. codegraph/plugins/swift/__init__.py +0 -0
  115. codegraph/plugins/swift/baseurl.py +109 -0
  116. codegraph/plugins/swift/exact.py +415 -0
  117. codegraph/plugins/swift/indexstore.py +209 -0
  118. codegraph/plugins/swift/packages.py +174 -0
  119. codegraph/plugins/swift/plugin.py +2890 -0
  120. codegraph/plugins/ts/__init__.py +0 -0
  121. codegraph/plugins/ts/baseurl.py +185 -0
  122. codegraph/plugins/ts/extractor/extract.mjs +2652 -0
  123. codegraph/plugins/ts/extractor/fw.mjs +685 -0
  124. codegraph/plugins/ts/extractor/package-lock.json +205 -0
  125. codegraph/plugins/ts/extractor/package.json +9 -0
  126. codegraph/plugins/ts/plugin.py +480 -0
  127. codegraph/plugins/tsweb/__init__.py +0 -0
  128. codegraph/plugins/tsweb/common.py +290 -0
  129. codegraph/plugins/tsweb/data.py +276 -0
  130. codegraph/presets/__init__.py +146 -0
  131. codegraph/presets/c_cpp.yaml +9 -0
  132. codegraph/presets/common.yaml +66 -0
  133. codegraph/presets/dart.yaml +9 -0
  134. codegraph/presets/django-ninja.yaml +15 -0
  135. codegraph/presets/django.yaml +25 -0
  136. codegraph/presets/djangorestframework.yaml +17 -0
  137. codegraph/presets/express.yaml +17 -0
  138. codegraph/presets/kotlin.yaml +11 -0
  139. codegraph/presets/laravel.yaml +40 -0
  140. codegraph/presets/nest.yaml +11 -0
  141. codegraph/presets/nextjs.yaml +15 -0
  142. codegraph/presets/nuxt.yaml +9 -0
  143. codegraph/presets/php.yaml +5 -0
  144. codegraph/presets/python.yaml +10 -0
  145. codegraph/presets/rust.yaml +5 -0
  146. codegraph/presets/swift.yaml +10 -0
  147. codegraph/presets/typescript.yaml +13 -0
  148. codegraph/process_runs.py +328 -0
  149. codegraph/protocols/__init__.py +299 -0
  150. codegraph/protocols/builtin.py +67 -0
  151. codegraph/protocols/matchers.py +144 -0
  152. codegraph/protocols/view.py +334 -0
  153. codegraph/query.py +2089 -0
  154. codegraph/realtime.py +260 -0
  155. codegraph/roundtrip.py +346 -0
  156. codegraph/routes.py +442 -0
  157. codegraph/starters.py +218 -0
  158. codegraph/tests_index.py +117 -0
  159. codegraph/viz/__init__.py +0 -0
  160. codegraph/viz/graph.py +369 -0
  161. codegraph/viz/server.py +198 -0
  162. codegraph/viz/static/app.css +148 -0
  163. codegraph/viz/static/app.js +1082 -0
  164. codegraph/viz/static/index.html +81 -0
  165. codegraph/viz/static/layered.js +237 -0
  166. codegraph/viz/static/vendor/VERSIONS.txt +4 -0
  167. codegraph/viz/static/vendor/cose-base.js +3214 -0
  168. codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
  169. codegraph/viz/static/vendor/cytoscape.min.js +31 -0
  170. codegraph/viz/static/vendor/layout-base.js +5230 -0
  171. codegraph/viz/tools/package-lock.json +303 -0
  172. codegraph/viz/tools/package.json +7 -0
  173. codegraph/viz/tools/shoot.mjs +165 -0
  174. 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}