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,234 @@
|
|
|
1
|
+
"""Member-level re-parse of a Kotlin file that tree-sitter-kotlin cannot parse (#104).
|
|
2
|
+
|
|
3
|
+
tree-sitter's error recovery often turns one construct the grammar lacks (a parenthesized function type with a
|
|
4
|
+
receiver, a `$$"..."` string, a context parameter) into an ERROR that swallows the rest of the class or the file.
|
|
5
|
+
Here the file is split into members by a small lexer (braces, parentheses and brackets outside strings, characters and
|
|
6
|
+
comments): top-level declarations, and recursively the members of class / object / interface bodies. Each member is
|
|
7
|
+
parsed on its own inside the file's skeleton (package, imports, class headers and braces; every other member blanked)
|
|
8
|
+
and a member that still errors is blanked in the final copy. Blanking replaces bytes with spaces and keeps newlines,
|
|
9
|
+
so the other members keep their lines, offsets and text, and the dropped members are reported as error spans."""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
_DECL_START = re.compile(rb"(?:@|/\*|//|(?:(?:public|private|internal|protected|open|abstract|override|suspend|inline|"
|
|
15
|
+
rb"data|sealed|enum|annotation|inner|value|companion|actual|expect|operator|infix|tailrec|"
|
|
16
|
+
rb"external|const|lateinit|final|noinline|crossinline|fun)\b)|(?:fun|val|var|class|object|"
|
|
17
|
+
rb"interface|init|constructor|typealias)\b)")
|
|
18
|
+
_ATTACH = re.compile(rb"(?:@[\w.]+(?:\(.*\))?\s*|/\*.*|//.*|\*.*)$")
|
|
19
|
+
_HEADER = re.compile(rb"(?:package|import)\s")
|
|
20
|
+
_HEADER_CONT = re.compile(rb"(?:(?:public|private|internal|protected)\s+)?(?:@\w+\s+)*constructor\b|:|where\b")
|
|
21
|
+
_CLASSY = re.compile(rb"\b(?:class|object|interface)\b")
|
|
22
|
+
MAX_MEMBERS = 400
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _depths(src: bytes) -> tuple[list[int], list[bool]]:
|
|
26
|
+
"""Per byte: nesting depth of ( [ { outside strings / chars / comments, and whether the byte is code."""
|
|
27
|
+
n = len(src)
|
|
28
|
+
depth = [0] * (n + 1)
|
|
29
|
+
code = [True] * n
|
|
30
|
+
d, i = 0, 0
|
|
31
|
+
tmpl: list[int] = [] # brace depth at which a `${` template inside a string started
|
|
32
|
+
while i < n:
|
|
33
|
+
c = src[i]
|
|
34
|
+
depth[i] = d
|
|
35
|
+
if c == 0x2F and i + 1 < n and src[i + 1] == 0x2F: # // comment
|
|
36
|
+
j = src.find(b"\n", i)
|
|
37
|
+
j = n if j < 0 else j
|
|
38
|
+
for k in range(i, j):
|
|
39
|
+
depth[k], code[k] = d, False
|
|
40
|
+
i = j
|
|
41
|
+
continue
|
|
42
|
+
if c == 0x2F and i + 1 < n and src[i + 1] == 0x2A: # /* nested */ comment
|
|
43
|
+
lvl, j = 1, i + 2
|
|
44
|
+
while j < n and lvl:
|
|
45
|
+
if src[j:j + 2] == b"/*":
|
|
46
|
+
lvl, j = lvl + 1, j + 2
|
|
47
|
+
elif src[j:j + 2] == b"*/":
|
|
48
|
+
lvl, j = lvl - 1, j + 2
|
|
49
|
+
else:
|
|
50
|
+
j += 1
|
|
51
|
+
for k in range(i, min(j, n)):
|
|
52
|
+
depth[k], code[k] = d, False
|
|
53
|
+
i = j
|
|
54
|
+
continue
|
|
55
|
+
if c == 0x27: # 'c' / '\n' / '\u0041'
|
|
56
|
+
m = re.match(rb"'(?:\\u[0-9a-fA-F]{4}|\\.|[^'\\\n])'", src[i:i + 8])
|
|
57
|
+
if m:
|
|
58
|
+
for k in range(i, i + m.end()):
|
|
59
|
+
depth[k], code[k] = d, False
|
|
60
|
+
i += m.end()
|
|
61
|
+
continue
|
|
62
|
+
if c == 0x22: # "..." / """..."""
|
|
63
|
+
raw = src[i:i + 3] == b'"""'
|
|
64
|
+
j = i + (3 if raw else 1)
|
|
65
|
+
while j < n:
|
|
66
|
+
if not raw and src[j] == 0x5C:
|
|
67
|
+
j += 2
|
|
68
|
+
continue
|
|
69
|
+
if raw and src[j:j + 3] == b'"""':
|
|
70
|
+
j += 3
|
|
71
|
+
while j < n and src[j] == 0x22:
|
|
72
|
+
j += 1
|
|
73
|
+
break
|
|
74
|
+
if not raw and src[j] == 0x22:
|
|
75
|
+
j += 1
|
|
76
|
+
break
|
|
77
|
+
if not raw and src[j] == 0x0A:
|
|
78
|
+
break
|
|
79
|
+
if src[j] == 0x24 and j + 1 < n and src[j + 1] == 0x7B: # ${ ... } template: skip balanced
|
|
80
|
+
lvl, j = 1, j + 2
|
|
81
|
+
while j < n and lvl:
|
|
82
|
+
if src[j] == 0x7B:
|
|
83
|
+
lvl += 1
|
|
84
|
+
elif src[j] == 0x7D:
|
|
85
|
+
lvl -= 1
|
|
86
|
+
elif src[j] == 0x22: # a string inside the template
|
|
87
|
+
q = src.find(b'"', j + 1)
|
|
88
|
+
j = n if q < 0 else q
|
|
89
|
+
j += 1
|
|
90
|
+
continue
|
|
91
|
+
j += 1
|
|
92
|
+
for k in range(i, min(j, n)):
|
|
93
|
+
depth[k], code[k] = d, False
|
|
94
|
+
i = j
|
|
95
|
+
continue
|
|
96
|
+
if c in (0x28, 0x5B, 0x7B):
|
|
97
|
+
d += 1
|
|
98
|
+
elif c in (0x29, 0x5D, 0x7D):
|
|
99
|
+
d = max(d - 1, 0)
|
|
100
|
+
i += 1
|
|
101
|
+
depth[n] = d
|
|
102
|
+
return depth, code
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _members(src: bytes, depth, code, lo: int, hi: int, d: int, out: list, level: int = 0):
|
|
106
|
+
"""Split [lo, hi) (inside a body at depth d) into member chunks; recurse into class-like bodies."""
|
|
107
|
+
starts = []
|
|
108
|
+
pos = lo
|
|
109
|
+
while pos < hi:
|
|
110
|
+
e = src.find(b"\n", pos, hi)
|
|
111
|
+
e = hi if e < 0 else e
|
|
112
|
+
s = pos
|
|
113
|
+
while s < e and src[s] in (0x20, 0x09):
|
|
114
|
+
s += 1
|
|
115
|
+
if s < e and depth[s] == d and (code[s] or src[s:s + 2] in (b"/*", b"//")) and src[s] != 0x7D:
|
|
116
|
+
starts.append((pos, s, e))
|
|
117
|
+
pos = e + 1
|
|
118
|
+
cont = set() # `@Ann` lines in front of a `constructor(` that continues a class header
|
|
119
|
+
for k, (pos, s, e) in enumerate(starts):
|
|
120
|
+
if _HEADER_CONT.match(src[s:e]):
|
|
121
|
+
j = k - 1
|
|
122
|
+
while j >= 0 and _ATTACH.match(src[starts[j][1]:starts[j][2]].rstrip()):
|
|
123
|
+
cont.add(j)
|
|
124
|
+
j -= 1
|
|
125
|
+
chunks: list[list] = [] # [start, end, has_body, keep]
|
|
126
|
+
for k, (pos, s, e) in enumerate(starts):
|
|
127
|
+
line = src[s:e].rstrip()
|
|
128
|
+
if d == 0 and code[s] and _HEADER.match(line):
|
|
129
|
+
chunks.append([pos, e, True, True]) # package / import lines stay in the skeleton
|
|
130
|
+
continue
|
|
131
|
+
is_start = bool(_DECL_START.match(line))
|
|
132
|
+
attach = bool(_ATTACH.match(line)) or not code[s]
|
|
133
|
+
if (chunks and not chunks[-1][3] and (k in cont or _HEADER_CONT.match(line))
|
|
134
|
+
and _CLASSY.search(src[chunks[-1][0]:pos])):
|
|
135
|
+
is_start = False # `class A` + `private constructor(...)` / `: B()` on the next line
|
|
136
|
+
if chunks and not chunks[-1][3] and (not is_start or not chunks[-1][2]):
|
|
137
|
+
chunks[-1][1] = e
|
|
138
|
+
chunks[-1][2] = chunks[-1][2] or not attach
|
|
139
|
+
continue
|
|
140
|
+
chunks.append([pos, e, not attach, False])
|
|
141
|
+
# a chunk runs to the next one's start (its body's lines are deeper)
|
|
142
|
+
for k, ch in enumerate(chunks):
|
|
143
|
+
ch[1] = chunks[k + 1][0] if k + 1 < len(chunks) else hi
|
|
144
|
+
for a, b, _, keep in chunks:
|
|
145
|
+
if keep:
|
|
146
|
+
continue
|
|
147
|
+
# strip trailing whitespace-only region so the closing brace of the enclosing body stays outside
|
|
148
|
+
body = None
|
|
149
|
+
head_end = a
|
|
150
|
+
if level < 3:
|
|
151
|
+
j = a
|
|
152
|
+
while j < b:
|
|
153
|
+
if src[j] == 0x7B and depth[j] == d and code[j]:
|
|
154
|
+
body = j
|
|
155
|
+
break
|
|
156
|
+
if src[j] == 0x3D and depth[j] == d and code[j]: # `=`: an initializer, not a body
|
|
157
|
+
break
|
|
158
|
+
j += 1
|
|
159
|
+
if body is not None and not _CLASSY.search(_code_only(src, code, a, body)):
|
|
160
|
+
body = None
|
|
161
|
+
if body is not None:
|
|
162
|
+
close = body + 1
|
|
163
|
+
while close < b and not (src[close] == 0x7D and depth[close] == d + 1 and code[close]):
|
|
164
|
+
close += 1
|
|
165
|
+
if close < b:
|
|
166
|
+
head_end = body + 1
|
|
167
|
+
inner: list = []
|
|
168
|
+
_members(src, depth, code, head_end, close, d + 1, inner, level + 1)
|
|
169
|
+
if inner:
|
|
170
|
+
out.extend(inner)
|
|
171
|
+
continue
|
|
172
|
+
out.append((a, b))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _code_only(src: bytes, code, a: int, b: int) -> bytes:
|
|
176
|
+
return bytes(ch for k, ch in enumerate(src[a:b]) if code[a + k])
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _blank(buf: bytearray, a: int, b: int):
|
|
180
|
+
for k in range(a, b):
|
|
181
|
+
if buf[k] not in (0x0A, 0x0D):
|
|
182
|
+
buf[k] = 0x20
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _err_bytes(root) -> int:
|
|
186
|
+
tot, stack = 0, [root]
|
|
187
|
+
while stack:
|
|
188
|
+
n = stack.pop()
|
|
189
|
+
if n.type == "ERROR" or n.is_missing:
|
|
190
|
+
tot += max(n.end_byte - n.start_byte, 1)
|
|
191
|
+
elif n.has_error:
|
|
192
|
+
stack.extend(n.children)
|
|
193
|
+
return tot
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def reparse_members(parser, psrc: bytes, tree):
|
|
197
|
+
"""(tree, dropped [(first line, last line)], members) for a file whose tree has errors; tree is None when the
|
|
198
|
+
member split does not help (the skeleton itself does not parse, or nothing would be recovered)."""
|
|
199
|
+
depth, code = _depths(psrc)
|
|
200
|
+
chunks: list = []
|
|
201
|
+
_members(psrc, depth, code, 0, len(psrc), 0, chunks)
|
|
202
|
+
if not chunks or len(chunks) > MAX_MEMBERS:
|
|
203
|
+
return None, [], len(chunks)
|
|
204
|
+
skel = bytearray(psrc)
|
|
205
|
+
for a, b in chunks:
|
|
206
|
+
_blank(skel, a, b)
|
|
207
|
+
if parser.parse(bytes(skel)).root_node.has_error:
|
|
208
|
+
return None, [], len(chunks)
|
|
209
|
+
bad = []
|
|
210
|
+
for a, b in chunks:
|
|
211
|
+
t = bytearray(skel)
|
|
212
|
+
t[a:b] = psrc[a:b]
|
|
213
|
+
if parser.parse(bytes(t)).root_node.has_error:
|
|
214
|
+
bad.append((a, b))
|
|
215
|
+
if not bad:
|
|
216
|
+
return None, [], len(chunks)
|
|
217
|
+
out = bytearray(psrc)
|
|
218
|
+
for a, b in bad:
|
|
219
|
+
_blank(out, a, b)
|
|
220
|
+
nt = parser.parse(bytes(out))
|
|
221
|
+
lost = sum(b - a for a, b in bad) + _err_bytes(nt.root_node)
|
|
222
|
+
if lost >= _err_bytes(tree.root_node):
|
|
223
|
+
return None, [], len(chunks)
|
|
224
|
+
dropped = []
|
|
225
|
+
for a, b in bad:
|
|
226
|
+
s = a
|
|
227
|
+
while s < b and psrc[s] in b" \t\r\n":
|
|
228
|
+
s += 1
|
|
229
|
+
e = b
|
|
230
|
+
while e > s and psrc[e - 1] in b" \t\r\n":
|
|
231
|
+
e -= 1
|
|
232
|
+
if e > s:
|
|
233
|
+
dropped.append((psrc.count(b"\n", 0, s) + 1, psrc.count(b"\n", 0, e - 1) + 1))
|
|
234
|
+
return nt, dropped, len(chunks)
|
|
File without changes
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Laravel broadcasting: channel authorization, broadcast events and the auth route.
|
|
2
|
+
|
|
3
|
+
* `Broadcast::channel('orders.{orderId}', callback|ChannelClass, ['guards' => ...])` -> node `channel:<pattern>`
|
|
4
|
+
(entry_kind channel_auth). A closure callback's calls / reads are re-attributed from the defining file to the
|
|
5
|
+
channel node, so `callees channel:orders.{orderId}` lists exactly the checks that decide who may join. A channel
|
|
6
|
+
class gets HANDLED_BY -> its join() method. The callback's first parameter (the authenticated user) is typed as
|
|
7
|
+
the application's User model when it carries no type.
|
|
8
|
+
* The broadcasting auth route: `Broadcast::routes([...])`, `->withBroadcasting(path, [...])`,
|
|
9
|
+
`->withRouting(channels: ...)` synthesize `POST /broadcasting/auth` (prefix + middleware, default `web`); an
|
|
10
|
+
explicit route to BroadcastController / `.../broadcasting/auth` is used as is. Each gets AUTHORIZES_CHANNEL ->
|
|
11
|
+
every declared channel.
|
|
12
|
+
* ShouldBroadcast(Now) events: `event:<FQCN>` (attrs broadcast, broadcast_as) with HANDLED_BY -> broadcastOn /
|
|
13
|
+
broadcastWith / broadcastAs / broadcastWhen, and BROADCASTS_ON -> the channel(s) named in broadcastOn() (string
|
|
14
|
+
names, `new Channel/PrivateChannel/PresenceChannel(...)`, helper methods that build the name, and, when the name
|
|
15
|
+
is a constructor argument, the value passed at each `new Event(...)` / `Event::dispatch(...)` site). Names that
|
|
16
|
+
match no declared pattern become `channel:<name>` nodes with declared=false.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
|
|
23
|
+
from ...core.model import EXACT, HEURISTIC, RESOLVED
|
|
24
|
+
from ..php.plugin import PhpFunc, module_of
|
|
25
|
+
from ..php.strings import StrEval, channel_match, fully_dynamic, same_shape
|
|
26
|
+
|
|
27
|
+
BROADCAST_FACADES = {"illuminate\\support\\facades\\broadcast", "broadcast"}
|
|
28
|
+
CHANNEL_CLASSES = {"illuminate\\broadcasting\\channel": "public", "illuminate\\broadcasting\\privatechannel": "private",
|
|
29
|
+
"illuminate\\broadcasting\\presencechannel": "presence",
|
|
30
|
+
"illuminate\\broadcasting\\encryptedprivatechannel": "private-encrypted"}
|
|
31
|
+
SHOULD_BROADCAST = "Illuminate\\Contracts\\Broadcasting\\ShouldBroadcast"
|
|
32
|
+
SHOULD_BROADCAST_NOW = "Illuminate\\Contracts\\Broadcasting\\ShouldBroadcastNow"
|
|
33
|
+
USER_BASE = "Illuminate\\Foundation\\Auth\\User"
|
|
34
|
+
AUTH_URI_RE = re.compile(r"(^|/)broadcasting/auth$")
|
|
35
|
+
THIS_PH = re.compile(r"\{this\.(\w+)\}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _classconst(d):
|
|
39
|
+
return d.get("class") if d and d.get("k") == "classconst" else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _opt(arr, key):
|
|
43
|
+
if not arr or arr.get("k") != "arr":
|
|
44
|
+
return None
|
|
45
|
+
for it in arr.get("items") or []:
|
|
46
|
+
k = it.get("key") or {}
|
|
47
|
+
if k.get("k") == "str" and k.get("v") == key:
|
|
48
|
+
return it.get("v")
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BroadcastAnalysis:
|
|
53
|
+
def __init__(self, lp, prog):
|
|
54
|
+
self.lp, self.prog = lp, prog
|
|
55
|
+
self.sev = StrEval(prog)
|
|
56
|
+
self.defs: list[tuple[PhpFunc, dict]] = []
|
|
57
|
+
self.route_calls: list[tuple[PhpFunc, dict, str, object]] = []
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------------ before inference
|
|
60
|
+
def _user_model(self) -> str | None:
|
|
61
|
+
prog = self.prog
|
|
62
|
+
cands = [c.fqcn for c in prog.classes.values()
|
|
63
|
+
if c.kind == "class" and not prog.is_test_class(c.fqcn) and c.fqcn != USER_BASE and prog.is_a(c.fqcn, USER_BASE)]
|
|
64
|
+
if not cands:
|
|
65
|
+
return None
|
|
66
|
+
cands.sort(key=lambda q: (q.split("\\")[-1] != "User", not q.startswith("App\\Models\\"), q))
|
|
67
|
+
return cands[0]
|
|
68
|
+
|
|
69
|
+
def register(self) -> None:
|
|
70
|
+
for fn in self.prog.all_funcs:
|
|
71
|
+
for f in fn.facts:
|
|
72
|
+
if f.get("t") != "call":
|
|
73
|
+
continue
|
|
74
|
+
m = (f.get("m") or "").lower()
|
|
75
|
+
if f["kind"] == "static" and (f.get("class") or "").lower().lstrip("\\") in BROADCAST_FACADES:
|
|
76
|
+
if m == "channel" and f.get("args"):
|
|
77
|
+
self.defs.append((fn, f))
|
|
78
|
+
elif m == "routes":
|
|
79
|
+
self.route_calls.append((fn, f, "Broadcast::routes", 0))
|
|
80
|
+
elif f["kind"] == "method" and m == "withbroadcasting":
|
|
81
|
+
self.route_calls.append((fn, f, "->withBroadcasting", 1))
|
|
82
|
+
elif f["kind"] == "method" and m == "withrouting" and any(a.get("named") == "channels" for a in f.get("args") or []):
|
|
83
|
+
self.route_calls.append((fn, f, "->withRouting(channels:)", None))
|
|
84
|
+
user = self._user_model()
|
|
85
|
+
if not user:
|
|
86
|
+
return
|
|
87
|
+
for fn, f in self.defs:
|
|
88
|
+
args = f["args"]
|
|
89
|
+
if len(args) < 2 or args[1].get("k") != "closure":
|
|
90
|
+
continue
|
|
91
|
+
lo, hi = f["line"], f.get("end", f["line"])
|
|
92
|
+
ctx = next((g["ctx"] for g in fn.facts if (g.get("ctx") or {}).get("line") == lo
|
|
93
|
+
and (g["ctx"].get("m") or "").lower() == "channel"), None)
|
|
94
|
+
if not ctx or not ctx.get("cparams"):
|
|
95
|
+
continue
|
|
96
|
+
p = ctx["cparams"][0]
|
|
97
|
+
typed = any(g.get("t") == "vartype" and g.get("var") == p and g.get("src") == "closure_param" and lo <= g.get("line", 0) <= hi
|
|
98
|
+
for g in fn.facts)
|
|
99
|
+
if not typed:
|
|
100
|
+
fn.facts.append({"t": "vartype", "var": p, "types": [user], "src": "channel_user", "line": lo})
|
|
101
|
+
|
|
102
|
+
# ------------------------------------------------------------------ after reference emission
|
|
103
|
+
def contribute(self) -> dict:
|
|
104
|
+
b, prog = self.lp.b, self.prog
|
|
105
|
+
st = defaultdict(int)
|
|
106
|
+
declared: list[tuple[str, str]] = []
|
|
107
|
+
moves = defaultdict(list)
|
|
108
|
+
for fn, f in self.defs:
|
|
109
|
+
args = f["args"]
|
|
110
|
+
pats = self.sev.eval(args[0], fn)
|
|
111
|
+
pat = pats[0] if pats else None
|
|
112
|
+
if not pat or fully_dynamic(pat):
|
|
113
|
+
st["channels_dynamic_name"] += 1
|
|
114
|
+
continue
|
|
115
|
+
pat = THIS_PH.sub(lambda m: "{" + m.group(1) + "}", pat)
|
|
116
|
+
cb = args[1] if len(args) > 1 else None
|
|
117
|
+
lo, hi = f["line"], f.get("end", f["line"])
|
|
118
|
+
guards = []
|
|
119
|
+
g = _opt(args[2] if len(args) > 2 else None, "guards")
|
|
120
|
+
if g:
|
|
121
|
+
guards = [x for x in self.sev.eval(g, fn) if "{" not in x] if g.get("k") != "arr" else \
|
|
122
|
+
[it["v"]["v"] for it in g.get("items") or [] if (it.get("v") or {}).get("k") == "str"]
|
|
123
|
+
attrs = {"pattern": pat, "params": re.findall(r"\{(\w+)\}", pat), "declared": True}
|
|
124
|
+
if guards:
|
|
125
|
+
attrs["guards"] = guards
|
|
126
|
+
presence, join = False, None
|
|
127
|
+
if cb and cb.get("k") == "closure":
|
|
128
|
+
attrs["callback"] = f"closure {fn.file}:{lo}"
|
|
129
|
+
presence = any(x.get("t") == "return" and (x.get("expr") or {}).get("k") == "arr" and lo <= x.get("line", 0) <= hi
|
|
130
|
+
for x in fn.facts)
|
|
131
|
+
elif _classconst(cb) and prog.cls(_classconst(cb)):
|
|
132
|
+
cc = prog.cls(_classconst(cb))
|
|
133
|
+
attrs["handler"] = cc.fqcn
|
|
134
|
+
join = prog.find_method(cc.fqcn, "join")
|
|
135
|
+
presence = bool(join and any(x.get("t") == "return" and (x.get("expr") or {}).get("k") == "arr" for x in join.facts))
|
|
136
|
+
elif _classconst(cb):
|
|
137
|
+
attrs["handler"] = _classconst(cb)
|
|
138
|
+
attrs["visibility"] = "presence" if presence else "private"
|
|
139
|
+
nid = f"channel:{pat}"
|
|
140
|
+
if b.has(nid) and b.nodes[nid].attrs.get("declared"):
|
|
141
|
+
st["channels_duplicate"] += 1
|
|
142
|
+
b.nodes[nid].attrs.setdefault("also_defined", []).append(f"{fn.file}:{lo}")
|
|
143
|
+
continue
|
|
144
|
+
b.add_node("channel", pat, name=pat, file=fn.file, line=lo, end_line=hi, module=module_of(fn.file), lang="php",
|
|
145
|
+
entry_kind="channel_auth", attrs=attrs)
|
|
146
|
+
b.nodes[nid].entry_kind = "channel_auth"
|
|
147
|
+
declared.append((pat, nid))
|
|
148
|
+
moves[fn.id].append((lo, hi, nid))
|
|
149
|
+
if join:
|
|
150
|
+
b.add_edge(nid, join.id, "HANDLED_BY", join.file, join.line, EXACT, via="channel class")
|
|
151
|
+
st["channels"] += 1
|
|
152
|
+
if moves:
|
|
153
|
+
st["callback_edges_moved"] = b.move_edges(moves)
|
|
154
|
+
st["auth_routes"] = self._auth_routes(declared)
|
|
155
|
+
self._events(declared, st)
|
|
156
|
+
return dict(st)
|
|
157
|
+
|
|
158
|
+
def _auth_routes(self, declared) -> int:
|
|
159
|
+
b, prog, lp = self.lp.b, self.prog, self.lp
|
|
160
|
+
rids = []
|
|
161
|
+
for f, r in lp.records.items():
|
|
162
|
+
for rt in r.get("routes") or []:
|
|
163
|
+
act = rt.get("action") or {}
|
|
164
|
+
cls = (act.get("class") or "").lstrip("\\")
|
|
165
|
+
if cls.endswith("BroadcastController") or AUTH_URI_RE.search(rt.get("full_uri") or ""):
|
|
166
|
+
for verb in rt["methods"]:
|
|
167
|
+
rid = f"route:{verb.upper()} {rt['full_uri']}"
|
|
168
|
+
if b.has(rid):
|
|
169
|
+
b.nodes[rid].attrs["broadcast_auth"] = True
|
|
170
|
+
rids.append(rid)
|
|
171
|
+
aliases = lp._middleware_aliases()
|
|
172
|
+
for fn, f, via, idx in self.route_calls:
|
|
173
|
+
args = f.get("args") or []
|
|
174
|
+
opts = None
|
|
175
|
+
if idx is not None:
|
|
176
|
+
opts = next((a for a in args if a.get("named") == "attributes"), None)
|
|
177
|
+
if opts is None and idx < len(args) and not args[idx].get("named"):
|
|
178
|
+
opts = args[idx]
|
|
179
|
+
prefix, mws = "", ["web"]
|
|
180
|
+
if opts and opts.get("k") == "arr":
|
|
181
|
+
pv = _opt(opts, "prefix")
|
|
182
|
+
if pv:
|
|
183
|
+
prefix = (self.sev.eval(pv, fn) or [""])[0]
|
|
184
|
+
mv = _opt(opts, "middleware")
|
|
185
|
+
if mv is not None:
|
|
186
|
+
if mv.get("k") == "arr":
|
|
187
|
+
mws = [x for it in mv.get("items") or [] for x in self.sev.eval(it.get("v"), fn)[:1]]
|
|
188
|
+
else:
|
|
189
|
+
mws = self.sev.eval(mv, fn)[:1]
|
|
190
|
+
uri = "/" + "/".join(x.strip("/") for x in (prefix, "broadcasting/auth") if x.strip("/"))
|
|
191
|
+
key = f"POST {uri}"
|
|
192
|
+
rid = b.add_node("route", key, name=key, file=fn.file, line=f["line"], module=module_of(fn.file), lang="php",
|
|
193
|
+
entry_kind="http_route",
|
|
194
|
+
attrs={"name": None, "middleware": mws, "uri": uri, "method": "POST", "broadcast_auth": True,
|
|
195
|
+
"synthesized": f"{via} {fn.file}:{f['line']}"})
|
|
196
|
+
b.nodes[rid].entry_kind = b.nodes[rid].entry_kind or "http_route"
|
|
197
|
+
for mw in mws:
|
|
198
|
+
alias = mw.split(":")[0]
|
|
199
|
+
cls = aliases.get(alias) or (mw if prog.cls(mw) else None)
|
|
200
|
+
if cls and prog.cls(cls):
|
|
201
|
+
h = prog.find_method(cls, "handle")
|
|
202
|
+
if h:
|
|
203
|
+
b.add_edge(rid, h.id, "USES_MIDDLEWARE", fn.file, f["line"], RESOLVED, alias=alias)
|
|
204
|
+
rids.append(rid)
|
|
205
|
+
for rid in dict.fromkeys(rids):
|
|
206
|
+
for pat, nid in declared:
|
|
207
|
+
n = b.nodes[nid]
|
|
208
|
+
b.add_edge(rid, nid, "AUTHORIZES_CHANNEL", n.file, n.line, RESOLVED, via="broadcast auth")
|
|
209
|
+
return len(set(rids))
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------ events
|
|
212
|
+
def _channel_news(self, cls: str, start: PhpFunc) -> list[tuple[str, str, PhpFunc, int]]:
|
|
213
|
+
"""(pattern, visibility, fn, line) for each channel constructed / named in broadcastOn and the $this-methods
|
|
214
|
+
it calls (two levels)."""
|
|
215
|
+
out, seen, todo = [], set(), [(start, 0)]
|
|
216
|
+
while todo:
|
|
217
|
+
fn, depth = todo.pop(0)
|
|
218
|
+
if fn.id in seen:
|
|
219
|
+
continue
|
|
220
|
+
seen.add(fn.id)
|
|
221
|
+
for f in fn.facts:
|
|
222
|
+
t = f.get("t")
|
|
223
|
+
if t == "new" and (f.get("class") or "").lower().lstrip("\\") in CHANNEL_CLASSES and f.get("args"):
|
|
224
|
+
vis = CHANNEL_CLASSES[f["class"].lower().lstrip("\\")]
|
|
225
|
+
for p in self.sev.eval(f["args"][0], fn):
|
|
226
|
+
out.append((p, vis, fn, f["line"]))
|
|
227
|
+
elif t == "return" and fn is start and not f.get("ctx"):
|
|
228
|
+
e = f.get("expr") or {}
|
|
229
|
+
items = [it.get("v") for it in e.get("items") or []] if e.get("k") == "arr" else [e]
|
|
230
|
+
for it in items:
|
|
231
|
+
if it and it.get("k") in ("str", "interp", "concat"):
|
|
232
|
+
for p in self.sev.eval(it, fn):
|
|
233
|
+
out.append((p, "public", fn, f["line"]))
|
|
234
|
+
elif t == "call" and f["kind"] == "method" and (f.get("recv") or {}).get("k") == "this" and depth < 2:
|
|
235
|
+
m = self.prog.find_method(cls, f.get("m"))
|
|
236
|
+
if m and not m.abstract:
|
|
237
|
+
todo.append((m, depth + 1))
|
|
238
|
+
return out
|
|
239
|
+
|
|
240
|
+
def _sites(self, cls: str, prop: str) -> list[tuple[str, PhpFunc, int]]:
|
|
241
|
+
"""Values of constructor argument `prop` at each `new Cls(...)` / `Cls::dispatch(...)` site."""
|
|
242
|
+
ctor = self.prog.find_method(cls, "__construct")
|
|
243
|
+
if not ctor:
|
|
244
|
+
return []
|
|
245
|
+
idx = next((i for i, p in enumerate(ctor.params) if p["name"] == prop), None)
|
|
246
|
+
if idx is None:
|
|
247
|
+
return []
|
|
248
|
+
out = []
|
|
249
|
+
for fn in self.prog.all_funcs:
|
|
250
|
+
for f in fn.facts:
|
|
251
|
+
hit = (f.get("t") == "new" and f.get("class") == cls) or \
|
|
252
|
+
(f.get("t") == "call" and f["kind"] == "static" and f.get("class") == cls
|
|
253
|
+
and (f.get("m") or "").lower() in ("dispatch", "broadcast", "dispatchif", "dispatchunless"))
|
|
254
|
+
if not hit:
|
|
255
|
+
continue
|
|
256
|
+
args = f.get("args") or []
|
|
257
|
+
a = next((x for x in args if x.get("named") == prop), None)
|
|
258
|
+
if a is None and idx < len(args) and not args[idx].get("named"):
|
|
259
|
+
a = args[idx]
|
|
260
|
+
if a is None:
|
|
261
|
+
continue
|
|
262
|
+
for p in self.sev.eval(a, fn):
|
|
263
|
+
out.append((p, fn, f["line"]))
|
|
264
|
+
return out
|
|
265
|
+
|
|
266
|
+
def _events(self, declared, st):
|
|
267
|
+
b, prog = self.lp.b, self.prog
|
|
268
|
+
bevents = {}
|
|
269
|
+
for c in list(prog.classes.values()):
|
|
270
|
+
if c.kind != "class" or c.abstract or prog.is_test_class(c.fqcn):
|
|
271
|
+
continue
|
|
272
|
+
now = prog.is_a(c.fqcn, SHOULD_BROADCAST_NOW)
|
|
273
|
+
if not (now or prog.is_a(c.fqcn, SHOULD_BROADCAST)):
|
|
274
|
+
continue
|
|
275
|
+
attrs = {"broadcast": True}
|
|
276
|
+
if now:
|
|
277
|
+
attrs["broadcast_now"] = True
|
|
278
|
+
ba = prog.find_method(c.fqcn, "broadcastAs")
|
|
279
|
+
if ba:
|
|
280
|
+
names = [x for r in ba.facts if r.get("t") == "return" and not r.get("ctx") for x in self.sev.eval(r["expr"], ba)]
|
|
281
|
+
if names and "{" not in names[0]:
|
|
282
|
+
attrs["broadcast_as"] = names[0]
|
|
283
|
+
eid = b.add_node("event", c.fqcn, name=c.fqcn.split("\\")[-1], fqn=c.fqcn, file=c.file, line=c.line,
|
|
284
|
+
end_line=c.end_line, module=module_of(c.file), lang="php", attrs=attrs)
|
|
285
|
+
n = b.nodes[eid]
|
|
286
|
+
n.file, n.line = n.file or c.file, n.line or c.line
|
|
287
|
+
bevents[f"{c.kind}:{c.fqcn}"] = eid
|
|
288
|
+
st["broadcast_events"] += 1
|
|
289
|
+
for mn in ("broadcastOn", "broadcastWith", "broadcastAs", "broadcastWhen"):
|
|
290
|
+
m = prog.find_method(c.fqcn, mn)
|
|
291
|
+
if m and not m.abstract:
|
|
292
|
+
b.add_edge(eid, m.id, "HANDLED_BY", m.file, m.line, EXACT, via="broadcast")
|
|
293
|
+
bo = prog.find_method(c.fqcn, "broadcastOn")
|
|
294
|
+
if not bo or bo.abstract:
|
|
295
|
+
continue
|
|
296
|
+
unresolved = []
|
|
297
|
+
for pat, vis, fn, line in self._channel_news(c.fqcn, bo):
|
|
298
|
+
resolved = []
|
|
299
|
+
props = THIS_PH.findall(pat)
|
|
300
|
+
if fully_dynamic(pat) and len(props) == 1:
|
|
301
|
+
for val, sfn, sline in self._sites(c.fqcn, props[0]):
|
|
302
|
+
resolved.append((THIS_PH.sub(lambda m: "{" + m.group(1) + "}", pat.replace("{this." + props[0] + "}", val)),
|
|
303
|
+
{"site": f"{sfn.file}:{sline}", "site_fn": sfn.id}))
|
|
304
|
+
if not resolved:
|
|
305
|
+
resolved = [(THIS_PH.sub(lambda m: "{" + m.group(1) + "}", pat), {})]
|
|
306
|
+
# `{?}` (unknown) alternatives of a name that also resolved to a named placeholder add nothing
|
|
307
|
+
resolved = [(nm, ex) for nm, ex in resolved
|
|
308
|
+
if "{?}" not in nm or not any(o != nm and "{?}" not in o and same_shape(nm, o) and ex == oe
|
|
309
|
+
for o, oe in resolved)]
|
|
310
|
+
for name, extra in resolved:
|
|
311
|
+
if fully_dynamic(name):
|
|
312
|
+
unresolved.append(name)
|
|
313
|
+
st["channel_names_unresolved"] += 1
|
|
314
|
+
continue
|
|
315
|
+
hits = [nid for p, nid in declared if same_shape(name, p)]
|
|
316
|
+
conf = RESOLVED
|
|
317
|
+
if not hits:
|
|
318
|
+
hits = [nid for p, nid in declared if channel_match(name, p)]
|
|
319
|
+
conf = RESOLVED if len(hits) == 1 else HEURISTIC
|
|
320
|
+
if not hits:
|
|
321
|
+
nid = b.add_node("channel", name, name=name, lang="php",
|
|
322
|
+
attrs={"pattern": name, "declared": False, "visibility": vis})
|
|
323
|
+
hits = [nid]
|
|
324
|
+
conf = RESOLVED
|
|
325
|
+
st["channels_undeclared"] += 1
|
|
326
|
+
ef, el = (extra["site"].rsplit(":", 1)[0], int(extra["site"].rsplit(":", 1)[1])) if extra else (fn.file, line)
|
|
327
|
+
for nid in hits:
|
|
328
|
+
b.add_edge(eid, nid, "BROADCASTS_ON", ef, el, conf, name=name, visibility=vis, at=f"{fn.file}:{line}", **extra)
|
|
329
|
+
st["broadcasts_on"] += 1
|
|
330
|
+
if unresolved:
|
|
331
|
+
n.attrs["unresolved_channels"] = sorted(set(unresolved))
|
|
332
|
+
# a callback whose return is not an obvious array (arrow fn returning a model / query) still serves a presence
|
|
333
|
+
# channel when every event publishes on it as a PresenceChannel
|
|
334
|
+
pub_vis = {}
|
|
335
|
+
for e in b.edges.values():
|
|
336
|
+
if e.kind == "BROADCASTS_ON":
|
|
337
|
+
pub_vis.setdefault(e.dst, set()).add(e.attrs.get("visibility"))
|
|
338
|
+
for nid, vs in pub_vis.items():
|
|
339
|
+
n = b.nodes.get(nid)
|
|
340
|
+
if n and n.attrs.get("declared") and n.attrs.get("visibility") == "private" and vs == {"presence"}:
|
|
341
|
+
n.attrs["visibility"] = "presence"
|
|
342
|
+
n.attrs["visibility_from"] = "publishers (PresenceChannel)"
|
|
343
|
+
# an instantiated broadcast event is (almost always) broadcast: `$e = new X(...); broadcast($e)->toOthers()`
|
|
344
|
+
have = {(e.src, e.dst) for e in b.edges.values() if e.kind == "DISPATCHES"}
|
|
345
|
+
for e in list(b.edges.values()):
|
|
346
|
+
if e.kind == "INSTANTIATES" and e.dst in bevents and (e.src, bevents[e.dst]) not in have:
|
|
347
|
+
if e.src.startswith("method:") and e.src.split(":", 1)[1].split("::")[0] == e.dst.split(":", 1)[1]:
|
|
348
|
+
continue
|
|
349
|
+
b.add_edge(e.src, bevents[e.dst], "DISPATCHES", e.file, e.line, RESOLVED, via="instantiation (broadcast event)")
|
|
350
|
+
have.add((e.src, bevents[e.dst]))
|
|
351
|
+
st["dispatches_from_instantiation"] += 1
|