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
@@ -0,0 +1,603 @@
1
+ """URL resolution: Django urlconfs (path/re_path/url/include, namespaces, nested includes), DRF routers
2
+ (register/@action, trailing_slash, nested routers) and django-ninja (NinjaAPI/Router, add_router chains,
3
+ @router.<verb>, auth inheritance). Produces route dicts; plugin.py turns them into nodes/edges.
4
+
5
+ Route dict: {method, uri ('/a/{id}/'), file, line, handler (FuncInfo|ClassInfo|None), view (text),
6
+ framework, name, namespace, conf (exact|resolved|heuristic), mounted, extra...}
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+ import re
12
+
13
+ from ...core.model import EXACT, HEURISTIC, RESOLVED
14
+ from ..python.plugin import ClassInfo, Ctx, FuncInfo, ModInfo, PyProgram, ann_text, const_str, dotted, kwarg
15
+
16
+ HTTP_METHODS = ("get", "post", "put", "patch", "delete", "head", "options")
17
+ PATH_FUNCS = {"path", "re_path", "url"}
18
+ CONV = re.compile(r"<(?:(\w+):)?(\w+)>")
19
+ # DRF / Django generic views -> HTTP methods they implement (when not overridden locally)
20
+ GENERIC_METHODS = {
21
+ "View": set(), "APIView": set(), "GenericAPIView": set(), "TemplateView": {"GET"}, "RedirectView": {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"},
22
+ "ListView": {"GET"}, "DetailView": {"GET"}, "FormView": {"GET", "POST", "PUT"}, "CreateView": {"GET", "POST", "PUT"},
23
+ "UpdateView": {"GET", "POST", "PUT"}, "DeleteView": {"GET", "POST", "DELETE"}, "ArchiveIndexView": {"GET"},
24
+ "LoginView": {"GET", "POST"}, "LogoutView": {"POST"}, "PasswordChangeView": {"GET", "POST"},
25
+ "ListAPIView": {"GET"}, "CreateAPIView": {"POST"}, "RetrieveAPIView": {"GET"}, "DestroyAPIView": {"DELETE"},
26
+ "UpdateAPIView": {"PUT", "PATCH"}, "ListCreateAPIView": {"GET", "POST"}, "RetrieveUpdateAPIView": {"GET", "PUT", "PATCH"},
27
+ "RetrieveDestroyAPIView": {"GET", "DELETE"}, "RetrieveUpdateDestroyAPIView": {"GET", "PUT", "PATCH", "DELETE"},
28
+ "ObtainAuthToken": {"POST"}, "TokenObtainPairView": {"POST"}, "TokenRefreshView": {"POST"}, "TokenVerifyView": {"POST"},
29
+ }
30
+ VIEWSET_ACTIONS = {
31
+ "ModelViewSet": {"list", "create", "retrieve", "update", "partial_update", "destroy"},
32
+ "ReadOnlyModelViewSet": {"list", "retrieve"},
33
+ "ListModelMixin": {"list"}, "CreateModelMixin": {"create"}, "RetrieveModelMixin": {"retrieve"},
34
+ "UpdateModelMixin": {"update", "partial_update"}, "DestroyModelMixin": {"destroy"},
35
+ }
36
+ ACTION_ROUTES = [("list", "GET", False), ("create", "POST", False), ("retrieve", "GET", True), ("update", "PUT", True),
37
+ ("partial_update", "PATCH", True), ("destroy", "DELETE", True)]
38
+ ACTION_METHOD = {"list": "get", "retrieve": "get", "create": "post", "update": "put", "partial_update": "patch", "destroy": "delete"}
39
+ NINJA_API = ("NinjaAPI", "NinjaExtraAPI")
40
+ NINJA_ROUTER = ("Router",)
41
+
42
+
43
+ def django_path_to_template(route: str) -> tuple[str, list[str]]:
44
+ params = []
45
+
46
+ def rep(m):
47
+ params.append(m.group(2))
48
+ return "{" + m.group(2) + "}"
49
+ return CONV.sub(rep, route), params
50
+
51
+
52
+ def regex_to_template(rx: str) -> tuple[str, bool]:
53
+ """'^books/(?P<pk>[0-9]+)/$' -> ('books/{pk}/', exact?)."""
54
+ s = rx
55
+ if s.startswith("^"):
56
+ s = s[1:]
57
+ if s.endswith("$"):
58
+ s = s[:-1]
59
+ if s.endswith(r"\Z"):
60
+ s = s[:-2]
61
+ out, i, n_unnamed, clean = [], 0, 0, True
62
+ while i < len(s):
63
+ ch = s[i]
64
+ if ch == "\\" and i + 1 < len(s):
65
+ nx = s[i + 1]
66
+ if nx in "./-_~":
67
+ out.append(nx); i += 2; continue
68
+ clean = False
69
+ out.append("{_}"); i += 2
70
+ while i < len(s) and s[i] in "+*?":
71
+ i += 1
72
+ continue
73
+ if ch == "(":
74
+ depth, j = 1, i + 1
75
+ while j < len(s) and depth:
76
+ if s[j] == "\\":
77
+ j += 2; continue
78
+ if s[j] == "(":
79
+ depth += 1
80
+ elif s[j] == ")":
81
+ depth -= 1
82
+ j += 1
83
+ grp = s[i:j]
84
+ m = re.match(r"\(\?P<(\w+)>", grp)
85
+ opt = j < len(s) and s[j] == "?"
86
+ if m:
87
+ out.append("{" + m.group(1) + "}")
88
+ elif grp.startswith("(?:"):
89
+ inner, ex = regex_to_template(grp[3:-1])
90
+ if opt:
91
+ clean = False
92
+ if inner.strip("/") == "" or inner == "/":
93
+ j += 1
94
+ i = j
95
+ continue
96
+ out.append(inner)
97
+ clean = clean and ex
98
+ else:
99
+ out.append("{arg%d}" % n_unnamed)
100
+ n_unnamed += 1
101
+ if opt:
102
+ j += 1
103
+ clean = False
104
+ i = j
105
+ continue
106
+ if ch in "?*+|[]":
107
+ clean = False
108
+ if ch == "?" and out and out[-1] == "/":
109
+ i += 1
110
+ continue
111
+ if ch == "[":
112
+ j = s.find("]", i)
113
+ out.append("{_}")
114
+ i = j + 1 if j > 0 else len(s)
115
+ while i < len(s) and s[i] in "+*?":
116
+ i += 1
117
+ continue
118
+ i += 1
119
+ continue
120
+ if ch in ".":
121
+ clean = False
122
+ out.append("{_}"); i += 1
123
+ continue
124
+ out.append(ch)
125
+ i += 1
126
+ return "".join(out), clean
127
+
128
+
129
+ def join(*parts: str) -> str:
130
+ s = "/".join(p.strip("/") if k < len(parts) - 1 else p.lstrip("/") for k, p in enumerate(parts) if p not in ("", None))
131
+ s = re.sub(r"/{2,}", "/", s)
132
+ return "/" + s.lstrip("/")
133
+
134
+
135
+ class UrlResolver:
136
+ def __init__(self, prog: PyProgram, plugin):
137
+ self.prog = prog
138
+ self.p = plugin
139
+ self.routes: list[dict] = []
140
+ self.unresolved: list[dict] = []
141
+ self.walked: set = set()
142
+ self.mounts: dict = {} # ninja api key -> [mount prefixes]
143
+ self.included: set[str] = set() # urlconf modules reached from a root
144
+
145
+ # ---------------- generic evaluation of module-level lists
146
+ def module_list(self, m: ModInfo, var: str, depth=0) -> list[tuple]:
147
+ """Items of a module-level list built with =, +=, .append/.extend/.insert. -> [(item ast, cond)]"""
148
+ items: list[tuple] = []
149
+
150
+ def visit(stmts, cond):
151
+ nonlocal items
152
+ for st in stmts:
153
+ if isinstance(st, ast.Assign) and any(isinstance(t, ast.Name) and t.id == var for t in st.targets):
154
+ new = self.eval_list(m, st.value, depth, cond)
155
+ if not new and any(isinstance(x, ast.Name) and x.id == var for x in ast.walk(st.value)):
156
+ continue # urlpatterns = wrap(urlpatterns): an unknown pass-through keeps what we have
157
+ items = (items + new) if cond else new
158
+ elif isinstance(st, ast.AnnAssign) and isinstance(st.target, ast.Name) and st.target.id == var and st.value is not None:
159
+ items = self.eval_list(m, st.value, depth, cond)
160
+ elif isinstance(st, ast.AugAssign) and isinstance(st.target, ast.Name) and st.target.id == var:
161
+ items = items + self.eval_list(m, st.value, depth, cond)
162
+ elif isinstance(st, ast.Expr) and isinstance(st.value, ast.Call) and isinstance(st.value.func, ast.Attribute) \
163
+ and isinstance(st.value.func.value, ast.Name) and st.value.func.value.id == var:
164
+ a = st.value.func.attr
165
+ if a == "append" and st.value.args:
166
+ items.append((st.value.args[0], cond))
167
+ elif a == "extend" and st.value.args:
168
+ items += self.eval_list(m, st.value.args[0], depth, cond)
169
+ elif a == "insert" and len(st.value.args) > 1:
170
+ items.insert(0, (st.value.args[1], cond))
171
+ elif isinstance(st, ast.If):
172
+ c = ann_text(st.test)
173
+ visit(st.body, c)
174
+ visit(st.orelse, f"not ({c})")
175
+ elif isinstance(st, ast.Try):
176
+ visit(st.body, cond)
177
+ for h in st.handlers:
178
+ visit(h.body, "except")
179
+ visit(m.tree.body, None)
180
+ return items
181
+
182
+ def eval_list(self, m: ModInfo, e, depth=0, cond=None) -> list[tuple]:
183
+ if depth > 12 or e is None:
184
+ return []
185
+ if isinstance(e, (ast.List, ast.Tuple)):
186
+ out = []
187
+ for x in e.elts:
188
+ if isinstance(x, ast.Starred):
189
+ out += self.eval_list(m, x.value, depth + 1, cond)
190
+ else:
191
+ out.append((x, cond))
192
+ return out
193
+ if isinstance(e, ast.BinOp) and isinstance(e.op, ast.Add):
194
+ return self.eval_list(m, e.left, depth + 1, cond) + self.eval_list(m, e.right, depth + 1, cond)
195
+ if isinstance(e, ast.Name):
196
+ t = self.prog.resolve_name(m, e.id)
197
+ if t and t[0] == "var":
198
+ return self.module_list(t[1], t[2], depth + 1)
199
+ return []
200
+ if isinstance(e, ast.Attribute) and e.attr == "urls":
201
+ return [(e, cond)]
202
+ if isinstance(e, ast.Call):
203
+ fn = (dotted(e.func) or "").split(".")[-1]
204
+ if fn in ("format_suffix_patterns", "i18n_patterns", "list") and e.args:
205
+ out = []
206
+ for a in e.args:
207
+ if isinstance(a, ast.Starred):
208
+ out += self.eval_list(m, a.value, depth + 1, cond)
209
+ elif fn == "i18n_patterns":
210
+ out.append((a, cond))
211
+ else:
212
+ out += self.eval_list(m, a, depth + 1, cond)
213
+ if fn == "i18n_patterns":
214
+ return [(("i18n", x), c) for x, c in out]
215
+ return out
216
+ if fn in ("static", "staticfiles_urlpatterns", "debug_toolbar_urls"):
217
+ return []
218
+ if fn in PATH_FUNCS or fn in ("include",):
219
+ return [(e, cond)]
220
+ if isinstance(e, ast.Attribute):
221
+ t = self.prog.infer(e, Ctx(m, None, None))
222
+ if t and t[0] == "var":
223
+ return self.module_list(t[1], t[2], depth + 1)
224
+ return []
225
+
226
+ # ---------------- urlconf walk
227
+ def walk(self, m: ModInfo, prefix: str, ns: list, depth: int, chain: list, var="urlpatterns", items=None, conds=()):
228
+ key = (m.name, prefix, var)
229
+ if depth > 15 or key in self.walked:
230
+ return
231
+ self.walked.add(key)
232
+ self.included.add(m.name)
233
+ items = items if items is not None else self.module_list(m, var)
234
+ for it, cond in items:
235
+ i18n = False
236
+ if isinstance(it, tuple) and it[0] == "i18n":
237
+ it, i18n = it[1], True
238
+ pre = prefix + "{language}/" if i18n else prefix # Django concatenates pattern strings
239
+ cs = conds + ((cond,) if cond else ())
240
+ if isinstance(it, ast.Attribute) and it.attr == "urls":
241
+ self.mount_urls(m, it, pre, ns, chain, cs, it.lineno)
242
+ continue
243
+ if not isinstance(it, ast.Call):
244
+ continue
245
+ fname = (dotted(it.func) or "").split(".")[-1]
246
+ if fname not in PATH_FUNCS or not it.args:
247
+ continue
248
+ raw = const_str(it.args[0])
249
+ regex = fname != "path"
250
+ if raw is None:
251
+ tpl, ok = "{?}", False
252
+ self.unresolved.append({"file": m.file, "line": it.lineno, "reason": "non-literal route"})
253
+ elif regex:
254
+ tpl, ok = regex_to_template(raw)
255
+ else:
256
+ tpl, _ = django_path_to_template(raw)
257
+ ok = True
258
+ full = pre + tpl
259
+ view = it.args[1] if len(it.args) > 1 else kwarg(it, "view")
260
+ name = const_str(kwarg(it, "name")) or (const_str(it.args[3]) if len(it.args) > 3 else None)
261
+ where = {"file": m.file, "line": it.lineno}
262
+ self.view(m, view, full, ns, chain + [f"{m.file}:{it.lineno}"], cs, where, name, ok, raw)
263
+
264
+ def mount_urls(self, m, attr: ast.Attribute, prefix, ns, chain, conds, line):
265
+ """`api.urls` / `router.urls` / `admin.site.urls` used as a view or include target."""
266
+ ctx = Ctx(m, None, None)
267
+ base = attr.value
268
+ t = self.prog.infer(base, ctx)
269
+ rk = self.p.ninja.api_key(m, base)
270
+ if rk:
271
+ self.mounts.setdefault(rk, []).append({"prefix": prefix, "file": m.file, "line": line, "chain": chain, "conds": conds})
272
+ return
273
+ rr = self.p.drf.router_key(m, base)
274
+ if rr:
275
+ self.p.drf.mounts.setdefault(rr, []).append({"prefix": prefix, "file": m.file, "line": line, "chain": chain, "conds": conds, "ns": ns})
276
+ return
277
+ d = dotted(base) or ""
278
+ if d.endswith("site") or (t and t[0] in ("ext", "einst") and "admin" in t[1]):
279
+ self.routes.append({"method": "ANY", "uri": "/" + prefix,
280
+ "file": m.file, "line": line, "handler": None, "view": "django.contrib.admin.site.urls",
281
+ "framework": "django-admin", "conf": EXACT, "entry_kind": "admin_panel", "chain": chain,
282
+ "conds": list(conds), "mounted": True, "name": "admin"})
283
+ return
284
+ self.unresolved.append({"file": m.file, "line": line, "reason": f"unknown .urls target {d}"})
285
+
286
+ def view(self, m: ModInfo, view, full: str, ns: list, chain: list, conds, where, name, ok, raw):
287
+ prog = self.prog
288
+ ctx = Ctx(m, None, None)
289
+ if view is None:
290
+ return
291
+ if isinstance(view, ast.Attribute) and view.attr == "urls":
292
+ self.mount_urls(m, view, full, ns, chain, conds, where["line"])
293
+ return
294
+ if isinstance(view, ast.Call):
295
+ fn = view.func
296
+ fname = (dotted(fn) or "").split(".")[-1]
297
+ if fname == "include":
298
+ self.include(m, view, full, ns, chain, conds, where)
299
+ return
300
+ if isinstance(fn, ast.Attribute) and fn.attr in ("as_view", "as_asgi"):
301
+ t = prog.infer(fn.value, ctx)
302
+ actions = None
303
+ if view.args and isinstance(view.args[0], ast.Dict):
304
+ actions = {const_str(k): const_str(v) for k, v in zip(view.args[0].keys, view.args[0].values) if const_str(k)}
305
+ self.p.emit_cbv(t, fn.value, full, where, name, ns, chain, conds, ok, actions, raw)
306
+ return
307
+ # decorator-style wrappers: csrf_exempt(view), login_required(View.as_view()), cache_page(60)(view)
308
+ inner = view.args[-1] if view.args else None
309
+ if isinstance(fn, ast.Call) and view.args:
310
+ inner = view.args[0]
311
+ if inner is not None:
312
+ self.view(m, inner, full, ns, chain, conds + (f"wrapped:{fname or ann_text(fn)}",), where, name, ok, raw)
313
+ return
314
+ t = prog.infer(view, ctx)
315
+ if t and t[0] == "func":
316
+ self.p.emit_fbv(t[1], full, where, name, ns, chain, conds, ok, raw)
317
+ return
318
+ if t and t[0] == "type":
319
+ self.p.emit_cbv(t, view, full, where, name, ns, chain, conds, ok, None, raw)
320
+ return
321
+ if t and t[0] == "var":
322
+ # e.g. `my_view = SomeView.as_view()` at module level
323
+ val = prog.var_value(t[1], t[2])
324
+ if val is not None:
325
+ self.view(t[1], val, full, ns, chain, conds, where, name, ok, raw)
326
+ return
327
+ if isinstance(view, ast.Constant) and isinstance(view.value, str):
328
+ mod, _, fn = view.value.rpartition(".")
329
+ r = prog.lookup(mod, fn) if mod else None
330
+ if r and r[0] == "func":
331
+ self.p.emit_fbv(r[1], full, where, name, ns, chain, conds, ok, raw)
332
+ return
333
+ self.routes.append({"method": "ANY", "uri": "/" + full, "file": where["file"], "line": where["line"], "handler": None,
334
+ "view": ann_text(view), "framework": "django", "conf": EXACT if ok else HEURISTIC, "chain": chain,
335
+ "conds": list(conds), "mounted": True, "name": name, "namespace": ":".join(ns) or None,
336
+ "external_view": (t[1] if t and t[0] in ("ext", "einst") else None)})
337
+
338
+ def include(self, m: ModInfo, call: ast.Call, prefix, ns, chain, conds, where):
339
+ prog = self.prog
340
+ arg = call.args[0] if call.args else kwarg(call, "arg")
341
+ nsp = const_str(kwarg(call, "namespace"))
342
+ app_name = None
343
+ if isinstance(arg, ast.Tuple) and arg.elts:
344
+ app_name = const_str(arg.elts[1]) if len(arg.elts) > 1 else None
345
+ arg = arg.elts[0]
346
+ ns2 = ns + [nsp or app_name] if (nsp or app_name) else ns
347
+
348
+ def mod_ns(tm):
349
+ """include(module) without a namespace: Django uses the module's `app_name` (reverse('core-api:x'))."""
350
+ if nsp or app_name:
351
+ return ns2
352
+ vals = tm.vars.get("app_name") or []
353
+ an = const_str(vals[-1][0]) if vals else None
354
+ return ns + [an] if an else ns
355
+ s = const_str(arg)
356
+ if s is not None:
357
+ tm = prog.module(s)
358
+ if tm is None:
359
+ self.unresolved.append({**where, "reason": f"include('{s}') not found"})
360
+ return
361
+ self.walk(tm, prefix, mod_ns(tm), len(chain), chain)
362
+ return
363
+ if isinstance(arg, ast.Attribute) and arg.attr == "urls":
364
+ self.mount_urls(m, arg, prefix, ns2, chain, conds, where["line"])
365
+ return
366
+ if isinstance(arg, (ast.List, ast.Tuple, ast.BinOp)):
367
+ self.walk(m, prefix, ns2, len(chain), chain, var=f"<inline@{where['line']}>", items=self.eval_list(m, arg), conds=conds)
368
+ return
369
+ t = prog.infer(arg, Ctx(m, None, None))
370
+ if t and t[0] == "mod":
371
+ self.walk(t[1], prefix, mod_ns(t[1]), len(chain), chain)
372
+ return
373
+ if t and t[0] == "var":
374
+ self.walk(t[1], prefix, ns2, len(chain), chain, var=t[2])
375
+ return
376
+ self.unresolved.append({**where, "reason": f"include({ann_text(arg)}) unresolved"})
377
+
378
+
379
+ # --------------------------------------------------------------------------------------- DRF routers
380
+
381
+ class DrfRouters:
382
+ ROUTER_NAMES = ("DefaultRouter", "SimpleRouter", "ExtendedDefaultRouter", "ExtendedSimpleRouter", "NestedSimpleRouter",
383
+ "NestedDefaultRouter", "OptionalSlashRouter", "BulkRouter")
384
+
385
+ def __init__(self, prog: PyProgram):
386
+ self.prog = prog
387
+ self.routers: dict = {} # key -> {file, line, trailing_slash, default, parent, lookup, regs: []}
388
+ self.mounts: dict = {}
389
+ self._collect()
390
+
391
+ def _is_router_call(self, m, call) -> bool:
392
+ if not isinstance(call, ast.Call):
393
+ return False
394
+ t = self.prog.infer(call.func, Ctx(m, None, None))
395
+ if t and t[0] == "ext" and t[1].split(".")[-1] in self.ROUTER_NAMES and "ninja" not in t[1]:
396
+ return True
397
+ if t and t[0] == "type":
398
+ lin = self.prog.lineage(t[1]) + [t[1].qual]
399
+ if any("ninja" in x for x in lin):
400
+ return False
401
+ return any(x.split(".")[-1] in self.ROUTER_NAMES + ("BaseRouter",) for x in lin)
402
+ return False
403
+
404
+ def router_key(self, m: ModInfo, e):
405
+ t = self.prog.infer(e, Ctx(m, None, None)) if isinstance(e, ast.Name) else None
406
+ if isinstance(e, ast.Name):
407
+ r = self.prog.resolve_name(m, e.id)
408
+ if r and r[0] == "var" and (r[1].name, r[2]) in self.routers:
409
+ return (r[1].name, r[2])
410
+ if isinstance(e, ast.Attribute):
411
+ t = self.prog.infer(e.value, Ctx(m, None, None))
412
+ if t and t[0] == "mod" and (t[1].name, e.attr) in self.routers:
413
+ return (t[1].name, e.attr)
414
+ return None
415
+
416
+ def _collect(self):
417
+ prog = self.prog
418
+ for m in prog.modules.values():
419
+ for st in m.tree.body:
420
+ if isinstance(st, ast.Assign) and isinstance(st.targets[0], ast.Name) and self._is_router_call(m, st.value):
421
+ call = st.value
422
+ ts = kwarg(call, "trailing_slash")
423
+ tsv = True
424
+ if isinstance(ts, ast.Constant):
425
+ tsv = ts.value if isinstance(ts.value, bool) else (ts.value != "")
426
+ cls = (dotted(call.func) or "").split(".")[-1]
427
+ info = {"file": m.file, "line": st.lineno, "trailing_slash": tsv, "class": cls,
428
+ "default": "Default" in cls, "regs": [], "parent": None}
429
+ if cls.startswith("Nested") and len(call.args) >= 2:
430
+ info["parent"] = self.router_key(m, call.args[0])
431
+ info["parent_prefix"] = const_str(call.args[1])
432
+ info["lookup"] = const_str(kwarg(call, "lookup")) or "parent"
433
+ self.routers[(m.name, st.targets[0].id)] = info
434
+ for m in prog.modules.values():
435
+ for st in ast.walk(m.tree):
436
+ if isinstance(st, ast.Call) and isinstance(st.func, ast.Attribute) and st.func.attr == "register" and len(st.args) >= 2:
437
+ k = self.router_key(m, st.func.value)
438
+ if not k:
439
+ continue
440
+ prefix = const_str(st.args[0])
441
+ self.routers[k]["regs"].append({"prefix": prefix if prefix is not None else "{?}", "viewset": st.args[1],
442
+ "module": m, "line": st.lineno, "file": m.file,
443
+ "basename": const_str(kwarg(st, "basename") or kwarg(st, "base_name"))})
444
+ elif isinstance(st, ast.Call) and isinstance(st.func, ast.Attribute) and st.func.attr == "extend" and \
445
+ isinstance(st.func.value, ast.Attribute) and st.func.value.attr == "registry" and st.args:
446
+ k = self.router_key(m, st.func.value.value)
447
+ src = st.args[0]
448
+ k2 = self.router_key(m, src.value) if isinstance(src, ast.Attribute) else None
449
+ if k and k2:
450
+ self.routers[k].setdefault("extends", []).append(k2)
451
+
452
+ def regs(self, key, depth=0):
453
+ r = self.routers.get(key)
454
+ if not r or depth > 5:
455
+ return []
456
+ out = list(r["regs"])
457
+ for k2 in r.get("extends", []):
458
+ out += self.regs(k2, depth + 1)
459
+ return out
460
+
461
+
462
+ # --------------------------------------------------------------------------------------- django-ninja
463
+
464
+ class Ninja:
465
+ def __init__(self, prog: PyProgram):
466
+ self.prog = prog
467
+ self.objs: dict = {} # (mod, var) -> {kind: api|router, file, line, auth, call}
468
+ self.children: dict = {} # parent key -> [(prefix, child key, file, line, auth)]
469
+ self.ops: list[dict] = []
470
+ self.controllers: list = []
471
+ self._collect()
472
+
473
+ def _kind_of_call(self, m, call):
474
+ if not isinstance(call, ast.Call):
475
+ return None
476
+ t = self.prog.infer(call.func, Ctx(m, None, None))
477
+ names = []
478
+ if t and t[0] == "ext":
479
+ names = [t[1]]
480
+ elif t and t[0] == "type":
481
+ names = [t[1].qual] + self.prog.lineage(t[1])
482
+ for n in names:
483
+ last = n.split(".")[-1]
484
+ if ("ninja" in n or "." not in n) and last in NINJA_API:
485
+ return "api"
486
+ if ("ninja" in n) and last in NINJA_ROUTER:
487
+ return "router"
488
+ return None
489
+
490
+ def api_key(self, m: ModInfo, e):
491
+ if isinstance(e, ast.Name):
492
+ r = self.prog.resolve_name(m, e.id)
493
+ if r and r[0] == "var" and (r[1].name, r[2]) in self.objs:
494
+ return (r[1].name, r[2])
495
+ if isinstance(e, ast.Attribute):
496
+ t = self.prog.infer(e.value, Ctx(m, None, None))
497
+ if t and t[0] == "mod" and (t[1].name, e.attr) in self.objs:
498
+ return (t[1].name, e.attr)
499
+ return None
500
+
501
+ def _collect(self):
502
+ prog = self.prog
503
+ for m in prog.modules.values():
504
+ for st in m.tree.body:
505
+ if isinstance(st, (ast.Assign, ast.AnnAssign)):
506
+ tgt = st.targets[0] if isinstance(st, ast.Assign) else st.target
507
+ val = st.value
508
+ if isinstance(val, ast.IfExp): # api = NinjaAPI() if settings.DEBUG else NinjaAPI(docs_url=None)
509
+ val = val.body if self._kind_of_call(m, val.body) else val.orelse
510
+ k = self._kind_of_call(m, val) if val is not None else None
511
+ if k and isinstance(tgt, ast.Name):
512
+ call = val
513
+ self.objs[(m.name, tgt.id)] = {"kind": k, "file": m.file, "line": st.lineno,
514
+ "auth": kwarg(call, "auth"), "module": m,
515
+ "csrf": kwarg(call, "csrf"), "by_alias": kwarg(call, "by_alias"),
516
+ "urls_namespace": const_str(kwarg(call, "urls_namespace")),
517
+ "version": const_str(kwarg(call, "version"))}
518
+ for m in prog.modules.values():
519
+ for st in ast.walk(m.tree):
520
+ if isinstance(st, ast.Call) and isinstance(st.func, ast.Attribute) and st.func.attr == "add_router" and len(st.args) >= 2:
521
+ pk = self.api_key(m, st.func.value)
522
+ if not pk:
523
+ continue
524
+ pref = const_str(st.args[0])
525
+ child = st.args[1]
526
+ ck = None
527
+ s = const_str(child)
528
+ if s:
529
+ mod, _, var = s.rpartition(".")
530
+ if (mod, var) in self.objs:
531
+ ck = (mod, var)
532
+ else:
533
+ mm = prog.module(mod)
534
+ if mm and (mm.name, var) in self.objs:
535
+ ck = (mm.name, var)
536
+ else:
537
+ ck = self.api_key(m, child)
538
+ self.children.setdefault(pk, []).append({"prefix": pref if pref is not None else "{?}", "child": ck,
539
+ "file": m.file, "line": st.lineno, "auth": kwarg(st, "auth"),
540
+ "child_text": ann_text(child)})
541
+ # operations
542
+ for f in prog.funcs.values():
543
+ for d in f.decorators:
544
+ if not (isinstance(d, ast.Call) and isinstance(d.func, ast.Attribute)):
545
+ continue
546
+ verb = d.func.attr
547
+ if verb not in HTTP_METHODS and verb != "api_operation":
548
+ continue
549
+ key = self.api_key(f.module, d.func.value)
550
+ if not key:
551
+ continue
552
+ if verb == "api_operation":
553
+ ms = d.args[0] if d.args else kwarg(d, "methods")
554
+ methods = [const_str(x).upper() for x in (ms.elts if isinstance(ms, (ast.List, ast.Tuple)) else []) if const_str(x)]
555
+ pth = d.args[1] if len(d.args) > 1 else kwarg(d, "path")
556
+ else:
557
+ methods = [verb.upper()]
558
+ pth = d.args[0] if d.args else kwarg(d, "path")
559
+ p = const_str(pth)
560
+ self.ops.append({"key": key, "methods": methods, "path": p if p is not None else "{?}", "func": f,
561
+ "line": d.lineno, "file": f.file, "dec": d, "auth": kwarg(d, "auth"),
562
+ "auth_set": any(k.arg == "auth" for k in d.keywords),
563
+ "response": kwarg(d, "response"), "by_alias": kwarg(d, "by_alias"),
564
+ "url_name": const_str(kwarg(d, "url_name"))})
565
+ # ninja-extra controllers
566
+ for c in prog.classes.values():
567
+ for d in c.decorators:
568
+ if isinstance(d, ast.Call) and (dotted(d.func) or "").split(".")[-1] in ("api_controller", "Controller"):
569
+ pref = const_str(d.args[0]) if d.args else (const_str(kwarg(d, "prefix_or_class")) or "")
570
+ self.controllers.append({"cls": c, "prefix": pref or "", "auth": kwarg(d, "auth"), "line": d.lineno})
571
+
572
+ def mount_chains(self, mounts: dict) -> dict:
573
+ """object key -> list of (prefix chain [str], auth chain [ast|None], evidence chain, mounted)."""
574
+ out = {}
575
+ roots = [k for k, v in self.objs.items() if v["kind"] == "api"]
576
+ for r in roots:
577
+ ms = mounts.get(r) or []
578
+ base = [(m["prefix"], m) for m in ms] or [(None, None)]
579
+ for pre, mev in base:
580
+ stack = [(r, [pre or ""], [self.objs[r]["auth"]], [mev] if mev else [], 0)]
581
+ while stack:
582
+ k, pres, auths, ev, d = stack.pop()
583
+ out.setdefault(k, []).append({"prefixes": pres, "auths": auths, "evidence": ev, "mounted": pre is not None,
584
+ "api": r})
585
+ if d > 8:
586
+ continue
587
+ for ch in self.children.get(k, []):
588
+ if ch["child"] is None:
589
+ continue
590
+ stack.append((ch["child"], pres + [ch["prefix"]],
591
+ auths + [ch["auth"], self.objs.get(ch["child"], {}).get("auth")], ev + [ch], d + 1))
592
+ return out
593
+
594
+ @staticmethod
595
+ def full_path(mount_prefix: str, router_prefixes: list[str], op_path: str) -> str:
596
+ # ninja: "/".join([prefix, path]) then collapse '//' and lstrip('/'); Django mount prefix is prepended as-is
597
+ inner = "/".join(x for x in router_prefixes + [op_path] if x)
598
+ while "//" in inner:
599
+ inner = inner.replace("//", "/")
600
+ inner = inner.lstrip("/")
601
+ s = (mount_prefix or "") + inner # Django concatenates the mount pattern and ninja's route
602
+ s = re.sub(r"\{(?:\w+:)?(\w+)\}", r"{\1}", s)
603
+ return "/" + s
File without changes