leyline-code 0.8.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.
- leyline/__init__.py +3 -0
- leyline/adapters/__init__.py +19 -0
- leyline/adapters/csharp.py +1026 -0
- leyline/adapters/generic.py +844 -0
- leyline/adapters/python.py +929 -0
- leyline/adapters/typescript.py +1512 -0
- leyline/affected.py +509 -0
- leyline/agent_skills.py +311 -0
- leyline/change.py +500 -0
- leyline/channels.py +1782 -0
- leyline/cli.py +1029 -0
- leyline/cluster.py +149 -0
- leyline/context.py +531 -0
- leyline/coupling.py +485 -0
- leyline/coverage.py +376 -0
- leyline/diagrams.py +680 -0
- leyline/diff.py +1078 -0
- leyline/drift.py +747 -0
- leyline/exact.py +410 -0
- leyline/explain.py +973 -0
- leyline/export.py +673 -0
- leyline/fncoupling.py +328 -0
- leyline/formats.py +923 -0
- leyline/grade.py +207 -0
- leyline/incremental.py +931 -0
- leyline/indexer.py +3733 -0
- leyline/learnings.py +743 -0
- leyline/loop.py +584 -0
- leyline/model.py +142 -0
- leyline/outline.py +1066 -0
- leyline/packing.py +86 -0
- leyline/patterns.py +478 -0
- leyline/pr.py +1249 -0
- leyline/props.py +255 -0
- leyline/query.py +683 -0
- leyline/quick.py +853 -0
- leyline/related.py +266 -0
- leyline/removal.py +231 -0
- leyline/renames.py +231 -0
- leyline/rereview.py +375 -0
- leyline/roslyn_refs/Program.cs +179 -0
- leyline/roslyn_refs/RoslynRefs.csproj +16 -0
- leyline/rules.py +171 -0
- leyline/schema.sql +178 -0
- leyline/scip_pb2.py +97 -0
- leyline/server.py +1230 -0
- leyline/skills/leyline-adversarial-review/SKILL.md +121 -0
- leyline/skills/leyline-ask/SKILL.md +57 -0
- leyline/skills/leyline-change-impact/SKILL.md +79 -0
- leyline/skills/leyline-explain-flow/SKILL.md +70 -0
- leyline/skills/leyline-explore-module/SKILL.md +55 -0
- leyline/skills/leyline-pr-review/SKILL.md +77 -0
- leyline/skills/leyline-quick-change/SKILL.md +63 -0
- leyline/skills/leyline-spec/SKILL.md +179 -0
- leyline/skills/leyline-tour/SKILL.md +46 -0
- leyline/spec.py +1819 -0
- leyline/store.py +560 -0
- leyline/tours.py +352 -0
- leyline/verdicts.py +390 -0
- leyline/viewer/viewer.html +1622 -0
- leyline_code-0.8.1.dist-info/METADATA +1325 -0
- leyline_code-0.8.1.dist-info/RECORD +65 -0
- leyline_code-0.8.1.dist-info/WHEEL +4 -0
- leyline_code-0.8.1.dist-info/entry_points.txt +2 -0
- leyline_code-0.8.1.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
"""C# adapter: tree-sitter structure, with enough local type inference to resolve most calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import tree_sitter_c_sharp
|
|
9
|
+
from tree_sitter import Language, Parser
|
|
10
|
+
|
|
11
|
+
from .. import channels
|
|
12
|
+
from ..model import FieldUse, CallSite, Edge, Endpoint, EventUse, FileResult, ImportRef, Node, TypeRef
|
|
13
|
+
|
|
14
|
+
NAME = "tree-sitter-c-sharp"
|
|
15
|
+
VERSION = "0.1"
|
|
16
|
+
LANGUAGE = "csharp"
|
|
17
|
+
EXTENSIONS = (".cs",)
|
|
18
|
+
|
|
19
|
+
_parser = Parser(Language(tree_sitter_c_sharp.language()))
|
|
20
|
+
|
|
21
|
+
TYPE_DECLS = {
|
|
22
|
+
"class_declaration": "class",
|
|
23
|
+
"struct_declaration": "struct",
|
|
24
|
+
"interface_declaration": "interface",
|
|
25
|
+
"enum_declaration": "enum",
|
|
26
|
+
"record_declaration": "record",
|
|
27
|
+
"record_struct_declaration": "record",
|
|
28
|
+
"delegate_declaration": "delegate",
|
|
29
|
+
}
|
|
30
|
+
CALLABLE_DECLS = {
|
|
31
|
+
"method_declaration", "constructor_declaration", "destructor_declaration",
|
|
32
|
+
"operator_declaration", "conversion_operator_declaration", "local_function_statement",
|
|
33
|
+
}
|
|
34
|
+
ACCESS = ("public", "private", "protected", "internal")
|
|
35
|
+
LAMBDAS = ("lambda_expression", "anonymous_method_expression")
|
|
36
|
+
# `this` and `base` as the grammar names them: this_expression in older releases, this from 0.23 on.
|
|
37
|
+
THIS = ("this_expression", "this")
|
|
38
|
+
BASE = ("base_expression", "base")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
LITERALS = {"integer_literal": "int", "real_literal": "double", "boolean_literal": "bool", "character_literal": "char",
|
|
42
|
+
"string_literal": "string", "verbatim_string_literal": "string", "raw_string_literal": "string",
|
|
43
|
+
"interpolated_string_expression": "string"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
HTTP_CLIENT = {"GetAsync": "GET", "GetStringAsync": "GET", "GetFromJsonAsync": "GET", "GetStreamAsync": "GET",
|
|
47
|
+
"GetByteArrayAsync": "GET", "PostAsync": "POST", "PostAsJsonAsync": "POST", "PutAsync": "PUT",
|
|
48
|
+
"PutAsJsonAsync": "PUT", "DeleteAsync": "DELETE", "PatchAsync": "PATCH"}
|
|
49
|
+
HTTP_MAP = {"MapGet": "GET", "MapPost": "POST", "MapPut": "PUT", "MapDelete": "DELETE", "MapPatch": "PATCH"}
|
|
50
|
+
# An attribute with a template, first in its list or after another: [HttpGet("x")], [Authorize, HttpPost("x")].
|
|
51
|
+
HTTP_ATTR = re.compile(r"[\[,]\s*(Http(Get|Post|Put|Delete|Patch)|Route)\s*\(\s*\"([^\"]*)\"")
|
|
52
|
+
HTTP_BARE = re.compile(r"\[\s*(?:[^\]]*,\s*)?Http(Get|Post|Put|Delete|Patch)\s*(?:\(\s*\)\s*)?[\],]")
|
|
53
|
+
FILE_CALL = re.compile(r"(?:^|\.)(File|Directory|FileAccess)\.(\w+)$")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _cs_strings(node) -> list[str]:
|
|
57
|
+
"""String literals under a node. An interpolated hole becomes {}."""
|
|
58
|
+
out, stack = [], [node]
|
|
59
|
+
while stack:
|
|
60
|
+
n = stack.pop()
|
|
61
|
+
if n.type == "interpolated_string_expression":
|
|
62
|
+
parts = []
|
|
63
|
+
for c in n.children:
|
|
64
|
+
if c.type == "interpolation":
|
|
65
|
+
parts.append("{}")
|
|
66
|
+
elif c.is_named and not c.type.startswith("interpolation_"): # not the $ that opens it
|
|
67
|
+
parts.append(_text(c))
|
|
68
|
+
out.append("".join(parts))
|
|
69
|
+
elif n.type in ("string_literal", "verbatim_string_literal", "raw_string_literal"):
|
|
70
|
+
body = "".join(_text(c) for c in n.children if c.type == "string_literal_content" or c.type.endswith("content"))
|
|
71
|
+
out.append(body or _text(n).lstrip("@").strip('"'))
|
|
72
|
+
else:
|
|
73
|
+
stack.extend(reversed(n.children))
|
|
74
|
+
return [x for x in out if x]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Calls whose first string is never a request: logging, string work, routing and parsing.
|
|
78
|
+
NOT_REQUESTS = frozenset("""WriteLine Write Log LogInformation LogDebug LogWarning LogError LogTrace LogCritical Format
|
|
79
|
+
Concat Join Split Replace StartsWith EndsWith Contains IndexOf Equals Compare Parse TryParse Match IsMatch Matches
|
|
80
|
+
Combine GetFullPath Exists Map MapGroup MapFallback UseRouting Route Redirect RedirectPermanent Throw Assert
|
|
81
|
+
Created CreatedAtRoute CreatedAtAction Accepted AcceptedAtRoute LocalRedirect AddRoute MapForwarder""".split())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _relative_url(path: str):
|
|
85
|
+
"""A request path with no leading slash, said against a client's base address: "api/items/1" -> /api/items/1.
|
|
86
|
+
None for anything that is not plainly a path of two or more parts."""
|
|
87
|
+
path = path[2:] if path.startswith("{}/") else path
|
|
88
|
+
path = path.split("?", 1)[0]
|
|
89
|
+
if not path or path.startswith(("http:", "https:")) or any(ch.isspace() for ch in path):
|
|
90
|
+
return None
|
|
91
|
+
parts = [p for p in path.split("/") if p]
|
|
92
|
+
if len(parts) < 2 or "." in parts[0] or "." in parts[-1] or not re.fullmatch(r"[\w\-{}:]+", parts[0]):
|
|
93
|
+
return None
|
|
94
|
+
return "/" + "/".join(parts)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _controller_prefix(method_node, method_name: str):
|
|
98
|
+
"""The route an ASP.NET controller's [Route("api/[controller]")] gives its actions, or None without one."""
|
|
99
|
+
cls = method_node.parent
|
|
100
|
+
while cls is not None and cls.type not in ("class_declaration", "record_declaration"):
|
|
101
|
+
if cls.type in CALLABLE_DECLS or cls.type == "compilation_unit":
|
|
102
|
+
return None
|
|
103
|
+
cls = cls.parent
|
|
104
|
+
if cls is None:
|
|
105
|
+
return None
|
|
106
|
+
text = " ".join(_text(a) for a in cls.children if a.type == "attribute_list")
|
|
107
|
+
m = re.search(r"\bRoute\s*\(\s*@?\"([^\"]*)\"", text)
|
|
108
|
+
if not m:
|
|
109
|
+
return None
|
|
110
|
+
cname = _text(cls.child_by_field_name("name"))
|
|
111
|
+
cname = cname[:-len("Controller")] if cname.endswith("Controller") and len(cname) > len("Controller") else cname
|
|
112
|
+
return m.group(1).replace("[controller]", cname).replace("[action]", method_name).strip("/")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def pathlike(s: str) -> bool:
|
|
116
|
+
if not s or len(s) > 120 or any(ch.isspace() for ch in s) or s.startswith(("http:", "https:")):
|
|
117
|
+
return False
|
|
118
|
+
return bool(re.fullmatch(r"(\{\}|\*)?(\.[A-Za-z0-9_]{1,10}){1,2}", s)) or "/" in s.strip("/")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _identifiers(node) -> list[str]:
|
|
122
|
+
if node is None:
|
|
123
|
+
return []
|
|
124
|
+
if node.type == "identifier":
|
|
125
|
+
return [_text(node)]
|
|
126
|
+
return [x for c in node.children for x in _identifiers(c)]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _declared_names(node) -> list[str]:
|
|
130
|
+
"""Names a lambda, pattern, catch clause or out-declaration introduces."""
|
|
131
|
+
if node.type == "lambda_expression":
|
|
132
|
+
first = node.child_by_field_name("parameters") or (node.named_children[0] if node.named_children else None)
|
|
133
|
+
if first is None or first.type not in ("implicit_parameter", "parameter_list", "identifier"):
|
|
134
|
+
return []
|
|
135
|
+
if first.type == "parameter_list":
|
|
136
|
+
return [_text(p.child_by_field_name("name") or p.named_children[-1]) for p in first.children if p.type == "parameter"]
|
|
137
|
+
return [_text(first)]
|
|
138
|
+
name = node.child_by_field_name("name")
|
|
139
|
+
if name is not None:
|
|
140
|
+
return _identifiers(name)
|
|
141
|
+
ids = [c for c in node.named_children
|
|
142
|
+
if c.type in ("identifier", "single_variable_designation", "parenthesized_variable_designation")]
|
|
143
|
+
return _identifiers(ids[-1]) if ids else []
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _predefined(tnode) -> str:
|
|
147
|
+
"""int, float, string: a parameter of one of these is matched against a literal or a variable of it."""
|
|
148
|
+
return _text(tnode) if tnode is not None and tnode.type == "predefined_type" else ""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _product(node, scope: dict) -> Optional[tuple]:
|
|
152
|
+
"""`total * share` as an argument parses as a declaration of a pointer variable. When the "type" is a local
|
|
153
|
+
variable, it is a multiplication: the two names."""
|
|
154
|
+
if node.type != "declaration_expression":
|
|
155
|
+
return None
|
|
156
|
+
tnode, name = node.child_by_field_name("type"), node.child_by_field_name("name")
|
|
157
|
+
inner = tnode.named_children[0] if tnode is not None and tnode.type == "pointer_type" and tnode.named_children else None
|
|
158
|
+
if inner is None or name is None or inner.type != "identifier" or ("#" + _text(inner)) not in scope:
|
|
159
|
+
return None
|
|
160
|
+
return _text(inner), _text(name)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _arg_hint(arg, scope: dict):
|
|
164
|
+
"""What is known about one argument: a lambda's parameter count (int), a type name (str), or None."""
|
|
165
|
+
if arg.type == "lambda_expression":
|
|
166
|
+
params = arg.child_by_field_name("parameters")
|
|
167
|
+
if params is None:
|
|
168
|
+
return None
|
|
169
|
+
if params.type in ("identifier", "implicit_parameter"):
|
|
170
|
+
return 1
|
|
171
|
+
return len([c for c in params.children if c.type in ("parameter", "identifier", "implicit_parameter")])
|
|
172
|
+
if arg.type == "real_literal":
|
|
173
|
+
return {"f": "float", "m": "decimal"}.get(_text(arg)[-1:].lower(), "double") # 9f is a float, 9.0 a double
|
|
174
|
+
if arg.type in LITERALS:
|
|
175
|
+
return LITERALS[arg.type]
|
|
176
|
+
if arg.type == "parenthesized_expression" and arg.named_children:
|
|
177
|
+
return _arg_hint(arg.named_children[0], scope)
|
|
178
|
+
if _product(arg, scope):
|
|
179
|
+
sides = [scope.get("%" + x) for x in _product(arg, scope)]
|
|
180
|
+
return next((wide for wide in NUMERIC if wide in sides), None) if all(x in NUMERIC for x in sides) else None
|
|
181
|
+
if arg.type == "binary_expression":
|
|
182
|
+
# total * share: arithmetic on numbers has the widest operand's type; on a string, + gives a string.
|
|
183
|
+
op = arg.child_by_field_name("operator")
|
|
184
|
+
left, right = arg.child_by_field_name("left"), arg.child_by_field_name("right")
|
|
185
|
+
if op is None or left is None or right is None or _text(op) not in ("+", "-", "*", "/", "%"):
|
|
186
|
+
return None
|
|
187
|
+
sides = [_arg_hint(left, scope), _arg_hint(right, scope)]
|
|
188
|
+
if _text(op) == "+" and "string" in sides:
|
|
189
|
+
return "string"
|
|
190
|
+
for wide in NUMERIC:
|
|
191
|
+
if wide in sides:
|
|
192
|
+
return wide if all(x in NUMERIC for x in sides) else None
|
|
193
|
+
return None
|
|
194
|
+
if arg.type == "object_creation_expression":
|
|
195
|
+
names = _type_names(arg.child_by_field_name("type"))
|
|
196
|
+
return names[0].split("`")[0] if names else None
|
|
197
|
+
if arg.type == "identifier":
|
|
198
|
+
known = scope.get(_text(arg)) or scope.get("%" + _text(arg))
|
|
199
|
+
return known.split("`")[0] if known else None
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
NUMERIC = ("decimal", "double", "float", "ulong", "long", "uint", "int") # widest first
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
PREDEFINED = {"int", "long", "short", "byte", "uint", "ulong", "float", "double", "decimal", "bool", "string",
|
|
207
|
+
"char", "TimeSpan", "DateTime", "DateTimeOffset", "Guid", "CancellationToken"}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _delegate_arity(ptype: str) -> int:
|
|
211
|
+
"""How many arguments a delegate-typed parameter takes; -1 when the type is not a known delegate."""
|
|
212
|
+
head, _, rest = ptype.rstrip("?").partition("<")
|
|
213
|
+
head = head.rsplit(".", 1)[-1]
|
|
214
|
+
if head in PREDEFINED:
|
|
215
|
+
return -2 # certainly not a delegate: a lambda cannot be passed here
|
|
216
|
+
if head not in ("Action", "Func", "Predicate", "Comparison"):
|
|
217
|
+
return -1
|
|
218
|
+
if not rest:
|
|
219
|
+
return 0
|
|
220
|
+
depth, n = 0, 1
|
|
221
|
+
for ch in rest[:-1]:
|
|
222
|
+
depth += ch == "<"
|
|
223
|
+
depth -= ch == ">"
|
|
224
|
+
n += ch == "," and depth == 0
|
|
225
|
+
return {"Action": n, "Func": n - 1, "Predicate": 1, "Comparison": 2}[head]
|
|
226
|
+
STRINGS = ("string_literal", "verbatim_string_literal", "raw_string_literal", "interpolated_string_expression")
|
|
227
|
+
# Calls of the shape Run("name", () => { ... }) declare a test inline.
|
|
228
|
+
TEST_RUNNERS = {"Run", "Test", "It", "Case", "Scenario", "Fact", "Check", "Spec"}
|
|
229
|
+
TEST_ATTRIBUTES = {"Fact", "Theory", "Test", "TestCase", "TestMethod", "DataTestMethod"}
|
|
230
|
+
LIFECYCLE = {"_Ready", "_Process", "_PhysicsProcess", "_Input", "_UnhandledInput", "_EnterTree",
|
|
231
|
+
"_ExitTree", "_Draw", "_Notification"}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _text(node) -> str:
|
|
235
|
+
return node.text.decode("utf8", "replace") if node is not None else ""
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _squash(s: str) -> str:
|
|
239
|
+
return re.sub(r"\s+", " ", s).strip()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _modifiers(node) -> list[str]:
|
|
243
|
+
return [_text(c) for c in node.children if c.type == "modifier"]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _visibility(mods: list[str], default: str) -> str:
|
|
247
|
+
found = [m for m in mods if m in ACCESS]
|
|
248
|
+
return " ".join(found) if found else default
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _child(node, *types):
|
|
252
|
+
for c in node.children:
|
|
253
|
+
if c.type in types:
|
|
254
|
+
return c
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _type_names(node) -> list[str]:
|
|
259
|
+
"""Every identifier in a type expression: List<Foo> -> [List, Foo]."""
|
|
260
|
+
out: list[str] = []
|
|
261
|
+
if node is None:
|
|
262
|
+
return out
|
|
263
|
+
stack = [node]
|
|
264
|
+
while stack:
|
|
265
|
+
n = stack.pop()
|
|
266
|
+
if n.type == "identifier":
|
|
267
|
+
out.append(_text(n))
|
|
268
|
+
elif n.type == "generic_name":
|
|
269
|
+
# Foo<A, B> is written Foo`2, so it is told apart from a non-generic Foo.
|
|
270
|
+
targs = _child(n, "type_argument_list")
|
|
271
|
+
count = len([c for c in targs.named_children]) if targs is not None else 0
|
|
272
|
+
out.append(f"{_text(_child(n, 'identifier'))}`{count}" if count else _text(_child(n, "identifier")))
|
|
273
|
+
if targs is not None:
|
|
274
|
+
stack.extend(reversed(targs.children))
|
|
275
|
+
elif n.type == "qualified_name":
|
|
276
|
+
# A.B.C: the last segment is the type; keep it only.
|
|
277
|
+
ids = [c for c in n.children if c.type in ("identifier", "generic_name")]
|
|
278
|
+
if ids:
|
|
279
|
+
stack.append(ids[-1])
|
|
280
|
+
else:
|
|
281
|
+
stack.extend(reversed(n.children))
|
|
282
|
+
return out
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
SEQUENCES = {"List", "IList", "IEnumerable", "IReadOnlyList", "ICollection", "IReadOnlyCollection",
|
|
286
|
+
"HashSet", "ISet", "Queue", "Stack", "LinkedList", "SortedSet", "ImmutableArray",
|
|
287
|
+
"ImmutableList", "Span", "ReadOnlySpan"}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _elem_type(node) -> Optional[str]:
|
|
291
|
+
"""Element type of a sequence or array type expression: List<Foo> and Foo[] both give Foo."""
|
|
292
|
+
if node is None:
|
|
293
|
+
return None
|
|
294
|
+
names = _type_names(node)
|
|
295
|
+
if not names:
|
|
296
|
+
return None
|
|
297
|
+
text = _text(node)
|
|
298
|
+
if text.rstrip("?").endswith("]"):
|
|
299
|
+
return names[0]
|
|
300
|
+
if len(names) == 2 and names[0].split("`")[0] in SEQUENCES:
|
|
301
|
+
return names[1]
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _outer_type(node) -> Optional[str]:
|
|
306
|
+
names = _type_names(node)
|
|
307
|
+
return names[0] if names else None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# Methods that change the collection they are called on. A field used this way is written, not only read.
|
|
311
|
+
MUTATORS = frozenset("""Add AddRange AddFirst AddLast Insert InsertRange Remove RemoveAt RemoveAll RemoveRange RemoveFirst
|
|
312
|
+
RemoveLast RemoveWhere Clear Enqueue Dequeue TryDequeue Push Pop TryPop TryAdd TryRemove TryUpdate TryTake Sort Reverse
|
|
313
|
+
UnionWith ExceptWith IntersectWith SymmetricExceptWith Append AppendLine AppendFormat Set SetValue Fill EnsureCapacity
|
|
314
|
+
TrimExcess GetOrAdd AddOrUpdate""".split())
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class _Walker:
|
|
318
|
+
def __init__(self, repo: str, rel_path: str, file_id: str, src: bytes, module: str):
|
|
319
|
+
self.repo = repo
|
|
320
|
+
self.path = rel_path
|
|
321
|
+
self.file_id = file_id
|
|
322
|
+
self.module = module
|
|
323
|
+
self.res = FileResult()
|
|
324
|
+
self.src = src # spans are offsets into the file; the root node starts after a byte order mark
|
|
325
|
+
self.tree = _parser.parse(src)
|
|
326
|
+
self.field_types: dict[str, dict[str, str]] = {} # type id -> member name -> type name
|
|
327
|
+
self.redirect: dict[tuple, str] = {} # lambda span -> node its body's calls belong to
|
|
328
|
+
self.done_calls: dict[tuple, Optional[CallSite]] = {} # invocation span -> its call site
|
|
329
|
+
|
|
330
|
+
# -- ids -----------------------------------------------------------------
|
|
331
|
+
def _qid(self, qualified: str) -> str:
|
|
332
|
+
# The assembly is part of a .NET type's identity: two projects may both declare `Program`.
|
|
333
|
+
return f"{self.repo}:csharp:{self.module}::{qualified}"
|
|
334
|
+
|
|
335
|
+
# -- top level -----------------------------------------------------------
|
|
336
|
+
def run(self) -> FileResult:
|
|
337
|
+
root = self.tree.root_node
|
|
338
|
+
globals_ = [c for c in root.children if c.type == "global_statement"]
|
|
339
|
+
self._members(root, ns="", parent_id=self.file_id, type_id=None, qual="")
|
|
340
|
+
if globals_:
|
|
341
|
+
cid = self._qid(f"{self.path}::<top-level>")
|
|
342
|
+
self.res.nodes.append(Node(
|
|
343
|
+
id=cid, kind="callable", name="<top-level>", parent_id=self.file_id,
|
|
344
|
+
language=LANGUAGE, path=self.path, span_start=globals_[0].start_point[0] + 1,
|
|
345
|
+
span_end=globals_[-1].end_point[0] + 1,
|
|
346
|
+
attrs={"signature": "top-level statements", "visibility": "internal",
|
|
347
|
+
"native_kind": "top_level"}))
|
|
348
|
+
self.res.nodes.append(Node(
|
|
349
|
+
id=cid + "#entry", kind="entry_point", name=f"{self.path} (top-level)",
|
|
350
|
+
parent_id=self.file_id, language=LANGUAGE, path=self.path,
|
|
351
|
+
attrs={"trigger": "cli", "address": self.path}))
|
|
352
|
+
self.res.edges.append(Edge("exposes", cid + "#entry", cid))
|
|
353
|
+
scope: dict[str, str] = {}
|
|
354
|
+
for g in globals_:
|
|
355
|
+
self._body(g, cid, None, scope)
|
|
356
|
+
return self.res
|
|
357
|
+
|
|
358
|
+
def _members(self, node, ns: str, parent_id: str, type_id: Optional[str], qual: str) -> None:
|
|
359
|
+
for c in node.children:
|
|
360
|
+
t = c.type
|
|
361
|
+
if t == "using_directive":
|
|
362
|
+
self._using(c)
|
|
363
|
+
elif t in ("namespace_declaration", "file_scoped_namespace_declaration"):
|
|
364
|
+
name = _text(c.child_by_field_name("name"))
|
|
365
|
+
full = f"{ns}.{name}" if ns else name
|
|
366
|
+
if full not in self.res.declares:
|
|
367
|
+
self.res.declares.append(full)
|
|
368
|
+
body = c.child_by_field_name("body") or c
|
|
369
|
+
self._members(body, full, parent_id, type_id, full)
|
|
370
|
+
if t == "file_scoped_namespace_declaration":
|
|
371
|
+
# `namespace X;` covers the rest of the file: what follows is a sibling, not a child.
|
|
372
|
+
ns = qual = full
|
|
373
|
+
elif t in TYPE_DECLS:
|
|
374
|
+
self._type(c, ns, parent_id, qual)
|
|
375
|
+
elif t == "declaration_list":
|
|
376
|
+
self._members(c, ns, parent_id, type_id, qual)
|
|
377
|
+
|
|
378
|
+
def _using(self, node) -> None:
|
|
379
|
+
raw = _squash(_text(node)).rstrip(";")
|
|
380
|
+
raw = re.sub(r"^(global\s+)?using\s+", "", raw)
|
|
381
|
+
is_static = raw.startswith("static ")
|
|
382
|
+
raw = raw[7:] if is_static else raw
|
|
383
|
+
alias = None
|
|
384
|
+
if "=" in raw:
|
|
385
|
+
alias, raw = [p.strip() for p in raw.split("=", 1)]
|
|
386
|
+
self.res.imports.append(ImportRef(self.file_id, raw, alias=alias, is_static=is_static))
|
|
387
|
+
|
|
388
|
+
# -- types ---------------------------------------------------------------
|
|
389
|
+
def _type(self, node, ns: str, parent_id: str, qual: str) -> None:
|
|
390
|
+
native = TYPE_DECLS[node.type]
|
|
391
|
+
name = _text(node.child_by_field_name("name"))
|
|
392
|
+
tparams = _child(node, "type_parameter_list")
|
|
393
|
+
arity = len([c for c in tparams.children if c.type == "type_parameter"]) if tparams else 0
|
|
394
|
+
qname = f"{qual}.{name}" if qual else name
|
|
395
|
+
if arity:
|
|
396
|
+
qname += f"`{arity}"
|
|
397
|
+
tid = self._qid(qname)
|
|
398
|
+
mods = _modifiers(node)
|
|
399
|
+
body = node.child_by_field_name("body") or _child(node, "declaration_list", "enum_member_declaration_list")
|
|
400
|
+
head_end = body.start_byte if body is not None else node.end_byte
|
|
401
|
+
self.res.nodes.append(Node(
|
|
402
|
+
id=tid, kind="type", name=name, parent_id=parent_id, language=LANGUAGE, path=self.path,
|
|
403
|
+
span_start=node.start_point[0] + 1, span_end=node.end_point[0] + 1,
|
|
404
|
+
attrs={"native_kind": native, "namespace": ns,
|
|
405
|
+
"visibility": _visibility(mods, "internal" if parent_id == self.file_id else "private"),
|
|
406
|
+
"is_abstract": "abstract" in mods or native == "interface",
|
|
407
|
+
"is_static": "static" in mods, "is_partial": "partial" in mods,
|
|
408
|
+
"signature": _squash(self.src[node.start_byte:head_end].decode("utf8", "replace"))}))
|
|
409
|
+
base_list = _child(node, "base_list")
|
|
410
|
+
if base_list is not None:
|
|
411
|
+
for b in base_list.children:
|
|
412
|
+
if b.type in (",", ":"):
|
|
413
|
+
continue
|
|
414
|
+
names = _type_names(b)
|
|
415
|
+
if names:
|
|
416
|
+
self.res.type_refs.append(TypeRef(tid, names, "base", b.start_point[0] + 1))
|
|
417
|
+
self.field_types.setdefault(tid, {})
|
|
418
|
+
if native == "delegate":
|
|
419
|
+
return
|
|
420
|
+
# Record primary-constructor parameters as fields (records), with a body or without: record Item(int Id);
|
|
421
|
+
# A class's or struct's (C# 12: class Svc(ILogger logger)) are parameters it captures, not properties.
|
|
422
|
+
plist = _child(node, "parameter_list")
|
|
423
|
+
if plist is not None and native == "record":
|
|
424
|
+
for p in plist.children:
|
|
425
|
+
if p.type == "parameter":
|
|
426
|
+
self._field_node(tid, _text(p.child_by_field_name("name")), p.child_by_field_name("type"),
|
|
427
|
+
p, ["public"], "property")
|
|
428
|
+
if body is None:
|
|
429
|
+
return
|
|
430
|
+
for m in body.children:
|
|
431
|
+
mt = m.type
|
|
432
|
+
if mt in TYPE_DECLS:
|
|
433
|
+
self._type(m, ns, tid, qname)
|
|
434
|
+
elif mt == "field_declaration" or mt == "event_field_declaration":
|
|
435
|
+
decl = _child(m, "variable_declaration")
|
|
436
|
+
if decl is None:
|
|
437
|
+
continue
|
|
438
|
+
tnode = decl.child_by_field_name("type")
|
|
439
|
+
for v in decl.children:
|
|
440
|
+
if v.type == "variable_declarator":
|
|
441
|
+
nm = v.child_by_field_name("name") or _child(v, "identifier")
|
|
442
|
+
self._field_node(tid, _text(nm), tnode, m, _modifiers(m),
|
|
443
|
+
"event" if mt.startswith("event") else "field")
|
|
444
|
+
elif mt in ("property_declaration", "indexer_declaration", "event_declaration"):
|
|
445
|
+
nm = m.child_by_field_name("name")
|
|
446
|
+
self._field_node(tid, _text(nm) if nm else "this[]", m.child_by_field_name("type"), m,
|
|
447
|
+
_modifiers(m), mt.split("_")[0])
|
|
448
|
+
elif mt == "enum_member_declaration":
|
|
449
|
+
nm = m.child_by_field_name("name") or _child(m, "identifier")
|
|
450
|
+
self.res.nodes.append(Node(
|
|
451
|
+
id=f"{tid}.{_text(nm)}", kind="field", name=_text(nm), parent_id=tid,
|
|
452
|
+
language=LANGUAGE, path=self.path, span_start=m.start_point[0] + 1,
|
|
453
|
+
span_end=m.end_point[0] + 1,
|
|
454
|
+
attrs={"native_kind": "enum_member", "visibility": "public", "is_mutable": False}))
|
|
455
|
+
# Callables second, so every field type of this type is known to the body walk.
|
|
456
|
+
for m in body.children:
|
|
457
|
+
if m.type in CALLABLE_DECLS:
|
|
458
|
+
self._callable(m, tid, tid, native == "interface")
|
|
459
|
+
elif m.type in ("property_declaration", "indexer_declaration"):
|
|
460
|
+
# Property bodies: attribute their calls to the property itself.
|
|
461
|
+
nm = m.child_by_field_name("name")
|
|
462
|
+
pid = f"{tid}.{_text(nm) if nm else 'this[]'}"
|
|
463
|
+
self._body(m, pid, tid, {})
|
|
464
|
+
|
|
465
|
+
def _field_node(self, tid, name, tnode, node, mods, native) -> None:
|
|
466
|
+
if not name:
|
|
467
|
+
return
|
|
468
|
+
default_vis = "private"
|
|
469
|
+
self.res.nodes.append(Node(
|
|
470
|
+
id=f"{tid}.{name}", kind="field", name=name, parent_id=tid, language=LANGUAGE,
|
|
471
|
+
path=self.path, span_start=node.start_point[0] + 1, span_end=node.end_point[0] + 1,
|
|
472
|
+
attrs={"native_kind": native, "declared_type": _squash(_text(tnode)),
|
|
473
|
+
"type_name": _outer_type(tnode),
|
|
474
|
+
"visibility": _visibility(mods, default_vis), "is_static": "static" in mods or "const" in mods,
|
|
475
|
+
"is_mutable": not ("readonly" in mods or "const" in mods)}))
|
|
476
|
+
self.res.edges.append(Edge("has_field", tid, f"{tid}.{name}"))
|
|
477
|
+
names = _type_names(tnode)
|
|
478
|
+
if names:
|
|
479
|
+
self.res.type_refs.append(TypeRef(f"{tid}.{name}", names, "field_type", node.start_point[0] + 1))
|
|
480
|
+
self.field_types.setdefault(tid, {})[name] = names[0]
|
|
481
|
+
elem = _elem_type(tnode)
|
|
482
|
+
if elem:
|
|
483
|
+
self.field_types[tid][name + "[]"] = elem
|
|
484
|
+
|
|
485
|
+
# -- callables -----------------------------------------------------------
|
|
486
|
+
def _callable(self, node, parent_id: str, type_id: Optional[str], in_interface: bool,
|
|
487
|
+
outer_scope: Optional[dict] = None) -> None:
|
|
488
|
+
t = node.type
|
|
489
|
+
if t == "constructor_declaration":
|
|
490
|
+
name = ".ctor"
|
|
491
|
+
elif t == "destructor_declaration":
|
|
492
|
+
name = ".dtor"
|
|
493
|
+
elif t in ("operator_declaration", "conversion_operator_declaration"):
|
|
494
|
+
op = node.child_by_field_name("operator")
|
|
495
|
+
name = "operator " + (_text(op) if op else _squash(_text(node.child_by_field_name("type"))))
|
|
496
|
+
else:
|
|
497
|
+
name = _text(node.child_by_field_name("name"))
|
|
498
|
+
params = node.child_by_field_name("parameters") or _child(node, "parameter_list")
|
|
499
|
+
scope: dict[str, str] = dict(outer_scope or {})
|
|
500
|
+
ptypes: list[str] = []
|
|
501
|
+
required = 0
|
|
502
|
+
plist = [p for p in params.children if p.type == "parameter"] if params is not None else []
|
|
503
|
+
for p in plist:
|
|
504
|
+
ptype = p.child_by_field_name("type")
|
|
505
|
+
ptypes.append(_squash(_text(ptype)).replace(", ", ","))
|
|
506
|
+
pname = _text(p.child_by_field_name("name"))
|
|
507
|
+
outer = _outer_type(ptype)
|
|
508
|
+
if pname:
|
|
509
|
+
scope["#" + pname] = "1" # a local name: never a field of the enclosing type
|
|
510
|
+
scope.pop("%" + pname, None)
|
|
511
|
+
if ptype is not None and ptype.type == "predefined_type":
|
|
512
|
+
scope["%" + pname] = _text(ptype) # float, int: not a receiver, but tells overloads apart
|
|
513
|
+
if pname and outer:
|
|
514
|
+
scope[pname] = outer
|
|
515
|
+
elem = _elem_type(ptype)
|
|
516
|
+
if elem:
|
|
517
|
+
scope[pname + "[]"] = elem
|
|
518
|
+
has_default = any(c.type in ("equals_value_clause", "=") for c in p.children)
|
|
519
|
+
is_params = any(_text(c) == "params" for c in p.children)
|
|
520
|
+
if not has_default and not is_params:
|
|
521
|
+
required += 1
|
|
522
|
+
sep = "/" if t == "local_function_statement" else "."
|
|
523
|
+
tparams = _child(node, "type_parameter_list")
|
|
524
|
+
generic_arity = len([c for c in tparams.children if c.type == "type_parameter"]) if tparams is not None else 0
|
|
525
|
+
# M(int) and M<T>(int) are different methods, so the count of type parameters is part of the id.
|
|
526
|
+
cid = f"{parent_id}{sep}{name}{'`' + str(generic_arity) if generic_arity else ''}({','.join(ptypes)})"
|
|
527
|
+
mods = _modifiers(node)
|
|
528
|
+
body = node.child_by_field_name("body") or _child(node, "block", "arrow_expression_clause")
|
|
529
|
+
head_end = body.start_byte if body is not None else node.end_byte
|
|
530
|
+
ret = node.child_by_field_name("returns") or node.child_by_field_name("type")
|
|
531
|
+
is_variadic = any(any(_text(c) == "params" for c in p.children) for p in plist)
|
|
532
|
+
self.res.nodes.append(Node(
|
|
533
|
+
id=cid, kind="callable", name=name, parent_id=parent_id, language=LANGUAGE, path=self.path,
|
|
534
|
+
span_start=node.start_point[0] + 1, span_end=node.end_point[0] + 1,
|
|
535
|
+
attrs={"signature": _squash(self.src[node.start_byte:head_end].decode("utf8", "replace")).rstrip(";"),
|
|
536
|
+
"visibility": "public" if in_interface else _visibility(mods, "private"),
|
|
537
|
+
"is_static": "static" in mods, "is_async": "async" in mods,
|
|
538
|
+
"is_virtual": in_interface or any(m in mods for m in ("virtual", "abstract", "override")),
|
|
539
|
+
"native_kind": t.replace("_declaration", "").replace("_statement", ""),
|
|
540
|
+
"argc_min": required, "argc_max": 99 if is_variadic else len(plist),
|
|
541
|
+
"body_line": body.start_point[0] + 1 if body is not None else None,
|
|
542
|
+
"delegate_arity": [_delegate_arity(x) for x in ptypes], "generic_arity": generic_arity,
|
|
543
|
+
"returns_names": _type_names(ret) if ret is not None and t != "constructor_declaration" else [],
|
|
544
|
+
"type_params": [_text(_child(c, "identifier") or c) for c in tparams.children
|
|
545
|
+
if c.type == "type_parameter"] if tparams is not None else [],
|
|
546
|
+
"is_extension": bool(plist) and any(_text(c) == "this" for c in plist[0].children) or None,
|
|
547
|
+
"param_types": [(_type_names(p.child_by_field_name("type")) or [_predefined(p.child_by_field_name("type"))])[0]
|
|
548
|
+
for p in plist],
|
|
549
|
+
"type_id": type_id}))
|
|
550
|
+
for p, ptext in zip(plist, ptypes):
|
|
551
|
+
names = _type_names(p.child_by_field_name("type"))
|
|
552
|
+
if names:
|
|
553
|
+
self.res.type_refs.append(TypeRef(cid, names, "param", p.start_point[0] + 1))
|
|
554
|
+
if ret is not None:
|
|
555
|
+
names = _type_names(ret)
|
|
556
|
+
if names:
|
|
557
|
+
self.res.type_refs.append(TypeRef(cid, names, "return", ret.start_point[0] + 1))
|
|
558
|
+
attrs_text = " ".join(_text(a) for a in node.children if a.type == "attribute_list")
|
|
559
|
+
prefix = _controller_prefix(node, name)
|
|
560
|
+
# [HttpGet] with no template: the verb of the action's own [Route("...")], else of the controller's route.
|
|
561
|
+
bare = [m.group(1).upper() for m in HTTP_BARE.finditer(attrs_text)]
|
|
562
|
+
routed = False
|
|
563
|
+
for m in HTTP_ATTR.finditer(attrs_text):
|
|
564
|
+
path = m.group(3)
|
|
565
|
+
if prefix is not None and not path.startswith(("/", "~")): # [Route("api/[controller]")] on the class
|
|
566
|
+
path = f"{prefix}/{path}" if path else prefix
|
|
567
|
+
path = path.lstrip("~")
|
|
568
|
+
routed = routed or not m.group(2)
|
|
569
|
+
for method in [m.group(2).upper()] if m.group(2) else bare or [None]:
|
|
570
|
+
self.res.endpoints.append(Endpoint("http", "serve", cid, "/" + path.lstrip("/"), node.start_point[0] + 1,
|
|
571
|
+
method))
|
|
572
|
+
if prefix is not None and not routed: # the action answers at the controller's own route
|
|
573
|
+
for method in bare:
|
|
574
|
+
self.res.endpoints.append(Endpoint("http", "serve", cid, "/" + prefix.lstrip("/"), node.start_point[0] + 1,
|
|
575
|
+
method))
|
|
576
|
+
# The attributes' own names ([Xunit.Fact], [TestCase(1)]), not words in their arguments ([Description("Test")]).
|
|
577
|
+
found = {re.sub(r"Attribute$", "", _text(a.child_by_field_name("name")).rsplit(".", 1)[-1])
|
|
578
|
+
for al in node.children if al.type == "attribute_list"
|
|
579
|
+
for a in al.named_children if a.type == "attribute"} & TEST_ATTRIBUTES
|
|
580
|
+
if found:
|
|
581
|
+
self.res.nodes[-1].attrs["is_test"] = True
|
|
582
|
+
self.res.nodes[-1].attrs["framework"] = sorted(found)[0]
|
|
583
|
+
if name == "Main" and "static" in mods:
|
|
584
|
+
self._entry(cid, "cli", self.path)
|
|
585
|
+
elif name in LIFECYCLE:
|
|
586
|
+
self._entry(cid, "lifecycle", name)
|
|
587
|
+
if body is not None:
|
|
588
|
+
self._body(body, cid, type_id, scope)
|
|
589
|
+
init = _child(node, "constructor_initializer")
|
|
590
|
+
if init is not None:
|
|
591
|
+
self._body(init, cid, type_id, scope)
|
|
592
|
+
|
|
593
|
+
def _entry(self, cid: str, trigger: str, address: str) -> None:
|
|
594
|
+
self.res.nodes.append(Node(
|
|
595
|
+
id=cid + "#entry", kind="entry_point", name=cid.split(":", 2)[-1], parent_id=self.file_id,
|
|
596
|
+
language=LANGUAGE, path=self.path, attrs={"trigger": trigger, "address": address}))
|
|
597
|
+
self.res.edges.append(Edge("exposes", cid + "#entry", cid))
|
|
598
|
+
|
|
599
|
+
# -- bodies --------------------------------------------------------------
|
|
600
|
+
def _body(self, node, cid: str, type_id: Optional[str], scope: dict[str, str]) -> None:
|
|
601
|
+
"""Walk a body in source order, tracking local variable types and emitting call sites."""
|
|
602
|
+
t = node.type
|
|
603
|
+
if t in LAMBDAS and self.redirect:
|
|
604
|
+
cid = self.redirect.pop((node.start_byte, node.end_byte), cid)
|
|
605
|
+
if t == "local_function_statement":
|
|
606
|
+
self._callable(node, cid, type_id, False, scope)
|
|
607
|
+
return
|
|
608
|
+
if t in TYPE_DECLS:
|
|
609
|
+
return
|
|
610
|
+
if t == "invocation_expression" and _text(node.child_by_field_name("function")) == "nameof":
|
|
611
|
+
return # nameof(_repo) is the name, read when compiling: nothing in it runs
|
|
612
|
+
if t == "variable_declaration":
|
|
613
|
+
tnode = node.child_by_field_name("type")
|
|
614
|
+
declared = _outer_type(tnode) if tnode is not None and _text(tnode) != "var" else None
|
|
615
|
+
if declared:
|
|
616
|
+
self.res.type_refs.append(TypeRef(cid, _type_names(tnode), "local", node.start_point[0] + 1))
|
|
617
|
+
for v in node.children:
|
|
618
|
+
if v.type != "variable_declarator":
|
|
619
|
+
continue
|
|
620
|
+
nm = _text(v.child_by_field_name("name") or _child(v, "identifier"))
|
|
621
|
+
vt = declared or self._infer(v, scope, type_id)
|
|
622
|
+
if nm:
|
|
623
|
+
scope.pop("~" + nm, None)
|
|
624
|
+
scope.pop("%" + nm, None)
|
|
625
|
+
scope["#" + nm] = "1"
|
|
626
|
+
if tnode is not None and tnode.type == "predefined_type":
|
|
627
|
+
scope["%" + nm] = _text(tnode)
|
|
628
|
+
if nm and vt:
|
|
629
|
+
scope[nm] = vt
|
|
630
|
+
elif nm:
|
|
631
|
+
# `var x = a.Make();`: the type is whatever Make returns, known once calls are resolved.
|
|
632
|
+
init = self._call_in(v)
|
|
633
|
+
if init is not None:
|
|
634
|
+
site = self._invocation(init, cid, type_id, scope)
|
|
635
|
+
if site is not None:
|
|
636
|
+
scope["~" + nm] = site
|
|
637
|
+
elem = _elem_type(tnode) if declared else self._infer(v, scope, type_id, elem=True)
|
|
638
|
+
if nm and elem:
|
|
639
|
+
scope[nm + "[]"] = elem
|
|
640
|
+
elif t in ("foreach_statement",):
|
|
641
|
+
tnode = node.child_by_field_name("type")
|
|
642
|
+
nm = _text(node.child_by_field_name("left"))
|
|
643
|
+
for ident in _identifiers(node.child_by_field_name("left")):
|
|
644
|
+
scope["#" + ident] = "1"
|
|
645
|
+
if tnode is not None and _text(tnode) != "var" and nm:
|
|
646
|
+
outer = _outer_type(tnode)
|
|
647
|
+
if outer:
|
|
648
|
+
scope[nm] = outer
|
|
649
|
+
elif nm:
|
|
650
|
+
elem = self._elem_of(node.child_by_field_name("right"), scope, type_id)
|
|
651
|
+
if elem:
|
|
652
|
+
scope[nm] = elem
|
|
653
|
+
elif t == "invocation_expression":
|
|
654
|
+
self._invocation(node, cid, type_id, scope)
|
|
655
|
+
elif t in ("lambda_expression", "catch_declaration", "declaration_expression", "declaration_pattern", "from_clause") \
|
|
656
|
+
and not _product(node, scope):
|
|
657
|
+
tnode = node.child_by_field_name("type") if t != "lambda_expression" else None
|
|
658
|
+
declared = _outer_type(tnode) if tnode is not None and _text(tnode) != "var" else None
|
|
659
|
+
for ident in _declared_names(node): # names these introduce are locals
|
|
660
|
+
scope["#" + ident] = "1"
|
|
661
|
+
if declared: # `x is Sandbox s`, `out Foo f`, `catch (IOException e)`: typed where introduced
|
|
662
|
+
scope.pop("~" + ident, None)
|
|
663
|
+
scope[ident] = declared
|
|
664
|
+
if t in ("identifier", "member_access_expression"):
|
|
665
|
+
self._use(node, cid, type_id, scope)
|
|
666
|
+
elif t in STRINGS:
|
|
667
|
+
for text in _cs_strings(node):
|
|
668
|
+
if pathlike(text):
|
|
669
|
+
self.res.path_strings.setdefault(cid, []).append(text)
|
|
670
|
+
if t == "assignment_expression" and any(_text(c) == "+=" for c in node.children if not c.is_named):
|
|
671
|
+
left, right = node.child_by_field_name("left"), node.child_by_field_name("right")
|
|
672
|
+
if left is not None and right is not None and right.type in LAMBDAS + ("identifier", "member_access_expression"):
|
|
673
|
+
handler = None
|
|
674
|
+
if right.type == "identifier":
|
|
675
|
+
handler = _text(right)
|
|
676
|
+
elif right.type == "member_access_expression":
|
|
677
|
+
handler = _text(right.child_by_field_name("name"))
|
|
678
|
+
if left.type == "identifier":
|
|
679
|
+
self.res.events.append(EventUse("subscribe", cid, _text(left), None, None, handler,
|
|
680
|
+
node.start_point[0] + 1, type_id))
|
|
681
|
+
elif left.type == "member_access_expression":
|
|
682
|
+
recv, rtype = self._receiver(left.child_by_field_name("expression"), scope, type_id)
|
|
683
|
+
self.res.events.append(EventUse("subscribe", cid, _text(left.child_by_field_name("name")),
|
|
684
|
+
recv, rtype, handler, node.start_point[0] + 1, type_id))
|
|
685
|
+
elif t == "object_creation_expression":
|
|
686
|
+
tnode = node.child_by_field_name("type")
|
|
687
|
+
names = _type_names(tnode)
|
|
688
|
+
args = node.child_by_field_name("arguments")
|
|
689
|
+
argc = len([a for a in args.children if a.type == "argument"]) if args is not None else 0
|
|
690
|
+
if names:
|
|
691
|
+
self.res.type_refs.append(TypeRef(cid, names, "instantiate", node.start_point[0] + 1))
|
|
692
|
+
if names[0] in ("StreamWriter", "StreamReader"):
|
|
693
|
+
self.res.endpoints.append(Endpoint("file", "write" if names[0] == "StreamWriter" else "read", cid, "",
|
|
694
|
+
node.start_point[0] + 1, None, _cs_strings(args) if args is not None else []))
|
|
695
|
+
ctor_args = [a.named_children[-1] for a in args.children
|
|
696
|
+
if a.type == "argument" and a.named_children] if args is not None else []
|
|
697
|
+
if names[0] in ("RestRequest", "HttpRequestMessage"):
|
|
698
|
+
self._request_object(names[0], ctor_args, cid, node.start_point[0] + 1)
|
|
699
|
+
self.res.calls.append(CallSite(cid, ".ctor", names[0], names[0], argc,
|
|
700
|
+
node.start_point[0] + 1, type_id, node.start_point[1],
|
|
701
|
+
tuple(_arg_hint(a, scope) for a in ctor_args) if len(ctor_args) == argc else ()))
|
|
702
|
+
for c in node.children:
|
|
703
|
+
self._body(c, cid, type_id, scope)
|
|
704
|
+
|
|
705
|
+
def _use(self, node, cid: str, type_id: Optional[str], scope: dict[str, str]) -> None:
|
|
706
|
+
"""Record a name that may be a field, and whether this use reads or assigns it."""
|
|
707
|
+
parent = node.parent
|
|
708
|
+
if parent is None:
|
|
709
|
+
return
|
|
710
|
+
pt = parent.type
|
|
711
|
+
same = lambda a, b: a is not None and b is not None and a.start_byte == b.start_byte and a.end_byte == b.end_byte
|
|
712
|
+
receiver = rtype = chain = None
|
|
713
|
+
if node.type == "member_access_expression":
|
|
714
|
+
if pt == "invocation_expression" and same(parent.child_by_field_name("function"), node):
|
|
715
|
+
return # a method call, recorded as a call site
|
|
716
|
+
name_node = node.child_by_field_name("name")
|
|
717
|
+
if name_node is None or name_node.type != "identifier":
|
|
718
|
+
return
|
|
719
|
+
name = _text(name_node)
|
|
720
|
+
expr = node.child_by_field_name("expression")
|
|
721
|
+
receiver, rtype = self._receiver(expr, scope, type_id)
|
|
722
|
+
inner = expr
|
|
723
|
+
while inner is not None and inner.type in ("parenthesized_expression", "await_expression") and inner.named_children:
|
|
724
|
+
inner = inner.named_children[-1]
|
|
725
|
+
if inner is not None and inner.type == "invocation_expression":
|
|
726
|
+
chain, receiver = self._invocation(inner, cid, type_id, scope), "?"
|
|
727
|
+
elif rtype is None and receiver and "~" + receiver in scope:
|
|
728
|
+
chain = scope["~" + receiver]
|
|
729
|
+
else:
|
|
730
|
+
name = _text(node)
|
|
731
|
+
if "#" + name in scope:
|
|
732
|
+
return # a local or a parameter
|
|
733
|
+
if pt == "member_access_expression" and same(parent.child_by_field_name("name"), node):
|
|
734
|
+
return # the member half of a.b, handled with its parent
|
|
735
|
+
if pt in ("invocation_expression", "generic_name", "qualified_name", "type_argument_list", "base_list",
|
|
736
|
+
"array_type", "nullable_type", "name_colon", "name_equals", "attribute", "variable_declarator",
|
|
737
|
+
"parameter", "type_parameter", "using_directive", "labeled_statement", "member_binding_expression",
|
|
738
|
+
"implicit_parameter", "tuple_element", "pointer_type", "type_parameter_constraint", "typeof_expression",
|
|
739
|
+
"alias_qualified_name", "enum_member_declaration", "local_function_statement", "catch_declaration",
|
|
740
|
+
"single_variable_designation", "declaration_pattern", "constant_pattern", "from_clause"):
|
|
741
|
+
if not (pt == "variable_declarator" and not same(parent.child_by_field_name("name") or _child(parent, "identifier"), node)):
|
|
742
|
+
return
|
|
743
|
+
if same(parent.child_by_field_name("type"), node) or same(parent.child_by_field_name("name"), node) and pt != "argument":
|
|
744
|
+
return # a type name, or the name being declared
|
|
745
|
+
if pt == "argument" and same(parent.child_by_field_name("name"), node) and any(_text(c) == ":" for c in parent.children):
|
|
746
|
+
return # the label of a named argument: Take(count: 5)
|
|
747
|
+
if pt == "anonymous_object_creation_expression" and node.next_sibling is not None and node.next_sibling.type == "=":
|
|
748
|
+
return # a member an anonymous object declares: new { Name = x }
|
|
749
|
+
if pt == "assignment_expression" and parent.parent is not None and parent.parent.type == "initializer_expression" \
|
|
750
|
+
and same(parent.child_by_field_name("left"), node):
|
|
751
|
+
made = parent.parent.parent
|
|
752
|
+
if made is not None and made.type == "object_creation_expression":
|
|
753
|
+
tn = _outer_type(made.child_by_field_name("type"))
|
|
754
|
+
if tn: # new Foo { a = 1 } assigns Foo.a
|
|
755
|
+
self.res.field_uses.append(FieldUse(cid, name, tn, tn, "i", node.start_point[0] + 1, type_id))
|
|
756
|
+
return
|
|
757
|
+
# Read or write: look at what the expression sits in.
|
|
758
|
+
cur, up = node, parent
|
|
759
|
+
# Through parentheses, and out of a tuple: (_name, _age) = (name, age) assigns both.
|
|
760
|
+
while up is not None and (up.type in ("parenthesized_expression", "tuple_expression") or up.type == "argument"
|
|
761
|
+
and up.parent is not None and up.parent.type == "tuple_expression"):
|
|
762
|
+
cur, up = up, up.parent
|
|
763
|
+
access = "r"
|
|
764
|
+
if up is not None:
|
|
765
|
+
ops = [_text(c) for c in up.children if not c.is_named]
|
|
766
|
+
if up.type == "assignment_expression" and same(up.child_by_field_name("left"), cur):
|
|
767
|
+
access = "w" if "=" in ops else "rw"
|
|
768
|
+
elif up.type in ("postfix_unary_expression", "prefix_unary_expression") and ("++" in ops or "--" in ops):
|
|
769
|
+
access = "rw"
|
|
770
|
+
elif up.type == "argument" and any(o in ("out", "ref") for o in ops):
|
|
771
|
+
access = "w" if "out" in ops else "rw"
|
|
772
|
+
elif up.type == "element_access_expression" and same(up.child_by_field_name("expression") or up.named_children[0], cur):
|
|
773
|
+
outer = up.parent
|
|
774
|
+
if outer is not None and outer.type == "assignment_expression" and same(outer.child_by_field_name("left"), up):
|
|
775
|
+
access = "rw" # field[i] = x changes what the field holds
|
|
776
|
+
if access == "r" and up is not None: # field.Add(x), field[k].Enqueue(x): a call that changes what the field holds
|
|
777
|
+
c2, u2 = cur, up
|
|
778
|
+
if u2.type == "element_access_expression" and same(u2.child_by_field_name("expression") or u2.named_children[0], c2):
|
|
779
|
+
c2, u2 = u2, u2.parent
|
|
780
|
+
if u2 is not None and u2.type == "member_access_expression" and same(u2.child_by_field_name("expression"), c2) \
|
|
781
|
+
and u2.parent is not None and u2.parent.type == "invocation_expression" \
|
|
782
|
+
and _text(u2.child_by_field_name("name")).split("<")[0] in MUTATORS:
|
|
783
|
+
access = "rw"
|
|
784
|
+
self.res.field_uses.append(FieldUse(cid, name, receiver, rtype, access, node.start_point[0] + 1, type_id, chain))
|
|
785
|
+
|
|
786
|
+
def _elem_of(self, expr, scope: dict[str, str], type_id: Optional[str]) -> Optional[str]:
|
|
787
|
+
"""Element type of a collection expression that is a local, a parameter or a field."""
|
|
788
|
+
if expr is None:
|
|
789
|
+
return None
|
|
790
|
+
own = self.field_types.get(type_id or "", {})
|
|
791
|
+
if expr.type == "identifier":
|
|
792
|
+
return scope.get(_text(expr) + "[]") or own.get(_text(expr) + "[]")
|
|
793
|
+
if expr.type == "member_access_expression":
|
|
794
|
+
inner = expr.child_by_field_name("expression")
|
|
795
|
+
if inner is not None and inner.type in THIS:
|
|
796
|
+
return own.get(_text(expr.child_by_field_name("name")) + "[]")
|
|
797
|
+
return None
|
|
798
|
+
|
|
799
|
+
def _infer(self, declarator, scope: dict[str, str], type_id: Optional[str], elem: bool = False) -> Optional[str]:
|
|
800
|
+
"""Type of `var x = <expr>` for the easy cases (or its element type when elem=True)."""
|
|
801
|
+
if elem:
|
|
802
|
+
for i, c in enumerate(declarator.children):
|
|
803
|
+
n = c.children[-1] if c.type == "equals_value_clause" and c.children else c
|
|
804
|
+
if i and n.type in ("object_creation_expression", "array_creation_expression"):
|
|
805
|
+
return _elem_type(n.child_by_field_name("type"))
|
|
806
|
+
if i and n.type == "identifier":
|
|
807
|
+
return self._elem_of(n, scope, type_id)
|
|
808
|
+
return None
|
|
809
|
+
for i, c in enumerate(declarator.children):
|
|
810
|
+
n = c
|
|
811
|
+
if i == 0:
|
|
812
|
+
continue
|
|
813
|
+
if n.type == "equals_value_clause":
|
|
814
|
+
n = n.children[-1] if n.children else n
|
|
815
|
+
if n.type == "object_creation_expression":
|
|
816
|
+
return _outer_type(n.child_by_field_name("type"))
|
|
817
|
+
if n.type == "cast_expression":
|
|
818
|
+
return _outer_type(n.child_by_field_name("type"))
|
|
819
|
+
if n.type == "as_expression":
|
|
820
|
+
return _outer_type(n.child_by_field_name("right"))
|
|
821
|
+
if n.type == "identifier":
|
|
822
|
+
return scope.get(_text(n)) or (self.field_types.get(type_id or "", {}).get(_text(n)))
|
|
823
|
+
if n.type == "member_access_expression":
|
|
824
|
+
expr = n.child_by_field_name("expression")
|
|
825
|
+
name = _text(n.child_by_field_name("name"))
|
|
826
|
+
if expr is not None and expr.type in THIS:
|
|
827
|
+
return self.field_types.get(type_id or "", {}).get(name)
|
|
828
|
+
return None
|
|
829
|
+
|
|
830
|
+
def _map_group(self, receiver: str) -> str:
|
|
831
|
+
"""The route prefix of a minimal-API group a MapGet is called on: `app.MapGroup("api/x")` written in
|
|
832
|
+
place, or a variable this file sets to one (`var api = app.MapGroup("api/x")`, groups of groups too)."""
|
|
833
|
+
if not hasattr(self, "_groups"):
|
|
834
|
+
text = self.tree.root_node.text.decode("utf8", "replace")
|
|
835
|
+
found: dict[str, set] = {}
|
|
836
|
+
for m in re.finditer(r"\b(\w+)\s*=\s*([\w.]+?)\s*\.\s*MapGroup\s*\(\s*@?\"([^\"]*)\"", text):
|
|
837
|
+
found.setdefault(m.group(1), set()).add((m.group(2).rsplit(".", 1)[-1], m.group(3)))
|
|
838
|
+
self._groups = {k: next(iter(v)) for k, v in found.items() if len(v) == 1}
|
|
839
|
+
m = re.search(r"MapGroup\s*\(\s*@?\"([^\"]*)\"\s*\)[^\"]*$", receiver)
|
|
840
|
+
if m:
|
|
841
|
+
base = receiver[:m.start()].rstrip(". ")
|
|
842
|
+
return (self._map_group(base) + "/" + m.group(1).strip("/")).strip("/") if base else m.group(1).strip("/")
|
|
843
|
+
prefix, seen = [], set()
|
|
844
|
+
while receiver in self._groups and receiver not in seen:
|
|
845
|
+
seen.add(receiver)
|
|
846
|
+
receiver, part = self._groups[receiver]
|
|
847
|
+
prefix.insert(0, part.strip("/"))
|
|
848
|
+
return "/".join(p for p in prefix if p)
|
|
849
|
+
|
|
850
|
+
def _request_object(self, kind: str, ctor_args, cid: str, line: int) -> None:
|
|
851
|
+
"""new RestRequest("api/items", Method.Post) or new HttpRequestMessage(HttpMethod.Get, "/api/items"): a
|
|
852
|
+
request built as an object and sent later. A path a route may serve (see Indexer._resolve_endpoints)."""
|
|
853
|
+
strings = [a for a in ctor_args if a.type in STRINGS]
|
|
854
|
+
if not strings:
|
|
855
|
+
return
|
|
856
|
+
path = (_cs_strings(strings[0]) or [""])[0]
|
|
857
|
+
url = path if path.startswith("/") and path.count("/") >= 2 else _relative_url(path)
|
|
858
|
+
if not url:
|
|
859
|
+
return
|
|
860
|
+
m = re.search(r"\b(?:Http)?Method\.(Get|Post|Put|Delete|Patch)\b", " ".join(_text(a) for a in ctor_args), re.I)
|
|
861
|
+
self.res.endpoints.append(Endpoint("http", "maybe", cid, url.split("?", 1)[0], line, m.group(1).upper() if m else None))
|
|
862
|
+
|
|
863
|
+
def _endpoint(self, full: str, name: str, args, arg_nodes, cid: str, line: int) -> None:
|
|
864
|
+
"""Is this call one end of an HTTP or file channel?"""
|
|
865
|
+
first = arg_nodes[0] if arg_nodes else None
|
|
866
|
+
if first is not None and first.type in STRINGS and name in HTTP_CLIENT | HTTP_MAP:
|
|
867
|
+
path = (_cs_strings(first) or [""])[0]
|
|
868
|
+
if path.startswith(("/", "http://", "https://")) or name in HTTP_MAP:
|
|
869
|
+
role, method = ("serve", HTTP_MAP[name]) if name in HTTP_MAP else ("call", HTTP_CLIENT[name])
|
|
870
|
+
if role == "serve":
|
|
871
|
+
group = self._map_group(full.rsplit(".", 1)[0])
|
|
872
|
+
if group: # api = app.MapGroup("api/catalog"); api.MapGet("/items/{id}", ...)
|
|
873
|
+
path = group.rstrip("/") + "/" + path.lstrip("/") if path.strip("/") else group
|
|
874
|
+
self.res.endpoints.append(Endpoint("http", role, cid, path if path.startswith(("/", "http")) else "/" + path, line, method))
|
|
875
|
+
return
|
|
876
|
+
rel = _relative_url(path)
|
|
877
|
+
if rel: # client.GetAsync("api/items/1") against a base address, or $"{baseUrl}/api/items"
|
|
878
|
+
self.res.endpoints.append(Endpoint("http", "maybe", cid, rel, line, HTTP_CLIENT[name]))
|
|
879
|
+
return
|
|
880
|
+
if first is not None and first.type in STRINGS and name not in HTTP_CLIENT | HTTP_MAP \
|
|
881
|
+
and not FILE_CALL.search(full.replace("?", "")) and not full.endswith(("Path.Combine", "Path.Join")) \
|
|
882
|
+
and name not in NOT_REQUESTS:
|
|
883
|
+
# A request through the project's own wrapper (Get<T>("/api/items/1")): a path a route may serve.
|
|
884
|
+
path = (_cs_strings(first) or [""])[0]
|
|
885
|
+
path = path[2:] if path.startswith("{}/") else path
|
|
886
|
+
if not path.startswith("/"): # Get<T>("api/orders/all"), against a base address, as GetAsync takes it
|
|
887
|
+
path = _relative_url(path) or path
|
|
888
|
+
if path.startswith("/") and path.count("/") >= 2 and not any(ch.isspace() for ch in path) \
|
|
889
|
+
and "." not in path.rsplit("/", 1)[-1]:
|
|
890
|
+
self.res.endpoints.append(Endpoint("http", "maybe", cid, path.split("?", 1)[0], line, None))
|
|
891
|
+
return
|
|
892
|
+
m = FILE_CALL.search(full.replace("?", ""))
|
|
893
|
+
if m:
|
|
894
|
+
owner, op = m.group(1), m.group(2)
|
|
895
|
+
text = _text(args) if args is not None else ""
|
|
896
|
+
if owner == "FileAccess":
|
|
897
|
+
role = "write" if "Write" in text else "read" if op == "Open" else None
|
|
898
|
+
elif owner == "Directory":
|
|
899
|
+
role = "read" if op in ("GetFiles", "EnumerateFiles", "GetDirectories") else None
|
|
900
|
+
else:
|
|
901
|
+
role = ("write" if op.startswith(("Write", "Append", "Create", "OpenWrite", "Copy", "Move")) else
|
|
902
|
+
"read" if op.startswith(("Read", "OpenRead", "OpenText")) else None)
|
|
903
|
+
if role:
|
|
904
|
+
self.res.endpoints.append(Endpoint("file", role, cid, "", line, None, _cs_strings(args) if args is not None else []))
|
|
905
|
+
if full.endswith(("Path.Combine", "Path.Join")) and args is not None:
|
|
906
|
+
self.res.path_strings.setdefault(cid, []).extend(_cs_strings(args))
|
|
907
|
+
|
|
908
|
+
def _call_in(self, declarator):
|
|
909
|
+
"""The call a variable is initialised from, looking through `await` and parentheses."""
|
|
910
|
+
cur = declarator.named_children[-1] if declarator.named_child_count > 1 else None
|
|
911
|
+
if cur is not None and cur.type == "equals_value_clause":
|
|
912
|
+
cur = cur.named_children[-1] if cur.named_children else None
|
|
913
|
+
while cur is not None and cur.type in ("await_expression", "parenthesized_expression") and cur.named_children:
|
|
914
|
+
cur = cur.named_children[-1]
|
|
915
|
+
return cur if cur is not None and cur.type == "invocation_expression" else None
|
|
916
|
+
|
|
917
|
+
def _invocation(self, node, cid: str, type_id: Optional[str], scope: dict[str, str]):
|
|
918
|
+
key = (node.start_byte, node.end_byte)
|
|
919
|
+
if key in self.done_calls:
|
|
920
|
+
return self.done_calls[key] # already recorded as the receiver or initialiser of something
|
|
921
|
+
self.done_calls[key] = None
|
|
922
|
+
fn = node.child_by_field_name("function")
|
|
923
|
+
args = node.child_by_field_name("arguments")
|
|
924
|
+
argc = len([a for a in args.children if a.type == "argument"]) if args is not None else 0
|
|
925
|
+
line = node.start_point[0] + 1
|
|
926
|
+
if fn is None:
|
|
927
|
+
return
|
|
928
|
+
name = None
|
|
929
|
+
receiver = None
|
|
930
|
+
rtype = None
|
|
931
|
+
chain = None
|
|
932
|
+
targs = 0
|
|
933
|
+
generic = fn if fn.type == "generic_name" else (
|
|
934
|
+
fn.child_by_field_name("name") if fn.type == "member_access_expression" else None)
|
|
935
|
+
if generic is not None and generic.type == "generic_name":
|
|
936
|
+
tl = _child(generic, "type_argument_list")
|
|
937
|
+
targs = len(tl.named_children) if tl is not None else 0
|
|
938
|
+
if fn.type in ("identifier", "generic_name"):
|
|
939
|
+
name = _text(_child(fn, "identifier")) if fn.type == "generic_name" else _text(fn)
|
|
940
|
+
elif fn.type == "member_access_expression":
|
|
941
|
+
nm = fn.child_by_field_name("name")
|
|
942
|
+
name = _text(_child(nm, "identifier")) if nm is not None and nm.type == "generic_name" else _text(nm)
|
|
943
|
+
expr = fn.child_by_field_name("expression")
|
|
944
|
+
receiver, rtype = self._receiver(expr, scope, type_id)
|
|
945
|
+
inner = expr
|
|
946
|
+
while inner is not None and inner.type in ("parenthesized_expression", "await_expression") and inner.named_children:
|
|
947
|
+
inner = inner.named_children[-1]
|
|
948
|
+
if inner is not None and inner.type == "invocation_expression":
|
|
949
|
+
chain = self._invocation(inner, cid, type_id, scope) # a.Make().Run(): typed by what Make returns
|
|
950
|
+
receiver = "?"
|
|
951
|
+
elif rtype is None and receiver and "~" + receiver in scope:
|
|
952
|
+
chain = scope["~" + receiver]
|
|
953
|
+
elif fn.type == "conditional_access_expression":
|
|
954
|
+
binding = None
|
|
955
|
+
for c in fn.children:
|
|
956
|
+
if c.type == "member_binding_expression":
|
|
957
|
+
binding = c
|
|
958
|
+
if binding is not None:
|
|
959
|
+
name = _text(binding.child_by_field_name("name") or _child(binding, "identifier"))
|
|
960
|
+
receiver, rtype = self._receiver(fn.children[0], scope, type_id)
|
|
961
|
+
if not name:
|
|
962
|
+
return
|
|
963
|
+
if name == "nameof":
|
|
964
|
+
return
|
|
965
|
+
if name == "Invoke" and receiver and receiver[:1].isalpha() and receiver not in ("this", "base"):
|
|
966
|
+
self.res.events.append(EventUse("raise", cid, receiver, None, None, None, line, type_id))
|
|
967
|
+
arg_nodes = [a.named_children[-1] for a in args.children if a.type == "argument" and a.named_children] if args is not None else []
|
|
968
|
+
if (name in TEST_RUNNERS and len(arg_nodes) == 2 and arg_nodes[0].type in STRINGS
|
|
969
|
+
and arg_nodes[1].type in LAMBDAS):
|
|
970
|
+
title = _text(arg_nodes[0]).strip('@$"')
|
|
971
|
+
tid = f"{cid}/test:{re.sub(r'[^A-Za-z0-9]+', '-', title).strip('-').lower()[:80]}"
|
|
972
|
+
lam = arg_nodes[1]
|
|
973
|
+
self.res.nodes.append(Node(
|
|
974
|
+
id=tid, kind="test", name=title, parent_id=cid, language=LANGUAGE, path=self.path,
|
|
975
|
+
span_start=node.start_point[0] + 1, span_end=node.end_point[0] + 1,
|
|
976
|
+
attrs={"framework": "inline runner", "runner": name}))
|
|
977
|
+
self.redirect[(lam.start_byte, lam.end_byte)] = tid
|
|
978
|
+
self._endpoint(_text(fn), name, args, arg_nodes, cid, line)
|
|
979
|
+
site = CallSite(cid, name, receiver, rtype, argc, line, type_id, node.start_point[1],
|
|
980
|
+
tuple(_arg_hint(a, scope) for a in arg_nodes) if len(arg_nodes) == argc else (),
|
|
981
|
+
targs, chain)
|
|
982
|
+
self.res.calls.append(site)
|
|
983
|
+
self.done_calls[key] = site
|
|
984
|
+
return site
|
|
985
|
+
|
|
986
|
+
def _receiver(self, expr, scope: dict[str, str], type_id: Optional[str]):
|
|
987
|
+
if expr is None:
|
|
988
|
+
return None, None
|
|
989
|
+
t = expr.type
|
|
990
|
+
own = self.field_types.get(type_id or "", {})
|
|
991
|
+
if t in THIS:
|
|
992
|
+
return "this", None
|
|
993
|
+
if t in BASE:
|
|
994
|
+
return "base", None
|
|
995
|
+
if t == "identifier":
|
|
996
|
+
nm = _text(expr)
|
|
997
|
+
return nm, scope.get(nm) or own.get(nm)
|
|
998
|
+
if t == "generic_name":
|
|
999
|
+
names = _type_names(expr) # Policy<int>.Handle(): the generic Policy, not the plain one
|
|
1000
|
+
return _text(_child(expr, "identifier")), (names[0] if names and "`" in names[0] else None)
|
|
1001
|
+
if t == "member_access_expression":
|
|
1002
|
+
inner = expr.child_by_field_name("expression")
|
|
1003
|
+
nm = _text(expr.child_by_field_name("name"))
|
|
1004
|
+
if inner is not None and inner.type in THIS:
|
|
1005
|
+
return nm, own.get(nm)
|
|
1006
|
+
# a.b.Call(): the receiver is member b of something; mark it as chained.
|
|
1007
|
+
return "." + nm, None
|
|
1008
|
+
if t == "object_creation_expression":
|
|
1009
|
+
tn = _outer_type(expr.child_by_field_name("type"))
|
|
1010
|
+
return tn, tn
|
|
1011
|
+
if t == "parenthesized_expression" and expr.named_child_count == 1:
|
|
1012
|
+
return self._receiver(expr.named_children[0], scope, type_id)
|
|
1013
|
+
if t == "cast_expression":
|
|
1014
|
+
tn = _outer_type(expr.child_by_field_name("type"))
|
|
1015
|
+
return tn, tn
|
|
1016
|
+
if t == "element_access_expression":
|
|
1017
|
+
elem = self._elem_of(expr.child_by_field_name("expression"), scope, type_id)
|
|
1018
|
+
return "[]", elem
|
|
1019
|
+
return "?", None
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def parse(repo: str, rel_path: str, file_id: str, src: bytes, module: str = "") -> FileResult:
|
|
1023
|
+
w = _Walker(repo, rel_path, file_id, src, module)
|
|
1024
|
+
res = w.run()
|
|
1025
|
+
channels.extract(LANGUAGE, w.tree, res, file_id)
|
|
1026
|
+
return res
|