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,262 @@
|
|
|
1
|
+
"""Laravel / PHPUnit / Pest tests as graph nodes.
|
|
2
|
+
|
|
3
|
+
* every PHPUnit test method (`test*`, `@test`, #[Test]) of a concrete class under tests/ -> node
|
|
4
|
+
`test:<Class>::<method>` (kind test) with TEST_CALLS -> the method (and -> setUp());
|
|
5
|
+
* every Pest `test('...')` / `it('...')` (inside `describe(...)` blocks too) -> node `test:<file>::<description>`;
|
|
6
|
+
the calls made inside its closure are re-attributed from the file to the test node, and the test gets
|
|
7
|
+
TEST_CALLS -> the file (beforeEach, uses(...) and other file-level setup);
|
|
8
|
+
* HTTP test calls (`$this->getJson('/api/x')`, `->actingAs($u)->post(route('x.store'))`, Pest `get('/x')`,
|
|
9
|
+
`$this->json('PATCH', '/x')`, and the same calls through project helpers such as `$this->getAs('/x')` whose URL /
|
|
10
|
+
verb are parameters) -> TEST_HTTP -> the matching route (URIs matched like cross-repo links, with the
|
|
11
|
+
`/api` prefix Laravel adds to routes/api.php; `route('name')` by route name).
|
|
12
|
+
|
|
13
|
+
Edges made by test code are retyped to non-propagating TEST_* kinds afterwards (codegraph/tests_index.py), so
|
|
14
|
+
tests never change blast radius, caller counts or entry tagging. `cg tests <symbol|route>` walks them on purpose.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
|
|
21
|
+
from ...core.model import EXACT, HEURISTIC, RESOLVED
|
|
22
|
+
from ..php.plugin import PhpFunc, is_test_path, module_of
|
|
23
|
+
from ..php.strings import StrEval
|
|
24
|
+
|
|
25
|
+
HTTP_VERBS = {"get": "GET", "getjson": "GET", "post": "POST", "postjson": "POST", "put": "PUT", "putjson": "PUT",
|
|
26
|
+
"patch": "PATCH", "patchjson": "PATCH", "delete": "DELETE", "deletejson": "DELETE", "options": "OPTIONS",
|
|
27
|
+
"optionsjson": "OPTIONS", "head": "HEAD"}
|
|
28
|
+
GENERIC_VERB = {"json", "call"} # ($method, $uri, ...)
|
|
29
|
+
PEST_TEST_FUNCS = {"test", "it"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _base(d):
|
|
33
|
+
while d and d.get("k") in ("mcall", "prop"):
|
|
34
|
+
d = d.get("of")
|
|
35
|
+
return d or {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def suite_of(path: str) -> str:
|
|
39
|
+
m = re.search(r"(?:^|/)(?:tests|Tests)/([^/]+)/", path)
|
|
40
|
+
return m.group(1) if m else "tests"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class LaravelTests:
|
|
44
|
+
def __init__(self, lp, prog):
|
|
45
|
+
self.lp, self.prog = lp, prog
|
|
46
|
+
self.sev = StrEval(prog)
|
|
47
|
+
self.calls: list[tuple] = []
|
|
48
|
+
self.helpers: dict = defaultdict(dict) # lower method name -> {fn id: {fn, verb | vi (verb param), ui (url param)}}
|
|
49
|
+
self.maybe_helper: list[tuple] = []
|
|
50
|
+
|
|
51
|
+
# ---- test-code fact handler (runs on tests/ only)
|
|
52
|
+
def handle_fact(self, prog, fn: PhpFunc, f: dict, ctx) -> None:
|
|
53
|
+
if f.get("t") != "call":
|
|
54
|
+
return
|
|
55
|
+
kind = f["kind"]
|
|
56
|
+
m = (f.get("m") if kind == "method" else (f.get("n") or "").split("\\")[-1] if kind == "func" else None) or ""
|
|
57
|
+
ml = m.lower()
|
|
58
|
+
if kind == "method":
|
|
59
|
+
base = _base(f.get("recv"))
|
|
60
|
+
if base.get("k") != "this" and not (base.get("k") == "func" and (base.get("n") or "").split("\\")[-1].lower() in ("actingas", "withheaders", "withoutmiddleware")):
|
|
61
|
+
return
|
|
62
|
+
elif kind != "func":
|
|
63
|
+
return
|
|
64
|
+
args = f.get("args") or []
|
|
65
|
+
if ml == "artisan" and kind == "method" and args and args[0].get("k") == "str":
|
|
66
|
+
# $this->artisan('orders:prune --days=1'): the command runs in process (#60)
|
|
67
|
+
lb = self.lp.b
|
|
68
|
+
lb.add_edge(fn.id, lb.add_node("command", args[0]["v"].split(" ")[0]), "DISPATCHES", fn.file, f["line"],
|
|
69
|
+
EXACT, via="$this->artisan")
|
|
70
|
+
self.lp.stats["test_artisan_calls"] += 1
|
|
71
|
+
return
|
|
72
|
+
if ml not in HTTP_VERBS and ml not in GENERIC_VERB:
|
|
73
|
+
self.maybe_helper.append((fn, ml, f)) # $this->getAs('/x') through a project test helper
|
|
74
|
+
return
|
|
75
|
+
args = f.get("args") or []
|
|
76
|
+
if ml in GENERIC_VERB:
|
|
77
|
+
if len(args) < 2:
|
|
78
|
+
return
|
|
79
|
+
vd, ua = args[0], args[1]
|
|
80
|
+
mv = self.sev.eval(vd, fn)
|
|
81
|
+
verb = mv[0].upper() if mv and "{" not in mv[0] else None
|
|
82
|
+
else:
|
|
83
|
+
vd, (verb, ua) = None, (HTTP_VERBS[ml], args[0] if args else None)
|
|
84
|
+
if not ua:
|
|
85
|
+
return
|
|
86
|
+
ui, vi = self._param_idx(fn, ua), (self._param_idx(fn, vd) if vd is not None and not verb else None)
|
|
87
|
+
if ui is not None and (verb or vi is not None):
|
|
88
|
+
# a helper whose URL (and maybe verb) is a parameter: expanded at its call sites in contribute()
|
|
89
|
+
self.helpers[fn.name.lower()][fn.id] = {"fn": fn, "verb": verb, "vi": vi, "ui": ui}
|
|
90
|
+
return
|
|
91
|
+
if verb:
|
|
92
|
+
self._record(fn, verb, ua, f["line"], m)
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _param_idx(fn: PhpFunc, d) -> int | None:
|
|
96
|
+
"""Index of the parameter `d` is (a bare `$param` never reassigned in fn), else None."""
|
|
97
|
+
if not d or d.get("k") != "var":
|
|
98
|
+
return None
|
|
99
|
+
names = [p["name"] for p in fn.params]
|
|
100
|
+
if d.get("n") not in names or any(x.get("t") == "assign" and x.get("var") == d["n"] for x in fn.facts):
|
|
101
|
+
return None
|
|
102
|
+
return names.index(d["n"])
|
|
103
|
+
|
|
104
|
+
def _record(self, fn: PhpFunc, verb: str, ua: dict, line: int, via: str, bind=None) -> None:
|
|
105
|
+
if ua.get("k") == "func" and (ua.get("n") or "").split("\\")[-1].lower() == "route" and ua.get("args"):
|
|
106
|
+
rn = self.sev.eval(ua["args"][0], fn)
|
|
107
|
+
if rn and "{" not in rn[0]:
|
|
108
|
+
self.calls.append((fn, verb, None, rn[0], line, via))
|
|
109
|
+
return
|
|
110
|
+
if ua.get("k") == "func" and (ua.get("n") or "").split("\\")[-1].lower() == "url" and ua.get("args"):
|
|
111
|
+
ua = ua["args"][0]
|
|
112
|
+
for p in self.sev.eval(ua, fn)[:3]:
|
|
113
|
+
p = re.sub(r"^[a-z]+://[^/]+", "", p.split("?")[0].split("#")[0])
|
|
114
|
+
if not p or (not p.startswith("/") and "/" not in p and not p.startswith("{")):
|
|
115
|
+
continue # `$this->get('key')` on something that is not an HTTP test client
|
|
116
|
+
self.calls.append((fn, verb, "/" + p.lstrip("/"), None, line, via))
|
|
117
|
+
|
|
118
|
+
def _expand_helpers(self, st) -> None:
|
|
119
|
+
"""Calls through test helpers (`$this->getAs('/x')` -> jsonAs('get', $url) -> $this->json($method, $uri)):
|
|
120
|
+
a call passing a literal verb / URL is recorded; one passing its own parameters makes the caller a helper too."""
|
|
121
|
+
done = set()
|
|
122
|
+
for _ in range(4):
|
|
123
|
+
grew = False
|
|
124
|
+
for fn, ml, f in self.maybe_helper:
|
|
125
|
+
hs = self.helpers.get(ml)
|
|
126
|
+
key = (fn.id, f["line"], ml)
|
|
127
|
+
if not hs or len(hs) != 1 or key in done:
|
|
128
|
+
continue
|
|
129
|
+
h = next(iter(hs.values()))
|
|
130
|
+
if h["fn"].id == fn.id:
|
|
131
|
+
continue
|
|
132
|
+
args = f.get("args") or []
|
|
133
|
+
ua = args[h["ui"]] if h["ui"] < len(args) else None
|
|
134
|
+
vd = args[h["vi"]] if h["vi"] is not None and h["vi"] < len(args) else None
|
|
135
|
+
if not ua:
|
|
136
|
+
continue
|
|
137
|
+
verb = h["verb"]
|
|
138
|
+
if not verb and vd is not None:
|
|
139
|
+
mv = self.sev.eval(vd, fn)
|
|
140
|
+
verb = mv[0].upper() if mv and "{" not in mv[0] else None
|
|
141
|
+
ui = self._param_idx(fn, ua)
|
|
142
|
+
vi = self._param_idx(fn, vd) if (vd is not None and not verb) else None
|
|
143
|
+
done.add(key)
|
|
144
|
+
if ui is not None and (verb or vi is not None):
|
|
145
|
+
if fn.id not in self.helpers[fn.name.lower()]:
|
|
146
|
+
self.helpers[fn.name.lower()][fn.id] = {"fn": fn, "verb": verb, "vi": vi, "ui": ui}
|
|
147
|
+
grew = True
|
|
148
|
+
continue
|
|
149
|
+
if verb:
|
|
150
|
+
self._record(fn, verb, ua, f["line"], f"{ml}()")
|
|
151
|
+
st["http_calls_via_helper"] += 1
|
|
152
|
+
if not grew:
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
# ---- after reference emission
|
|
156
|
+
def contribute(self) -> dict:
|
|
157
|
+
from ...link import match_endpoint
|
|
158
|
+
b, prog = self.lp.b, self.prog
|
|
159
|
+
st = defaultdict(int)
|
|
160
|
+
self._expand_helpers(st)
|
|
161
|
+
routes, by_name = [], defaultdict(list)
|
|
162
|
+
for nid, n in b.nodes.items():
|
|
163
|
+
if n.kind != "route" or not n.attrs.get("uri") or not n.attrs.get("method"):
|
|
164
|
+
continue
|
|
165
|
+
uris = [("as-declared", n.attrs["uri"])]
|
|
166
|
+
if (n.file or "").startswith(("routes/api.php", "routes/api/")):
|
|
167
|
+
uris.append(("api-prefixed", "/api" + n.attrs["uri"]))
|
|
168
|
+
routes.append({"id": nid, "uri": n.attrs["uri"], "method": n.attrs["method"], "uris": uris})
|
|
169
|
+
if n.attrs.get("name"):
|
|
170
|
+
by_name[n.attrs["name"]].append(n)
|
|
171
|
+
for fn, verb, path, rname, line, via in self.calls:
|
|
172
|
+
st["http_calls"] += 1
|
|
173
|
+
if rname:
|
|
174
|
+
hits = [n for n in by_name.get(rname, []) if n.attrs.get("method") in (verb, "ANY") or (verb == "HEAD" and n.attrs.get("method") == "GET")]
|
|
175
|
+
# one name on several routes (unnamed routes inside a named group carry just the group prefix): Laravel
|
|
176
|
+
# resolves it to one of them, cg cannot say which
|
|
177
|
+
conf = EXACT if len(hits) == 1 else HEURISTIC
|
|
178
|
+
if len(hits) > 3:
|
|
179
|
+
hits = []
|
|
180
|
+
st["http_calls_ambiguous_name"] += 1
|
|
181
|
+
for n in hits:
|
|
182
|
+
b.add_edge(fn.id, n.id, "TEST_HTTP", fn.file, line, conf, via=f"{via}(route('{rname}'))")
|
|
183
|
+
st["http_calls_matched" if hits else "http_calls_unmatched"] += 1
|
|
184
|
+
continue
|
|
185
|
+
if re.fullmatch(r"/\{[^}]*\}", path):
|
|
186
|
+
st["http_calls_url_unknown"] += 1 # `$this->post($url)` with $url from elsewhere: no route to name
|
|
187
|
+
continue
|
|
188
|
+
res = match_endpoint(verb, path, routes, "api")
|
|
189
|
+
if len(res["matched"]) > 3 and all(mm["confidence"] == HEURISTIC for mm in res["matched"]):
|
|
190
|
+
res["matched"] = [] # a bare suffix shared by many routes says nothing
|
|
191
|
+
for mm in res["matched"]:
|
|
192
|
+
b.add_edge(fn.id, mm["route"], "TEST_HTTP", fn.file, line, mm["confidence"], via=via, path=path)
|
|
193
|
+
st["http_calls_matched" if res["matched"] else "http_calls_unmatched"] += 1
|
|
194
|
+
st["phpunit_tests"] = self._phpunit()
|
|
195
|
+
st["pest_tests"] = self._pest()
|
|
196
|
+
return dict(st)
|
|
197
|
+
|
|
198
|
+
def _is_test_method(self, m: PhpFunc) -> bool:
|
|
199
|
+
if m.abstract or m.visibility != "public" or m.static:
|
|
200
|
+
return False
|
|
201
|
+
if m.name.startswith("test") or (m.doc and re.search(r"@test\b", m.doc)):
|
|
202
|
+
return True
|
|
203
|
+
return any(a.lstrip("\\").endswith("Attributes\\Test") or a == "Test" for a in m.attributes)
|
|
204
|
+
|
|
205
|
+
def _phpunit(self) -> int:
|
|
206
|
+
b, prog = self.lp.b, self.prog
|
|
207
|
+
n = 0
|
|
208
|
+
for fq in sorted(prog.test_classes):
|
|
209
|
+
c = prog.cls(fq)
|
|
210
|
+
if not c or c.kind != "class" or c.abstract:
|
|
211
|
+
continue
|
|
212
|
+
seen = set()
|
|
213
|
+
for a in prog.ancestors(fq):
|
|
214
|
+
ac = prog.cls(a)
|
|
215
|
+
if not ac or a not in prog.test_classes:
|
|
216
|
+
continue
|
|
217
|
+
for ln, m in ac.methods.items():
|
|
218
|
+
if ln in seen or not self._is_test_method(m):
|
|
219
|
+
continue
|
|
220
|
+
seen.add(ln)
|
|
221
|
+
short = fq.split("\\")[-1]
|
|
222
|
+
tid = b.add_node("test", f"{fq}::{m.name}", name=f"{short}::{m.name}", fqn=f"{fq}::{m.name}", file=m.file,
|
|
223
|
+
line=m.line, end_line=m.end_line, module=module_of(m.file), lang="php",
|
|
224
|
+
attrs={"framework": "phpunit", "suite": suite_of(m.file), "test": True})
|
|
225
|
+
b.add_edge(tid, m.id, "TEST_CALLS", m.file, m.line, EXACT, via="test method")
|
|
226
|
+
su = prog.find_method(fq, "setUp")
|
|
227
|
+
if su and su.cls in prog.test_classes:
|
|
228
|
+
b.add_edge(tid, su.id, "TEST_CALLS", su.file, su.line, RESOLVED, via="setUp")
|
|
229
|
+
n += 1
|
|
230
|
+
return n
|
|
231
|
+
|
|
232
|
+
def _pest(self) -> int:
|
|
233
|
+
b, prog = self.lp.b, self.prog
|
|
234
|
+
moves = defaultdict(list)
|
|
235
|
+
n = 0
|
|
236
|
+
for file, sf in prog.scripts.items():
|
|
237
|
+
if not is_test_path(file):
|
|
238
|
+
continue
|
|
239
|
+
calls = [f for f in sf.facts if f.get("t") == "call" and f["kind"] == "func" and f.get("args")]
|
|
240
|
+
describes = [(f["line"], f.get("end", f["line"]), self.sev.eval(f["args"][0], sf)[0]) for f in calls
|
|
241
|
+
if (f.get("n") or "").split("\\")[-1].lower() == "describe"]
|
|
242
|
+
used = defaultdict(int)
|
|
243
|
+
for f in calls:
|
|
244
|
+
fname = (f.get("n") or "").split("\\")[-1].lower()
|
|
245
|
+
if fname not in PEST_TEST_FUNCS:
|
|
246
|
+
continue
|
|
247
|
+
lo, hi = f["line"], f.get("end", f["line"])
|
|
248
|
+
desc = self.sev.eval(f["args"][0], sf)[0]
|
|
249
|
+
if fname == "it":
|
|
250
|
+
desc = "it " + desc
|
|
251
|
+
path = [d for (dl, dh, d) in sorted(describes) if dl < lo and hi <= dh] + [desc]
|
|
252
|
+
name = " > ".join(path)
|
|
253
|
+
used[name] += 1
|
|
254
|
+
key = f"{file}::{name}" + (f" #{used[name]}" if used[name] > 1 else "")
|
|
255
|
+
tid = b.add_node("test", key, name=name, fqn=key, file=file, line=lo, end_line=hi, module=module_of(file),
|
|
256
|
+
lang="php", attrs={"framework": "pest", "suite": suite_of(file), "test": True})
|
|
257
|
+
b.add_edge(tid, sf.id, "TEST_CALLS", file, lo, RESOLVED, via="file setup (beforeEach / uses)")
|
|
258
|
+
moves[sf.id].append((lo, hi, tid))
|
|
259
|
+
n += 1
|
|
260
|
+
if moves:
|
|
261
|
+
b.move_edges(moves)
|
|
262
|
+
return n
|