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.
Files changed (65) hide show
  1. leyline/__init__.py +3 -0
  2. leyline/adapters/__init__.py +19 -0
  3. leyline/adapters/csharp.py +1026 -0
  4. leyline/adapters/generic.py +844 -0
  5. leyline/adapters/python.py +929 -0
  6. leyline/adapters/typescript.py +1512 -0
  7. leyline/affected.py +509 -0
  8. leyline/agent_skills.py +311 -0
  9. leyline/change.py +500 -0
  10. leyline/channels.py +1782 -0
  11. leyline/cli.py +1029 -0
  12. leyline/cluster.py +149 -0
  13. leyline/context.py +531 -0
  14. leyline/coupling.py +485 -0
  15. leyline/coverage.py +376 -0
  16. leyline/diagrams.py +680 -0
  17. leyline/diff.py +1078 -0
  18. leyline/drift.py +747 -0
  19. leyline/exact.py +410 -0
  20. leyline/explain.py +973 -0
  21. leyline/export.py +673 -0
  22. leyline/fncoupling.py +328 -0
  23. leyline/formats.py +923 -0
  24. leyline/grade.py +207 -0
  25. leyline/incremental.py +931 -0
  26. leyline/indexer.py +3733 -0
  27. leyline/learnings.py +743 -0
  28. leyline/loop.py +584 -0
  29. leyline/model.py +142 -0
  30. leyline/outline.py +1066 -0
  31. leyline/packing.py +86 -0
  32. leyline/patterns.py +478 -0
  33. leyline/pr.py +1249 -0
  34. leyline/props.py +255 -0
  35. leyline/query.py +683 -0
  36. leyline/quick.py +853 -0
  37. leyline/related.py +266 -0
  38. leyline/removal.py +231 -0
  39. leyline/renames.py +231 -0
  40. leyline/rereview.py +375 -0
  41. leyline/roslyn_refs/Program.cs +179 -0
  42. leyline/roslyn_refs/RoslynRefs.csproj +16 -0
  43. leyline/rules.py +171 -0
  44. leyline/schema.sql +178 -0
  45. leyline/scip_pb2.py +97 -0
  46. leyline/server.py +1230 -0
  47. leyline/skills/leyline-adversarial-review/SKILL.md +121 -0
  48. leyline/skills/leyline-ask/SKILL.md +57 -0
  49. leyline/skills/leyline-change-impact/SKILL.md +79 -0
  50. leyline/skills/leyline-explain-flow/SKILL.md +70 -0
  51. leyline/skills/leyline-explore-module/SKILL.md +55 -0
  52. leyline/skills/leyline-pr-review/SKILL.md +77 -0
  53. leyline/skills/leyline-quick-change/SKILL.md +63 -0
  54. leyline/skills/leyline-spec/SKILL.md +179 -0
  55. leyline/skills/leyline-tour/SKILL.md +46 -0
  56. leyline/spec.py +1819 -0
  57. leyline/store.py +560 -0
  58. leyline/tours.py +352 -0
  59. leyline/verdicts.py +390 -0
  60. leyline/viewer/viewer.html +1622 -0
  61. leyline_code-0.8.1.dist-info/METADATA +1325 -0
  62. leyline_code-0.8.1.dist-info/RECORD +65 -0
  63. leyline_code-0.8.1.dist-info/WHEEL +4 -0
  64. leyline_code-0.8.1.dist-info/entry_points.txt +2 -0
  65. leyline_code-0.8.1.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,1512 @@
1
+ """TypeScript and JavaScript adapter: tree-sitter structure, imports, call sites, JSX, and inline tests.
2
+
3
+ A file is a module. Functions, classes, interfaces, type aliases and enums become nodes; so does a
4
+ `const` that holds a function, and a top-level `const` object whose properties are functions (it is
5
+ treated as a type, so `api.load()` finds `load`). Anonymous callbacks are not nodes: what they call is
6
+ attributed to the function they are written in. Two exceptions: an inline test (`it("x", () => ...)`), and the
7
+ inline handler of a server route (`server.get("/api/x", async (req) => ...)`), which becomes a function named for its
8
+ route (`GET /api/x`), nested in the function that registers it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import posixpath
14
+ import re
15
+ import os
16
+ import sys
17
+ from typing import Optional
18
+
19
+ import tree_sitter_typescript
20
+ from tree_sitter import Language, Parser
21
+
22
+ from .. import channels
23
+ from ..model import CallSite, Edge, Endpoint, FieldUse, FileResult, ImportRef, Node, Spawn, TypeRef
24
+ from .python import pathlike
25
+
26
+ NAME = "tree-sitter-typescript"
27
+ VERSION = "0.1"
28
+ LANGUAGE = "typescript"
29
+ EXTENSIONS = (".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs")
30
+ CTOR = "constructor"
31
+
32
+ # Deeply nested JSX and long call chains recurse deeply. Python before 3.12 on Windows does not stop a recursion
33
+ # before the main thread's stack runs out (the process dies), so there the default limit stays, and a file nested
34
+ # past it is reported (see indexer._parse_file).
35
+ if sys.getrecursionlimit() < 6000 and not (os.name == "nt" and sys.version_info < (3, 12)):
36
+ sys.setrecursionlimit(6000)
37
+
38
+ _ts = Parser(Language(tree_sitter_typescript.language_typescript()))
39
+ _tsx = Parser(Language(tree_sitter_typescript.language_tsx()))
40
+
41
+ _EXT = re.compile(r"\.(d\.[cm]?ts|[cm]?[jt]sx?)$")
42
+ FUNCS = ("arrow_function", "function_expression", "generator_function", "function")
43
+ # Calls that hand back the function they are given, so `const x = useCallback(() => ..., [])` declares x.
44
+ PASS_THROUGH = {"memo", "forwardRef", "useCallback", "debounce", "throttle", "observer", "action", "cache"}
45
+ TEST_FNS = {"it", "test", "bench", "specify"}
46
+ SUITE_FNS = {"describe", "suite", "context"}
47
+ TEST_MODS = {"only", "skip", "concurrent", "sequential", "fails", "todo", "skipIf", "runIf", "each", "for"}
48
+ HTTP_VERBS = {"get", "post", "put", "delete", "patch", "head", "options"}
49
+ SERVERS = re.compile(r"(app|server|fastify|router|instance|api|route[rs]?)$", re.I)
50
+ CLIENTS = re.compile(r"(axios|http|client|request|ky|got|api)$", re.I)
51
+ # Names a server and a client both go by: api, and instance (a Fastify plugin's, or what axios.create() returns).
52
+ EITHER = re.compile(r"(api|instance)$", re.I)
53
+ FS_READ = {"readFile", "readFileSync", "readdir", "readdirSync", "createReadStream", "readJson", "readJsonSync", "opendir"}
54
+ FS_WRITE = {"writeFile", "writeFileSync", "appendFile", "appendFileSync", "createWriteStream", "writeJson",
55
+ "writeJsonSync", "copyFile", "copyFileSync", "outputFile"}
56
+ LAUNCHERS = {"spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync", "fork"}
57
+ MAIN_NAMES = {"main", "run", "start", "bootstrap", "cli", "serve"}
58
+ # Objects every JavaScript program is given. A call on one of them is never a call into this code.
59
+ GLOBALS = frozenset("""JSON Math Object Array Promise console Number String Boolean Date Reflect Symbol Intl Error RegExp
60
+ window document process Buffer globalThis navigator localStorage sessionStorage crypto performance history location
61
+ URL URLSearchParams Map Set WeakMap WeakSet Atomics BigInt Proxy structuredClone AbortSignal TextEncoder TextDecoder
62
+ expect vi jest React ReactDOM fs path os util http https url child_process zlib stream events assert""".split())
63
+ # Method names so common on built-in and library objects that sharing one says nothing about which function is meant.
64
+ COMMON_METHODS = frozenset("""push pop shift unshift splice slice map filter reduce forEach find findIndex some every includes
65
+ indexOf join keys values entries get set has add delete clear parse stringify log warn error info debug replace replaceAll
66
+ at split trim match test exec then catch finally toString call apply bind sort reverse concat flat flatMap fill from of
67
+ assign freeze click press type goto evaluate close open send status json text code header on off once emit write read end
68
+ render resolve reject all race now count first last nth focus blur hover check change wait run start stop next size append
69
+ remove insert update create load save pause play subscribe unsubscribe dispatch fetch search reset init request abort move
70
+ select reload route observe disconnect decode encode post put patch head options listen use register name value label
71
+ toJSON valueOf equals compare clone copy merge format print show hide toggle enable disable validate""".split())
72
+ # Methods that change the collection they are called on. A field used this way is written, not only read.
73
+ MUTATORS = frozenset("push pop shift unshift splice sort reverse set delete add clear fill copyWithin".split())
74
+ WRAPPERS = {"Promise", "Array", "ReadonlyArray", "Readonly", "Partial", "Required", "Pick", "Omit", "Record", "Map", "Set",
75
+ "WeakMap", "WeakSet", "Awaited", "NonNullable", "ReturnType", "Parameters", "Iterable", "Iterator",
76
+ "AsyncIterable", "AsyncIterator", "Generator", "AsyncGenerator", "Exclude", "Extract", "Uint8Array", "Buffer",
77
+ "Date", "Error", "RegExp", "Function", "Object", "String", "Number", "Boolean", "Symbol", "JSX", "React",
78
+ "ReactNode", "ReactElement", "Element", "FC", "PropsWithChildren", "Dispatch", "SetStateAction", "RefObject",
79
+ "MutableRefObject", "T", "K", "V", "U"}
80
+
81
+
82
+ def _text(node) -> str:
83
+ return node.text.decode("utf8", "replace") if node is not None else ""
84
+
85
+
86
+ def _squash(s: str) -> str:
87
+ return re.sub(r"\s+", " ", s).strip()
88
+
89
+
90
+ def module_path(rel_path: str, keep_ext: bool = False) -> str:
91
+ """keep_ext: another file in the directory differs from this one only in its extension (a.js beside a.ts), and
92
+ the indexer gave it the plain name; this one keeps its extension so that their ids do not collide."""
93
+ return (rel_path if keep_ext else _EXT.sub("", rel_path)).replace("/", ".")
94
+
95
+
96
+ def _string(node) -> Optional[str]:
97
+ """The text of a string or template literal; substitutions become {}."""
98
+ if node is None:
99
+ return None
100
+ if node.type == "string":
101
+ return "".join(_text(c) for c in node.children if c.type in ("string_fragment", "escape_sequence"))
102
+ if node.type == "template_string":
103
+ return "".join(_text(c) if c.type in ("string_fragment", "escape_sequence") else "{}"
104
+ for c in node.children if c.type in ("string_fragment", "escape_sequence", "template_substitution"))
105
+ return None
106
+
107
+
108
+ def _strings(node) -> list[str]:
109
+ out, stack = [], [node]
110
+ while stack:
111
+ n = stack.pop()
112
+ if n.type in ("string", "template_string"):
113
+ s = _string(n)
114
+ if s:
115
+ out.append(s)
116
+ else:
117
+ stack.extend(reversed(n.children))
118
+ return out
119
+
120
+
121
+ def _type_names(node) -> list[str]:
122
+ """Named types in a type expression, without the wrappers around them: Promise<Foo[]> -> [Foo]."""
123
+ out, stack = [], [node] if node is not None else []
124
+ while stack:
125
+ n = stack.pop()
126
+ if n.type == "type_identifier":
127
+ if _text(n) not in WRAPPERS:
128
+ out.append(_text(n))
129
+ elif n.type == "nested_type_identifier":
130
+ last = _text(n).rsplit(".", 1)[-1]
131
+ if last not in WRAPPERS:
132
+ out.append(last)
133
+ elif n.type not in ("object_type", "function_type", "literal_type", "type_query"):
134
+ stack.extend(reversed(n.children))
135
+ return out
136
+
137
+
138
+ def _unwrap(node):
139
+ while node is not None and node.type in ("parenthesized_expression", "non_null_expression", "as_expression",
140
+ "satisfies_expression", "await_expression", "type_assertion"):
141
+ inner = [c for c in node.named_children if c.type != "comment"]
142
+ if not inner:
143
+ break
144
+ node = inner[-1] if node.type == "type_assertion" else inner[0]
145
+ return node
146
+
147
+
148
+ IMPORT_TYPE = re.compile(r"typeof\s+import\(\s*['\"]([^'\"]+)['\"]\s*\)")
149
+
150
+
151
+ def _dynamic_import(node) -> Optional[str]:
152
+ """The module named by `import("x")` or `await import("x")`, if that is what this expression is."""
153
+ node = _unwrap(node)
154
+ if node is not None and node.type == "call_expression":
155
+ fn = node.child_by_field_name("function")
156
+ args = node.child_by_field_name("arguments")
157
+ if fn is not None and fn.type == "import" and args is not None and args.named_children:
158
+ return _string(args.named_children[0])
159
+ return None
160
+
161
+
162
+ def _pattern_names(pattern) -> list[tuple[str, str]]:
163
+ """(name taken, local name) for each simple entry of an object pattern."""
164
+ out = []
165
+ for p in pattern.named_children:
166
+ if p.type == "shorthand_property_identifier_pattern":
167
+ out.append((_text(p), _text(p)))
168
+ elif p.type == "pair_pattern":
169
+ key, value = p.child_by_field_name("key"), p.child_by_field_name("value")
170
+ if key is not None and value is not None and value.type == "identifier":
171
+ out.append((_text(key), _text(value)))
172
+ return out
173
+
174
+
175
+ def _arity(arg) -> Optional[int]:
176
+ """How many parameters a callback argument takes; None when the argument is not a function."""
177
+ fn = _unwrap(arg)
178
+ if fn is None or fn.type not in FUNCS:
179
+ return None
180
+ params = fn.child_by_field_name("parameters")
181
+ return len([p for p in params.named_children if p.type != "comment"]) if params is not None else 1
182
+
183
+
184
+ def _walk_nodes(node, types, stop=()):
185
+ """Nodes of these types under a node, not looking inside the `stop` ones."""
186
+ out, stack = [], list(node.named_children) if node is not None else []
187
+ while stack:
188
+ n = stack.pop()
189
+ if n.type in types:
190
+ out.append(n)
191
+ if n.type not in stop:
192
+ stack.extend(n.named_children)
193
+ return out
194
+
195
+
196
+ def _same(a, b) -> bool:
197
+ return a is not None and b is not None and a.start_byte == b.start_byte and a.end_byte == b.end_byte
198
+
199
+
200
+ class _Walker:
201
+ def __init__(self, repo: str, rel_path: str, file_id: str, src: bytes):
202
+ self.repo, self.path, self.file_id, self.src = repo, rel_path, file_id, src
203
+ self.mod = module_path(rel_path)
204
+ self.res = FileResult(declares=[_EXT.sub("", rel_path)])
205
+ ext = rel_path.rsplit(".", 1)[-1]
206
+ self.tree = (_ts if ext in ("ts", "mts", "cts") else _tsx).parse(src)
207
+ self.top_id = f"{repo}:typescript:{self.mod}.<module>"
208
+ self.top_used = False
209
+ self.entry = False
210
+ self.ids: set[str] = set()
211
+ self.by_id: dict[str, Node] = {}
212
+ self.self_types: dict[str, dict[str, str]] = {}
213
+ self.done_calls: dict = {}
214
+ self.redirect: dict = {} # callback span -> (test id, qualified name)
215
+ self.suites: list[str] = []
216
+ self.consts: dict[str, list[str]] = {}
217
+ self.all_strings: Optional[list[str]] = None
218
+ self.default_name: Optional[str] = None
219
+ self.exported: set[str] = set()
220
+ self.module_aliases: set[str] = set() # type X = typeof import("./y"): X stands for the module
221
+ self.stores: set[str] = set()
222
+ base = rel_path.rsplit("/", 1)[-1]
223
+ self.is_test_file = bool(re.search(r"\.(test|spec|bench)\.[cm]?[jt]sx?$", base)) or "/__tests__/" in "/" + rel_path
224
+ self.is_jsx = ext in ("tsx", "jsx")
225
+ # Names that are functions somewhere in this file, or come from an import: a bare mention of one
226
+ # as an argument (onClick={save}, items.map(render)) hands the function over to be called.
227
+ self.fn_names: set[str] = set()
228
+ self.fn_nodes: dict = {} # name -> the function's node, for a program path a helper of the file builds
229
+ self.imported: set[str] = set()
230
+ self.ns_members: dict[str, set] = {} # namespace id -> names of the functions declared in it
231
+ self.made_fns: set[str] = set() # const h = withAuth(async (req) => ...): a function a call wraps
232
+ self._prescan(self.tree.root_node)
233
+
234
+ def _prescan(self, root) -> None:
235
+ stack = [root]
236
+ while stack:
237
+ n = stack.pop()
238
+ t = n.type
239
+ if t in ("function_declaration", "generator_function_declaration"):
240
+ self.fn_names.add(_text(n.child_by_field_name("name")))
241
+ self.fn_nodes.setdefault(_text(n.child_by_field_name("name")), n)
242
+ elif t == "variable_declarator":
243
+ name, value = n.child_by_field_name("name"), n.child_by_field_name("value")
244
+ if name is not None and name.type == "identifier" and self._fn_of(value) is not None:
245
+ self.fn_names.add(_text(name))
246
+ self.fn_nodes.setdefault(_text(name), self._fn_of(value))
247
+ elif name is not None and name.type == "identifier" and _unwrap(value) is not None \
248
+ and _unwrap(value).type == "call_expression" and any(
249
+ _unwrap(a) is not None and _unwrap(a).type in FUNCS
250
+ for a in (_unwrap(value).child_by_field_name("arguments") or value).named_children):
251
+ self.made_fns.add(_text(name))
252
+ elif name is not None and name.type == "identifier" and value is not None and n.parent is not None \
253
+ and n.parent.parent is not None and n.parent.parent.type in ("program", "export_statement"):
254
+ found = _strings(value) if value.type in ("string", "template_string", "array") else []
255
+ if found:
256
+ self.consts[_text(name)] = found
257
+ elif t == "import_statement":
258
+ for c in n.children:
259
+ if c.type == "import_clause":
260
+ for d in c.children:
261
+ if d.type == "identifier":
262
+ self.imported.add(_text(d))
263
+ elif d.type == "named_imports":
264
+ for s in d.named_children:
265
+ if s.type == "import_specifier":
266
+ self.imported.add(_text(s.child_by_field_name("alias") or s.child_by_field_name("name")))
267
+ continue
268
+ stack.extend(n.children)
269
+
270
+ def _helper_paths(self, call, arg_nodes) -> Optional[list[str]]:
271
+ """The paths a launch call's program comes from when a function of this file computes it: an argument (or a
272
+ local variable it names) calls `hostEntry()`, which returns `resolve(__dirname, "..", "host", "index.js")`.
273
+ None when no argument calls a function of this file; otherwise the paths those functions build, relative to
274
+ the repository root (possibly none)."""
275
+ scope = call.parent
276
+ while scope is not None and scope.type not in FUNCS and scope.type not in ("function_declaration", "generator_function_declaration",
277
+ "method_definition", "program"):
278
+ scope = scope.parent
279
+ decls: dict = {}
280
+ stack = [scope] if scope is not None else []
281
+ while stack:
282
+ n = stack.pop()
283
+ if n.type == "variable_declarator" and n.child_by_field_name("name") is not None:
284
+ decls.setdefault(_text(n.child_by_field_name("name")), n.child_by_field_name("value"))
285
+ stack.extend(n.named_children)
286
+ helpers, stack, looked = [], list(arg_nodes), set()
287
+ while stack:
288
+ n = stack.pop()
289
+ if n is None:
290
+ continue
291
+ if n.type == "identifier" and _text(n) in decls and _text(n) not in looked:
292
+ looked.add(_text(n))
293
+ stack.append(decls[_text(n)])
294
+ continue
295
+ if n.type == "call_expression":
296
+ f = _unwrap(n.child_by_field_name("function"))
297
+ if f is not None and f.type == "identifier" and _text(f) in self.fn_nodes:
298
+ helpers.append(self.fn_nodes[_text(f)])
299
+ continue
300
+ if n.type not in FUNCS:
301
+ stack.extend(n.named_children)
302
+ if not helpers:
303
+ return None
304
+ here = self.path.rsplit("/", 1)[0] if "/" in self.path else ""
305
+ out = []
306
+ for fn in helpers:
307
+ body = fn.child_by_field_name("body")
308
+ exprs = [body] if body is not None and body.type != "statement_block" else [] # () => resolve(...)
309
+ stack = [body] if body is not None else []
310
+ while stack:
311
+ n = stack.pop()
312
+ if n.type == "return_statement":
313
+ exprs.extend(n.named_children)
314
+ elif n.type not in FUNCS:
315
+ stack.extend(n.named_children)
316
+ for e in exprs:
317
+ e = _unwrap(e)
318
+ if e is None or e.type != "call_expression":
319
+ continue
320
+ f, a = e.child_by_field_name("function"), e.child_by_field_name("arguments")
321
+ if f is None or a is None or _text(f).rsplit(".", 1)[-1] not in ("resolve", "join"):
322
+ continue
323
+ parts = [x for x in a.named_children if x.type != "comment"]
324
+ if not parts or _text(parts[0]) not in ("__dirname", "import.meta.dirname"):
325
+ continue
326
+ bits = [_string(x) for x in parts[1:]]
327
+ if not bits or any(b is None for b in bits):
328
+ continue
329
+ segs: Optional[list] = []
330
+ for seg in "/".join([here] + bits).split("/"):
331
+ if seg in ("", "."):
332
+ continue
333
+ if seg != "..":
334
+ segs.append(seg)
335
+ elif segs:
336
+ segs.pop()
337
+ else:
338
+ segs = None # above the repository root
339
+ break
340
+ if segs:
341
+ out.append("/".join(segs))
342
+ return list(dict.fromkeys(out))
343
+
344
+ def _fn_of(self, value):
345
+ """The function a declarator's value amounts to, if any."""
346
+ value = _unwrap(value)
347
+ if value is None:
348
+ return None
349
+ if value.type in FUNCS:
350
+ return value
351
+ if value.type == "call_expression":
352
+ fn = value.child_by_field_name("function")
353
+ args = value.child_by_field_name("arguments")
354
+ if fn is not None and args is not None and _text(fn).rsplit(".", 1)[-1] in PASS_THROUGH:
355
+ first = next((a for a in args.named_children if a.type != "comment"), None)
356
+ first = _unwrap(first)
357
+ if first is not None and first.type in FUNCS:
358
+ return first
359
+ return None
360
+
361
+ def run(self) -> FileResult:
362
+ root = self.tree.root_node
363
+ if self.src.startswith(b"#!"):
364
+ self.entry = True
365
+ self._walk_children(root, self.top_id, None, {}, self.mod, True)
366
+ if self.default_name:
367
+ for n in self.res.nodes:
368
+ if n.name == self.default_name and n.parent_id == self.file_id:
369
+ n.attrs["is_default_export"] = True
370
+ break
371
+ for n in self.res.nodes:
372
+ if n.parent_id == self.file_id and n.name in self.exported:
373
+ n.attrs["visibility"] = "public"
374
+ if self.entry:
375
+ self.top_used = True
376
+ self.res.nodes.append(Node(
377
+ id=self.top_id + "#entry", kind="entry_point", name=self.path, parent_id=self.file_id,
378
+ language=LANGUAGE, path=self.path, span_start=1, span_end=root.end_point[0] + 1,
379
+ attrs={"trigger": "ui" if self.is_jsx else "cli", "address": self.path}))
380
+ self.res.edges.append(Edge("exposes", self.top_id + "#entry", self.top_id))
381
+ if self.top_used:
382
+ self.res.nodes.append(Node(
383
+ id=self.top_id, kind="callable", name="<module>", parent_id=self.file_id,
384
+ language=LANGUAGE, path=self.path, span_start=1, span_end=root.end_point[0] + 1,
385
+ attrs={"signature": f"module {self.path}", "visibility": "public",
386
+ "native_kind": "module_body", "argc_min": 0, "argc_max": 0}))
387
+ return self.res
388
+
389
+ def _add(self, node: Node) -> bool:
390
+ if node.id in self.ids:
391
+ return False
392
+ self.ids.add(node.id)
393
+ self.by_id[node.id] = node
394
+ self.res.nodes.append(node)
395
+ if node.parent_id == self.top_id:
396
+ self.top_used = True
397
+ return True
398
+
399
+ # -- walking ---------------------------------------------------------------
400
+ def _walk_children(self, node, cid, class_id, scope, qual, top=False) -> None:
401
+ for c in node.children:
402
+ self._walk(c, cid, class_id, scope, qual, top)
403
+
404
+ def _walk(self, node, cid, class_id, scope, qual, top=False, exported=False) -> None:
405
+ t = node.type
406
+ if t == "comment":
407
+ return
408
+ if t == "import_statement":
409
+ self._import(node)
410
+ return
411
+ if t == "export_statement":
412
+ self._export(node, cid, class_id, scope, qual, top)
413
+ return
414
+ if t in ("function_declaration", "generator_function_declaration"):
415
+ name = _text(node.child_by_field_name("name")) or "default"
416
+ self._function(name, node, node, self.file_id if top else cid, qual, None, scope, exported=exported)
417
+ return
418
+ if t in ("class_declaration", "abstract_class_declaration", "class"):
419
+ name = _text(node.child_by_field_name("name"))
420
+ if name:
421
+ self._class(name, node, self.file_id if top else cid, qual, exported)
422
+ return
423
+ if t in ("interface_declaration", "type_alias_declaration", "enum_declaration"):
424
+ self._typeish(node, self.file_id if top else cid, qual, exported)
425
+ return
426
+ if t in ("lexical_declaration", "variable_declaration"):
427
+ for d in node.named_children:
428
+ if d.type == "variable_declarator":
429
+ self._declarator(d, node, cid, class_id, scope, qual, top, exported)
430
+ return
431
+ if t in ("internal_module", "module") and node.child_by_field_name("name") is not None \
432
+ and node.child_by_field_name("name").type in ("identifier", "nested_identifier"):
433
+ self._namespace(node, self.file_id if top else cid, qual, exported)
434
+ return
435
+ if t in ("function_signature", "ambient_declaration", "internal_module", "module"):
436
+ if t in ("ambient_declaration", "internal_module", "module"):
437
+ self._walk_children(node, cid, class_id, scope, qual, top)
438
+ return
439
+ if t in FUNCS:
440
+ self._callback(node, cid, class_id, scope, qual)
441
+ return
442
+ if t == "call_expression":
443
+ self._call_node(node, cid, class_id, scope, qual, top)
444
+ return
445
+ if t == "new_expression":
446
+ self._new(node, cid, class_id, scope)
447
+ elif t == "member_expression":
448
+ self._use(node, cid, class_id, scope)
449
+ elif t in ("jsx_opening_element", "jsx_self_closing_element"):
450
+ self._jsx(node, cid, class_id)
451
+ elif t == "assignment_expression":
452
+ if top and self._commonjs(node, qual, scope):
453
+ return
454
+ self._assign(node, cid, class_id, scope)
455
+ elif t in ("string", "template_string"):
456
+ s = _string(node)
457
+ if s and pathlike(s):
458
+ self.res.path_strings.setdefault(cid, []).append(s)
459
+ if t == "string":
460
+ return
461
+ elif t in ("jsx_expression", "pair", "shorthand_property_identifier"):
462
+ self._reference(node, cid, class_id, scope)
463
+ elif t == "object" and cid is not None:
464
+ self._object_members(node, cid, class_id, scope, qual)
465
+ return
466
+ self._walk_children(node, cid, class_id, scope, qual, top and t in ("expression_statement", "await_expression"))
467
+
468
+ def _callback(self, fn, cid, class_id, scope, qual) -> None:
469
+ """An anonymous function: what it does is attributed to the function it is written in."""
470
+ key = (fn.start_byte, fn.end_byte)
471
+ if key in self.redirect:
472
+ cid, qual = self.redirect[key]
473
+ inner = dict(scope)
474
+ self._params(fn, cid, inner, declare=False)
475
+ body = fn.child_by_field_name("body")
476
+ if body is not None:
477
+ self._walk(body, cid, class_id, inner, qual)
478
+
479
+ def _export(self, node, cid, class_id, scope, qual, top) -> None:
480
+ source = node.child_by_field_name("source")
481
+ is_default = any(c.type == "default" for c in node.children)
482
+ if source is not None:
483
+ target = self._spec(_string(source) or "")
484
+ clause = next((c for c in node.children if c.type == "export_clause"), None)
485
+ ns = next((c for c in node.children if c.type == "namespace_export"), None)
486
+ if clause is not None:
487
+ symbols = []
488
+ for s in clause.named_children:
489
+ if s.type == "export_specifier":
490
+ name, alias = _text(s.child_by_field_name("name")), _text(s.child_by_field_name("alias"))
491
+ symbols.append(f"{name} as {alias}" if alias and alias != name else name)
492
+ self.res.imports.append(ImportRef(self.file_id, target, symbols=symbols))
493
+ elif ns is not None:
494
+ self.res.imports.append(ImportRef(self.file_id, target, alias=_text(ns.named_children[0]) if ns.named_children else None))
495
+ else:
496
+ self.res.imports.append(ImportRef(self.file_id, target, symbols=["*"]))
497
+ return
498
+ decl = node.child_by_field_name("declaration")
499
+ value = node.child_by_field_name("value")
500
+ if decl is not None:
501
+ if is_default:
502
+ self.default_name = _text(decl.child_by_field_name("name")) or "default"
503
+ self._walk(decl, cid, class_id, scope, qual, top, exported=True)
504
+ elif value is not None:
505
+ inner = _unwrap(value)
506
+ if inner.type == "identifier":
507
+ self.default_name = _text(inner)
508
+ return
509
+ fn = self._fn_of(inner)
510
+ if fn is not None:
511
+ self.default_name = _text(fn.child_by_field_name("name")) or "default"
512
+ self._function(self.default_name, fn, node, self.file_id, qual, None, scope, exported=True)
513
+ elif inner.type == "class": # export default class { ... }: named default, as a function would be
514
+ self.default_name = _text(inner.child_by_field_name("name")) or "default"
515
+ self._class(self.default_name, inner, self.file_id, qual, True)
516
+ else:
517
+ self._walk(value, cid, class_id, scope, qual)
518
+ else:
519
+ for c in node.children:
520
+ if c.type == "export_clause":
521
+ for s in c.named_children:
522
+ if s.type == "export_specifier":
523
+ name, alias = _text(s.child_by_field_name("name")), _text(s.child_by_field_name("alias"))
524
+ if alias == "default":
525
+ self.default_name = name
526
+ self.exported.add(name)
527
+
528
+ # -- imports ---------------------------------------------------------------
529
+ def _spec(self, spec: str) -> str:
530
+ """A relative specifier becomes ./<path from the repo root, without extension>; a package name stays."""
531
+ if spec.startswith("."):
532
+ joined = posixpath.normpath(posixpath.join(posixpath.dirname(self.path), spec))
533
+ return "./" + _EXT.sub("", joined)
534
+ return spec
535
+
536
+ def _import(self, node) -> None:
537
+ source = node.child_by_field_name("source")
538
+ req = next((c for c in node.children if c.type == "import_require_clause"), None)
539
+ if req is not None:
540
+ src = next((c for c in req.children if c.type == "string"), None)
541
+ name = next((c for c in req.children if c.type == "identifier"), None)
542
+ self.res.imports.append(ImportRef(self.file_id, self._spec(_string(src) or ""), alias=_text(name) or None))
543
+ return
544
+ if source is None:
545
+ return
546
+ target = self._spec(_string(source) or "")
547
+ symbols, alias = [], None
548
+ for c in node.children:
549
+ if c.type != "import_clause":
550
+ continue
551
+ for d in c.children:
552
+ if d.type == "identifier":
553
+ symbols.append(f"default as {_text(d)}")
554
+ elif d.type == "namespace_import":
555
+ alias = _text(next((x for x in d.children if x.type == "identifier"), None)) or None
556
+ elif d.type == "named_imports":
557
+ for s in d.named_children:
558
+ if s.type == "import_specifier":
559
+ name, al = _text(s.child_by_field_name("name")), _text(s.child_by_field_name("alias"))
560
+ symbols.append(f"{name} as {al}" if al and al != name else name)
561
+ if alias:
562
+ self.res.imports.append(ImportRef(self.file_id, target, alias=alias))
563
+ if symbols or not alias:
564
+ self.res.imports.append(ImportRef(self.file_id, target, symbols=symbols))
565
+
566
+ # -- declarations ----------------------------------------------------------
567
+ def _declarator(self, d, stmt, cid, class_id, scope, qual, top, exported) -> None:
568
+ name_node, value = d.child_by_field_name("name"), d.child_by_field_name("value")
569
+ ann = d.child_by_field_name("type")
570
+ inner = _unwrap(value)
571
+ dyn = _dynamic_import(value)
572
+ if dyn and name_node is not None and name_node.type in ("identifier", "object_pattern"):
573
+ target = self._spec(dyn) # const files = await import("./files.js") / const { a, b } = await import(...)
574
+ if name_node.type == "identifier":
575
+ self.res.imports.append(ImportRef(self.file_id, target, alias=_text(name_node)))
576
+ else:
577
+ self.res.imports.append(ImportRef(self.file_id, target, symbols=[
578
+ a if a == b else f"{a} as {b}" for a, b in _pattern_names(name_node)]))
579
+ return
580
+ if name_node is not None and name_node.type == "identifier" and ann is not None:
581
+ m = IMPORT_TYPE.search(_text(ann))
582
+ if m: # let files: typeof import("./files.js")
583
+ self.res.imports.append(ImportRef(self.file_id, self._spec(m.group(1)), alias=_text(name_node)))
584
+ if value is not None:
585
+ self._walk(value, cid, class_id, scope, qual)
586
+ return
587
+ if name_node is not None and name_node.type == "object_pattern" and inner is not None and inner.type == "call_expression":
588
+ # const { save, load } = useStore() / = useStore.getState()
589
+ callee = _unwrap(inner.child_by_field_name("function"))
590
+ owner = _text(callee) if callee is not None and callee.type == "identifier" else \
591
+ _text(callee.child_by_field_name("object")) if callee is not None and callee.type == "member_expression" \
592
+ and _text(callee.child_by_field_name("property")) == "getState" else ""
593
+ if owner in self.imported or owner in self.stores:
594
+ for taken, local in _pattern_names(name_node):
595
+ scope["@" + local] = (owner, taken)
596
+ if name_node is None or name_node.type != "identifier":
597
+ # const { a, b } = require("x") / const [x, setX] = useState()
598
+ if inner is not None and inner.type == "call_expression" and _text(inner.child_by_field_name("function")) == "require":
599
+ target = self._require_target(inner)
600
+ if target and name_node is not None and name_node.type == "object_pattern":
601
+ syms = [_text(p.child_by_field_name("key") or p) for p in name_node.named_children
602
+ if p.type in ("shorthand_property_identifier_pattern", "pair_pattern")]
603
+ self.res.imports.append(ImportRef(self.file_id, target, symbols=syms))
604
+ return
605
+ if value is not None:
606
+ self._walk(value, cid, class_id, scope, qual)
607
+ return
608
+ name = _text(name_node)
609
+ for mark in ("", "~", "=", "@"):
610
+ scope.pop(mark + name, None)
611
+ fn = self._fn_of(value)
612
+ if fn is not None:
613
+ self._function(name, fn, stmt, self.file_id if top else cid, qual, class_id if fn.type == "arrow_function" else None,
614
+ scope, exported=exported)
615
+ if not _same(inner, fn): # the wrapper call's other arguments
616
+ args = inner.child_by_field_name("arguments")
617
+ for a in (args.named_children if args is not None else []):
618
+ if not _same(_unwrap(a), fn):
619
+ self._walk(a, cid, class_id, scope, qual)
620
+ return
621
+ if inner is not None and inner.type == "call_expression" and _text(inner.child_by_field_name("function")) == "require":
622
+ target = self._require_target(inner)
623
+ if target:
624
+ self.res.imports.append(ImportRef(self.file_id, target, alias=name))
625
+ return
626
+ if top and inner is not None and inner.type == "object" and self._object_type(name, inner, stmt, qual, exported):
627
+ return
628
+ if top and inner is not None and inner.type == "call_expression":
629
+ # const useStore = create<State>((set, get) => ({ save() {...}, load: () => {...} })): a type with those methods
630
+ made = self._made_object(inner)
631
+ if made is not None and self._object_type(name, made, stmt, qual, exported, native="store"):
632
+ self.stores.add(name)
633
+ self._call(inner, cid, class_id, scope)
634
+ return
635
+ if inner is not None and inner.type == "call_expression":
636
+ # const save = useStore((s) => s.save): calling save() calls the store's save
637
+ callee = _unwrap(inner.child_by_field_name("function"))
638
+ args = inner.child_by_field_name("arguments")
639
+ first = _unwrap(args.named_children[0]) if args is not None and args.named_children else None
640
+ if callee is not None and callee.type == "identifier" and first is not None and first.type == "arrow_function":
641
+ body = _unwrap(first.child_by_field_name("body"))
642
+ param = first.child_by_field_name("parameter") or (first.child_by_field_name("parameters").named_children[0]
643
+ if first.child_by_field_name("parameters") is not None
644
+ and first.child_by_field_name("parameters").named_children else None)
645
+ pname = _text(param.child_by_field_name("pattern") or param) if param is not None else ""
646
+ if body is not None and body.type == "member_expression" and _text(body.child_by_field_name("object")) == pname \
647
+ and pname and (_text(callee) in self.imported or _text(callee) in self.stores):
648
+ scope["@" + name] = (_text(callee), _text(body.child_by_field_name("property")))
649
+ if inner is not None and inner.type in ("class", "class_expression"):
650
+ self._class(name, inner, self.file_id if top else cid, qual, exported)
651
+ return
652
+ names = _type_names(ann) if ann is not None else []
653
+ if names:
654
+ scope[name] = names[0]
655
+ self.res.type_refs.append(TypeRef(cid, names, "local", d.start_point[0] + 1))
656
+ elif inner is not None and inner.type == "new_expression":
657
+ ctor = _text(inner.child_by_field_name("constructor")).rsplit(".", 1)[-1]
658
+ if ctor[:1].isupper():
659
+ scope[name] = ctor
660
+ if value is not None:
661
+ if not names and inner is not None and inner.type == "call_expression":
662
+ site = self._call(inner, cid, class_id, scope)
663
+ if site is not None:
664
+ scope["~" + name] = site
665
+ self._walk(value, cid, class_id, scope, qual)
666
+ if name not in scope and ("~" + name) not in scope and ("@" + name) not in scope:
667
+ scope["=" + name] = "local" # a value of unknown type: calling it is not calling a function of that name
668
+
669
+ def _made_object(self, call):
670
+ """The object literal a callback argument hands back: create((set) => ({ ... })) or create()((set) => ({ ... }))."""
671
+ args = call.child_by_field_name("arguments")
672
+ for a in (args.named_children if args is not None and args.type == "arguments" else []):
673
+ fn = _unwrap(a)
674
+ if fn is not None and fn.type in FUNCS:
675
+ body = _unwrap(fn.child_by_field_name("body"))
676
+ if body is not None and body.type == "object":
677
+ pairs = [m for m in body.named_children if m.type == "method_definition"
678
+ or (m.type == "pair" and self._fn_of(m.child_by_field_name("value")) is not None)]
679
+ if len(pairs) >= 2:
680
+ return body
681
+ return None
682
+
683
+ def _require_target(self, call) -> Optional[str]:
684
+ args = call.child_by_field_name("arguments")
685
+ first = args.named_children[0] if args is not None and args.named_children else None
686
+ s = _string(first)
687
+ return self._spec(s) if s else None
688
+
689
+ def _params(self, fn, cid, scope, declare=True):
690
+ """Read a function's parameters into scope. Returns (names, required, total, variadic)."""
691
+ params = fn.child_by_field_name("parameters")
692
+ single = fn.child_by_field_name("parameter")
693
+ names, required, total, variadic = [], 0, 0, False
694
+ items = [single] if single is not None else (params.named_children if params is not None else [])
695
+ for p in items:
696
+ if p.type == "comment":
697
+ continue
698
+ pat, ann, default, optional = p, None, None, False
699
+ if p.type in ("required_parameter", "optional_parameter"):
700
+ pat = p.child_by_field_name("pattern") or (p.named_children[0] if p.named_children else None)
701
+ ann, default = p.child_by_field_name("type"), p.child_by_field_name("value")
702
+ optional = p.type == "optional_parameter"
703
+ elif p.type == "assignment_pattern":
704
+ pat, default = p.child_by_field_name("left"), p.child_by_field_name("right")
705
+ if pat is None or pat.type == "this":
706
+ continue
707
+ if pat.type == "rest_pattern":
708
+ variadic = True
709
+ continue
710
+ total += 1
711
+ if default is None and not optional:
712
+ required += 1
713
+ if pat.type == "identifier":
714
+ pname = _text(pat)
715
+ names.append(pname)
716
+ scope.pop(pname, None)
717
+ scope.pop("~" + pname, None)
718
+ tn = _type_names(ann) if ann is not None else []
719
+ if tn:
720
+ scope[pname] = tn[0]
721
+ if declare:
722
+ self.res.type_refs.append(TypeRef(cid, tn, "param", p.start_point[0] + 1))
723
+ else:
724
+ scope["=" + pname] = "local" # shadows a function of the same name
725
+ elif ann is not None and declare:
726
+ tn = _type_names(ann)
727
+ if tn:
728
+ self.res.type_refs.append(TypeRef(cid, tn, "param", p.start_point[0] + 1))
729
+ return names, required, total, variadic
730
+
731
+ def _function(self, name, fn, outer, parent_id, qual, class_id, outer_scope, exported=False,
732
+ is_method=False, is_static=False, visibility=None, native=None, abstract=False) -> str:
733
+ cid = f"{self.repo}:typescript:{qual}.{name}"
734
+ scope: dict = dict(outer_scope or {}) if not is_method else {}
735
+ pnames, required, total, variadic = self._params(fn, cid, scope)
736
+ body = fn.child_by_field_name("body")
737
+ head_end = body.start_byte if body is not None else fn.end_byte
738
+ ret = fn.child_by_field_name("return_type")
739
+ rnames = _type_names(ret) if ret is not None else []
740
+ if rnames:
741
+ self.res.type_refs.append(TypeRef(cid, rnames, "return", fn.start_point[0] + 1))
742
+ sig = _squash(self.src[outer.start_byte:head_end].decode("utf8", "replace"))[:300].rstrip("{ ").rstrip("=>").rstrip()
743
+ component = self.is_jsx and name[:1].isupper() and not is_method
744
+ node = Node(
745
+ id=cid, kind="callable", name=name, parent_id=parent_id, language=LANGUAGE, path=self.path,
746
+ span_start=outer.start_point[0] + 1, span_end=outer.end_point[0] + 1,
747
+ attrs={"signature": sig,
748
+ "visibility": visibility or ("public" if exported or is_method else "private"),
749
+ "is_static": is_static or not is_method, "is_async": any(c.type == "async" for c in fn.children),
750
+ "is_virtual": is_method and not is_static, "is_abstract": abstract or None,
751
+ "native_kind": native or ("method" if is_method else "component" if component else "function"),
752
+ "argc_min": required, "argc_max": 99 if variadic else total, "type_id": class_id if is_method else None,
753
+ "params": pnames, "returns": rnames[0] if rnames else None,
754
+ "body_line": body.start_point[0] + 1 if body is not None else None})
755
+ fresh = self._add(node)
756
+ if body is not None:
757
+ self._walk(body, cid, class_id, scope, f"{qual}.{name}")
758
+ if fresh and not rnames:
759
+ made = self._returned_new(body)
760
+ if made:
761
+ node.attrs["returns"] = made
762
+ return cid
763
+
764
+ def _returned_new(self, body) -> Optional[str]:
765
+ """With no annotation, `return new Foo()` still says what comes back."""
766
+ expr = _unwrap(body) if body.type != "statement_block" else None
767
+ if expr is not None and expr.type == "new_expression":
768
+ return _text(expr.child_by_field_name("constructor")).rsplit(".", 1)[-1]
769
+ stack = list(body.children) if body.type == "statement_block" else []
770
+ while stack:
771
+ n = stack.pop()
772
+ if n.type in FUNCS or n.type in ("function_declaration", "class_declaration", "method_definition"):
773
+ continue
774
+ if n.type == "return_statement":
775
+ expr = _unwrap(n.named_children[0]) if n.named_children else None
776
+ if expr is not None and expr.type == "new_expression":
777
+ return _text(expr.child_by_field_name("constructor")).rsplit(".", 1)[-1]
778
+ stack.extend(n.children)
779
+ return None
780
+
781
+ def _member_name(self, node) -> str:
782
+ n = node.child_by_field_name("name")
783
+ if n is None:
784
+ return ""
785
+ if n.type in ("string", "template_string"):
786
+ return _string(n) or ""
787
+ return _text(n) if n.type != "computed_property_name" else ""
788
+
789
+ def _class(self, name, node, parent_id, qual, exported) -> None:
790
+ qname = f"{qual}.{name}"
791
+ tid = f"{self.repo}:typescript:{qname}"
792
+ body = node.child_by_field_name("body")
793
+ head_end = body.start_byte if body is not None else node.end_byte
794
+ self._add(Node(
795
+ id=tid, kind="type", name=name, parent_id=parent_id, language=LANGUAGE, path=self.path,
796
+ span_start=node.start_point[0] + 1, span_end=node.end_point[0] + 1,
797
+ attrs={"native_kind": "class", "namespace": qual, "visibility": "public" if exported else "private",
798
+ "is_abstract": node.type == "abstract_class_declaration",
799
+ "signature": _squash(self.src[node.start_byte:head_end].decode("utf8", "replace"))[:300]}))
800
+ for h in node.children:
801
+ if h.type == "class_heritage":
802
+ for clause in h.children:
803
+ if clause.type == "extends_clause":
804
+ value = clause.child_by_field_name("value")
805
+ base = _text(value).split("<")[0].rsplit(".", 1)[-1]
806
+ if base:
807
+ self.res.type_refs.append(TypeRef(tid, [base], "base", clause.start_point[0] + 1))
808
+ elif clause.type == "implements_clause":
809
+ for tn in _type_names(clause):
810
+ self.res.type_refs.append(TypeRef(tid, [tn], "base", clause.start_point[0] + 1))
811
+ self.self_types.setdefault(tid, {})
812
+ if body is None:
813
+ return
814
+ # Overload signatures: the method is its implementation, wherever the signatures sit.
815
+ implemented = {self._member_name(m) for m in body.children if m.type == "method_definition"}
816
+ for m in body.children:
817
+ if m.type == "method_signature" and self._member_name(m) in implemented:
818
+ continue
819
+ mods = {c.type for c in m.children} | {_text(c) for c in m.children if c.type == "accessibility_modifier"}
820
+ vis = "private" if "private" in mods else "protected" if "protected" in mods else "public"
821
+ static = "static" in mods
822
+ if m.type in ("method_definition", "abstract_method_signature", "method_signature"):
823
+ mname = self._member_name(m)
824
+ if not mname:
825
+ self._walk_children(m, None, tid, {}, qname)
826
+ continue
827
+ if mname.startswith("#"):
828
+ vis = "private"
829
+ kind = "getter" if "get" in mods else "setter" if "set" in mods else None
830
+ if mname == CTOR:
831
+ params = m.child_by_field_name("parameters")
832
+ for p in (params.named_children if params is not None else []):
833
+ if any(c.type in ("accessibility_modifier", "readonly") for c in p.children):
834
+ pat, ann = p.child_by_field_name("pattern"), p.child_by_field_name("type")
835
+ if pat is not None and pat.type == "identifier":
836
+ self._field(tid, _text(pat), p, ann, None)
837
+ if kind == "setter" and f"{tid}.{mname}" in self.ids:
838
+ self._walk_children(m, f"{tid}.{mname}", tid, {}, qname)
839
+ continue
840
+ self._function(mname, m, m, tid, qname, tid, None, is_method=True, is_static=static, visibility=vis,
841
+ native=kind, abstract=m.type != "method_definition")
842
+ elif m.type in ("public_field_definition", "field_definition", "property_signature"):
843
+ fname = self._member_name(m)
844
+ if not fname:
845
+ continue
846
+ value = m.child_by_field_name("value")
847
+ fn = self._fn_of(value)
848
+ if fn is not None:
849
+ self._function(fname, fn, m, tid, qname, tid, None, is_method=True, is_static=static, visibility=vis)
850
+ else:
851
+ self._field(tid, fname, m, m.child_by_field_name("type"), value, vis)
852
+ if value is not None:
853
+ self._walk(value, f"{tid}.{CTOR}" if f"{tid}.{CTOR}" in self.ids else None, tid, {}, qname)
854
+
855
+ def _field(self, tid, name, node, ann, value, vis="public", native="property") -> None:
856
+ fid = f"{tid}.{name}"
857
+ names = _type_names(ann) if ann is not None else []
858
+ declared = _squash(_text(ann)).lstrip(": ")[:120] if ann is not None else None
859
+ inner = _unwrap(value)
860
+ tname = names[0] if names else None
861
+ if tname is None and inner is not None and inner.type == "new_expression":
862
+ tname = _text(inner.child_by_field_name("constructor")).rsplit(".", 1)[-1]
863
+ if tname:
864
+ self.self_types.setdefault(tid, {})[name] = tname
865
+ if self._add(Node(
866
+ id=fid, kind="field", name=name, parent_id=tid, language=LANGUAGE, path=self.path,
867
+ span_start=node.start_point[0] + 1, span_end=node.end_point[0] + 1,
868
+ attrs={"native_kind": native, "declared_type": declared, "type_name": tname,
869
+ "visibility": "private" if name.startswith(("#", "_")) else vis, "is_mutable": True})):
870
+ self.res.edges.append(Edge("has_field", tid, fid))
871
+ if names:
872
+ self.res.type_refs.append(TypeRef(tid, names, "field_type", node.start_point[0] + 1))
873
+
874
+ def _typeish(self, node, parent_id, qual, exported) -> None:
875
+ name = _text(node.child_by_field_name("name"))
876
+ if not name:
877
+ return
878
+ if node.type == "type_alias_declaration":
879
+ m = IMPORT_TYPE.search(_text(node.child_by_field_name("value")))
880
+ if m: # type FilesModule = typeof import("../src/files.js")
881
+ self.module_aliases.add(name)
882
+ self.res.imports.append(ImportRef(self.file_id, self._spec(m.group(1)), alias=name))
883
+ return
884
+ qname = f"{qual}.{name}"
885
+ tid = f"{self.repo}:typescript:{qname}"
886
+ native = {"interface_declaration": "interface", "type_alias_declaration": "alias", "enum_declaration": "enum"}[node.type]
887
+ if not self._add(Node(
888
+ id=tid, kind="type", name=name, parent_id=parent_id, language=LANGUAGE, path=self.path,
889
+ span_start=node.start_point[0] + 1, span_end=node.end_point[0] + 1,
890
+ attrs={"native_kind": native, "namespace": qual, "visibility": "public" if exported else "private",
891
+ "is_abstract": native == "interface",
892
+ "signature": _squash(_text(node))[:200]})):
893
+ return # a declaration merged with one of the same name
894
+ if native == "enum":
895
+ body = node.child_by_field_name("body")
896
+ for m in (body.named_children if body is not None else []):
897
+ mname = _text(m.child_by_field_name("name") or m) if m.type in ("enum_assignment", "property_identifier") else ""
898
+ if mname:
899
+ self._field(tid, mname, m, None, None, native="enum_member")
900
+ return
901
+ if native == "interface":
902
+ for c in node.children:
903
+ if c.type == "extends_type_clause":
904
+ for tn in _type_names(c):
905
+ self.res.type_refs.append(TypeRef(tid, [tn], "base", c.start_point[0] + 1))
906
+ bodies = [node.child_by_field_name("body")]
907
+ else:
908
+ # type X = A & { b: string }: A is a base, b is a field.
909
+ value, bodies, parts = node.child_by_field_name("value"), [], []
910
+ stack = [value] if value is not None else []
911
+ while stack:
912
+ n = stack.pop()
913
+ if n.type in ("intersection_type", "parenthesized_type"):
914
+ stack.extend(n.named_children)
915
+ elif n.type == "object_type":
916
+ bodies.append(n)
917
+ else:
918
+ parts.append(n)
919
+ for p in parts:
920
+ if p.type in ("type_identifier", "generic_type", "nested_type_identifier") and len(parts) + len(bodies) > 1:
921
+ tn = _type_names(p)
922
+ if tn:
923
+ self.res.type_refs.append(TypeRef(tid, [tn[0]], "base", p.start_point[0] + 1))
924
+ else:
925
+ tn = _type_names(p)
926
+ if tn:
927
+ self.res.type_refs.append(TypeRef(tid, tn, "field_type", p.start_point[0] + 1))
928
+ for body in bodies:
929
+ for m in (body.named_children if body is not None else []):
930
+ if m.type == "property_signature":
931
+ fname = self._member_name(m)
932
+ if fname:
933
+ self._field(tid, fname, m, m.child_by_field_name("type"), None)
934
+ elif m.type == "method_signature":
935
+ mname = self._member_name(m)
936
+ if mname:
937
+ self._function(mname, m, m, tid, qname, tid, None, is_method=True, abstract=True)
938
+
939
+ def _object_type(self, name, obj, stmt, qual, exported, native="object") -> bool:
940
+ """`const api = { load() {}, save: () => {} }` at the top of a file: a type with those methods."""
941
+ members = []
942
+ for m in obj.named_children:
943
+ if m.type == "method_definition":
944
+ members.append((self._member_name(m), m, m))
945
+ elif m.type == "pair":
946
+ fn = self._fn_of(m.child_by_field_name("value"))
947
+ key = m.child_by_field_name("key")
948
+ kname = (_string(key) if key is not None and key.type == "string" else _text(key)) if key is not None else ""
949
+ if fn is not None and re.fullmatch(r"[A-Za-z_$][\w$]*", kname or ""):
950
+ members.append((kname, fn, m))
951
+ if not members:
952
+ return False
953
+ qname = f"{qual}.{name}"
954
+ tid = f"{self.repo}:typescript:{qname}"
955
+ self._add(Node(
956
+ id=tid, kind="type", name=name, parent_id=self.file_id, language=LANGUAGE, path=self.path,
957
+ span_start=stmt.start_point[0] + 1, span_end=stmt.end_point[0] + 1,
958
+ attrs={"native_kind": native, "namespace": qual, "visibility": "public" if exported else "private",
959
+ "is_abstract": False, "signature": f"const {name} = {{ ... }}"}))
960
+ done = {(m.start_byte, m.end_byte) for _, _, m in members}
961
+ for mname, fn, outer in members:
962
+ if mname:
963
+ self._function(mname, fn, outer, tid, qname, tid, None, is_method=True, is_static=True)
964
+ for m in obj.named_children:
965
+ if (m.start_byte, m.end_byte) not in done:
966
+ self._walk(m, self.top_id, None, {}, qual)
967
+ return True
968
+
969
+ def _commonjs(self, node, qual, scope) -> bool:
970
+ """CommonJS exports: `exports.foo = function () {}`, `module.exports.foo = () => ...` and
971
+ `module.exports = { foo() {}, bar: function () {}, baz }`. Each function is a function of the file, as an
972
+ exported declaration would be, so `require("./x").foo()` and `const { foo } = require("./x")` reach it."""
973
+ left, right = _unwrap(node.child_by_field_name("left")), _unwrap(node.child_by_field_name("right"))
974
+ if left is None or right is None or left.type != "member_expression":
975
+ return False
976
+ obj, prop = _text(left.child_by_field_name("object")), _text(left.child_by_field_name("property"))
977
+ if obj in ("exports", "module.exports") and re.fullmatch(r"[A-Za-z_$][\w$]*", prop):
978
+ fn = self._fn_of(right)
979
+ if fn is None:
980
+ return False
981
+ self.exported.add(prop)
982
+ self._function(prop, fn, node, self.file_id, qual, None, scope, exported=True)
983
+ return True
984
+ if _text(left) != "module.exports" or right.type != "object":
985
+ return False
986
+ for m in right.named_children:
987
+ if m.type == "method_definition" and self._member_name(m):
988
+ self.exported.add(self._member_name(m))
989
+ self._function(self._member_name(m), m, m, self.file_id, qual, None, scope, exported=True)
990
+ continue
991
+ if m.type == "shorthand_property_identifier":
992
+ self.exported.add(_text(m))
993
+ continue
994
+ if m.type == "pair":
995
+ key = m.child_by_field_name("key")
996
+ kname = (_string(key) if key is not None and key.type == "string" else _text(key)) if key is not None else ""
997
+ fn = self._fn_of(m.child_by_field_name("value"))
998
+ if fn is not None and re.fullmatch(r"[A-Za-z_$][\w$]*", kname or ""):
999
+ self.exported.add(kname)
1000
+ self._function(kname, fn, m, self.file_id, qual, None, scope, exported=True)
1001
+ continue
1002
+ self._walk(m, self.top_id, None, scope, qual)
1003
+ return True
1004
+
1005
+ def _namespace(self, node, parent_id, qual, exported) -> None:
1006
+ """`namespace Geo { export function area() {} export class Shape {} }`: a type whose functions are its
1007
+ static members (Geo.area()), with its classes, interfaces and nested namespaces declared under it."""
1008
+ name = _text(node.child_by_field_name("name"))
1009
+ qname = f"{qual}.{name}"
1010
+ tid = f"{self.repo}:typescript:{qname}"
1011
+ body = node.child_by_field_name("body")
1012
+ self._add(Node(
1013
+ id=tid, kind="type", name=name, parent_id=parent_id, language=LANGUAGE, path=self.path,
1014
+ span_start=node.start_point[0] + 1, span_end=node.end_point[0] + 1,
1015
+ attrs={"native_kind": "namespace", "namespace": qual, "visibility": "public" if exported else "private",
1016
+ "is_abstract": False, "signature": f"namespace {name}"}))
1017
+ self.self_types.setdefault(tid, {})
1018
+ # A bare call in the namespace to one of its own functions (scale(2)) is a call on the namespace.
1019
+ self.ns_members[tid] = {_text(c.child_by_field_name("name")) for c in _walk_nodes(body, ("function_declaration",
1020
+ "generator_function_declaration", "variable_declarator"), stop=FUNCS + ("class_body",))
1021
+ if c.type != "variable_declarator" or self._fn_of(c.child_by_field_name("value")) is not None}
1022
+ for st in (body.named_children if body is not None else []):
1023
+ target, exp = st, False
1024
+ if st.type == "export_statement" and st.child_by_field_name("declaration") is not None:
1025
+ target, exp = st.child_by_field_name("declaration"), True
1026
+ if target.type == "expression_statement" and target.named_children \
1027
+ and target.named_children[0].type in ("internal_module", "module"):
1028
+ target = target.named_children[0]
1029
+ t = target.type
1030
+ if t in ("function_declaration", "generator_function_declaration"):
1031
+ self._function(_text(target.child_by_field_name("name")), target, st, tid, qname, tid, None,
1032
+ is_method=True, is_static=True, visibility="public" if exp else "private")
1033
+ elif t in ("class_declaration", "abstract_class_declaration") and target.child_by_field_name("name") is not None:
1034
+ self._class(_text(target.child_by_field_name("name")), target, tid, qname, exp)
1035
+ elif t in ("interface_declaration", "type_alias_declaration", "enum_declaration"):
1036
+ self._typeish(target, tid, qname, exp)
1037
+ elif t in ("internal_module", "module") and target.child_by_field_name("name") is not None \
1038
+ and target.child_by_field_name("name").type in ("identifier", "nested_identifier"):
1039
+ self._namespace(target, tid, qname, exp)
1040
+ elif t in ("lexical_declaration", "variable_declaration") and any(
1041
+ d.type == "variable_declarator" and self._fn_of(d.child_by_field_name("value")) is not None
1042
+ for d in target.named_children):
1043
+ for d in target.named_children:
1044
+ fn = self._fn_of(d.child_by_field_name("value")) if d.type == "variable_declarator" else None
1045
+ if fn is not None and d.child_by_field_name("name").type == "identifier":
1046
+ self._function(_text(d.child_by_field_name("name")), fn, st, tid, qname, tid, None,
1047
+ is_method=True, is_static=True, visibility="public" if exp else "private")
1048
+ elif d.type == "variable_declarator":
1049
+ self._walk(d, self.top_id, None, {}, qual)
1050
+ else:
1051
+ self._walk(st, self.top_id, None, {}, qual) # code the namespace runs when the file loads
1052
+
1053
+ def _object_members(self, obj, cid, class_id, scope, qual) -> None:
1054
+ """Function-valued properties of an object written inside a function are that function's local functions."""
1055
+ for m in obj.children:
1056
+ if m.type == "method_definition":
1057
+ mname = self._member_name(m)
1058
+ if (m.start_byte, m.end_byte) in self.redirect: # a route's handler: { handler(req) { ... } }
1059
+ self._callback(m, cid, class_id, scope, qual)
1060
+ continue
1061
+ if mname:
1062
+ self._function(mname, m, m, cid, qual, class_id, scope)
1063
+ continue
1064
+ elif m.type == "pair":
1065
+ key = m.child_by_field_name("key")
1066
+ fn = self._fn_of(m.child_by_field_name("value"))
1067
+ kname = _text(key) if key is not None and key.type == "property_identifier" else ""
1068
+ if fn is not None and kname and fn.end_point[0] - fn.start_point[0] >= 2 \
1069
+ and (fn.start_byte, fn.end_byte) not in self.redirect:
1070
+ self._function(kname, fn, m, cid, qual, class_id, scope)
1071
+ continue
1072
+ self._walk(m, cid, class_id, scope, qual)
1073
+
1074
+ # -- expressions -----------------------------------------------------------
1075
+ def _assign(self, node, cid, class_id, scope) -> None:
1076
+ left, right = _unwrap(node.child_by_field_name("left")), _unwrap(node.child_by_field_name("right"))
1077
+ if left is None:
1078
+ return
1079
+ dyn = _dynamic_import(node.child_by_field_name("right"))
1080
+ if dyn and left.type in ("identifier", "object_pattern", "object"):
1081
+ target = self._spec(dyn)
1082
+ if left.type == "identifier":
1083
+ self.res.imports.append(ImportRef(self.file_id, target, alias=_text(left)))
1084
+ else:
1085
+ names = _pattern_names(left) or [(_text(c), _text(c)) for c in left.named_children
1086
+ if c.type in ("shorthand_property_identifier", "shorthand_property_identifier_pattern")]
1087
+ self.res.imports.append(ImportRef(self.file_id, target, symbols=[a if a == b else f"{a} as {b}" for a, b in names]))
1088
+ if left.type == "identifier" and right is not None and right.type == "new_expression":
1089
+ ctor = _text(right.child_by_field_name("constructor")).rsplit(".", 1)[-1]
1090
+ if ctor[:1].isupper():
1091
+ scope[_text(left)] = ctor
1092
+ if left.type == "member_expression" and class_id is not None and _text(left.child_by_field_name("object")) == "this":
1093
+ prop = left.child_by_field_name("property")
1094
+ if prop is not None and prop.type in ("property_identifier", "private_property_identifier") \
1095
+ and self.by_id.get(class_id) is not None and self.by_id[class_id].attrs.get("native_kind") == "class" \
1096
+ and f"{class_id}.{_text(prop)}" not in self.ids:
1097
+ self._field(class_id, _text(prop), node, None, right) # a JavaScript class declares fields by assigning them
1098
+
1099
+ def _receiver(self, obj, cid, class_id, scope):
1100
+ """(receiver, receiver type, chain) for the object a member is taken from."""
1101
+ obj = _unwrap(obj)
1102
+ if obj is None:
1103
+ return "?", None, None
1104
+ t = obj.type
1105
+ if t == "this":
1106
+ return "this", None, None
1107
+ if t == "super":
1108
+ return "base", None, None
1109
+ if t == "identifier":
1110
+ text = _text(obj)
1111
+ known = scope.get(text)
1112
+ if known in self.module_aliases:
1113
+ return known, None, None # a variable typed as a module: calls on it are calls into that module
1114
+ return text, known, None if known else scope.get("~" + text)
1115
+ if t == "call_expression":
1116
+ inner = _unwrap(obj.child_by_field_name("function"))
1117
+ if inner is not None and inner.type == "member_expression" and _text(inner.child_by_field_name("property")) == "getState" \
1118
+ and inner.child_by_field_name("object").type == "identifier":
1119
+ return _text(inner.child_by_field_name("object")), None, None # useStore.getState().save()
1120
+ if t == "member_expression" and _text(obj.child_by_field_name("object")) == "this":
1121
+ inner = _text(obj.child_by_field_name("property"))
1122
+ return "." + inner, self.self_types.get(class_id or "", {}).get(inner), None
1123
+ if t == "call_expression":
1124
+ return "?", None, self._call(obj, cid, class_id, scope)
1125
+ if t == "new_expression":
1126
+ return "?", _text(obj.child_by_field_name("constructor")).rsplit(".", 1)[-1], None
1127
+ return "?", None, None
1128
+
1129
+ def _call(self, node, cid, class_id, scope) -> Optional[CallSite]:
1130
+ """Record one call. A call used as the receiver of another is recorded once and shared."""
1131
+ key = (node.start_byte, node.end_byte)
1132
+ if key in self.done_calls:
1133
+ return self.done_calls[key]
1134
+ self.done_calls[key] = None
1135
+ fn = _unwrap(node.child_by_field_name("function"))
1136
+ args = node.child_by_field_name("arguments")
1137
+ if fn is None or cid is None:
1138
+ return None
1139
+ arg_nodes = [a for a in args.named_children if a.type != "comment"] if args is not None and args.type == "arguments" else []
1140
+ argc = len(arg_nodes)
1141
+ line, col = node.start_point[0] + 1, node.start_point[1]
1142
+ hints = tuple(_arity(a) for a in arg_nodes)
1143
+ site = None
1144
+ if fn.type == "identifier":
1145
+ name = _text(fn)
1146
+ if "@" + name in scope:
1147
+ owner, member = scope["@" + name]
1148
+ site = CallSite(cid, member, owner, None, argc, line, class_id, col, hints)
1149
+ elif name in scope or "=" + name in scope or "~" + name in scope:
1150
+ self.done_calls[key] = None
1151
+ return None # a local value being called: a parameter, a callback, the result of another call
1152
+ else:
1153
+ site = CallSite(cid, name, "this" if name in self.ns_members.get(class_id, ()) else None, None, argc,
1154
+ line, class_id, col, hints)
1155
+ elif fn.type == "super":
1156
+ site = CallSite(cid, CTOR, "base", None, argc, line, class_id, col, hints)
1157
+ elif fn.type == "member_expression":
1158
+ name = _text(fn.child_by_field_name("property"))
1159
+ receiver, rtype, chain = self._receiver(fn.child_by_field_name("object"), cid, class_id, scope)
1160
+ if name:
1161
+ site = CallSite(cid, name, receiver, rtype, argc, line, class_id, col, hints, chain=chain)
1162
+ if site is not None:
1163
+ self.res.calls.append(site)
1164
+ if cid == self.top_id:
1165
+ self.top_used = True
1166
+ self._endpoint(node, fn, site, arg_nodes, cid, line)
1167
+ self.done_calls[key] = site
1168
+ return site
1169
+
1170
+ def _call_node(self, node, cid, class_id, scope, qual, top) -> None:
1171
+ fn = _unwrap(node.child_by_field_name("function"))
1172
+ args = node.child_by_field_name("arguments")
1173
+ # Not the callee's whole text: in a builder chain a.b().c().d()... that is the chain so far, read again at
1174
+ # every link, which made a long chain quadratic. Only the last name is needed, and the whole text only for
1175
+ # a call that could be a test (a title and a function among its arguments).
1176
+ if fn is None:
1177
+ last = ""
1178
+ elif fn.type == "member_expression":
1179
+ last = _text(fn.child_by_field_name("property"))
1180
+ elif fn.type in ("identifier", "import"):
1181
+ last = _text(fn)
1182
+ else:
1183
+ last = _text(fn).rsplit(".", 1)[-1]
1184
+ arg_nodes = [a for a in args.named_children if a.type != "comment"] if args is not None and args.type == "arguments" else []
1185
+ if fn is not None and fn.type == "import":
1186
+ s = _string(arg_nodes[0]) if arg_nodes else None
1187
+ if s:
1188
+ self.res.imports.append(ImportRef(self.file_id, self._spec(s)))
1189
+ return
1190
+ if fn is not None and fn.type == "identifier" and last == "require" and arg_nodes and _string(arg_nodes[0]):
1191
+ self.res.imports.append(ImportRef(self.file_id, self._spec(_string(arg_nodes[0]))))
1192
+ return
1193
+ title = _string(arg_nodes[0]) if arg_nodes else None
1194
+ body = next((_unwrap(a) for a in arg_nodes[1:] if _unwrap(a) is not None and _unwrap(a).type in FUNCS), None) \
1195
+ if title is not None else None
1196
+ head, mods = "", set()
1197
+ if body is not None:
1198
+ # describe("x", () => { it("does y", () => { ... }) })
1199
+ ftext = _text(fn)
1200
+ head = ftext.split(".")[0].split("(")[0]
1201
+ runner = fn
1202
+ while runner is not None and runner.type in ("call_expression", "member_expression"): # it.each(rows)("...", fn)
1203
+ runner = runner.child_by_field_name("function") if runner.type == "call_expression" else runner.child_by_field_name("object")
1204
+ head = _text(runner) if runner is not None and runner.type == "identifier" else head
1205
+ mods = set(re.findall(r"\.(\w+)", ftext.split("(")[0]))
1206
+ if head in SUITE_FNS and mods <= TEST_MODS and title is not None and body is not None:
1207
+ self.suites.append(title)
1208
+ self._callback(body, cid, class_id, scope, qual)
1209
+ self.suites.pop()
1210
+ return
1211
+ if head in TEST_FNS and mods <= TEST_MODS and title is not None and body is not None and (self.is_test_file or cid == self.top_id):
1212
+ full = " > ".join(self.suites + [title])
1213
+ slug = re.sub(r"[^A-Za-z0-9]+", "-", full).strip("-").lower()[:120]
1214
+ tid = f"{self.top_id}/test:{slug}"
1215
+ n = 2
1216
+ while tid in self.ids:
1217
+ tid = f"{self.top_id}/test:{slug}-{n}"
1218
+ n += 1
1219
+ self.top_used = True
1220
+ self._add(Node(
1221
+ id=tid, kind="test", name=title, parent_id=self.top_id, language=LANGUAGE, path=self.path,
1222
+ span_start=node.start_point[0] + 1, span_end=node.end_point[0] + 1,
1223
+ attrs={"framework": "vitest", "runner": head, "suite": " > ".join(self.suites) or None, "full_name": full}))
1224
+ self.redirect[(body.start_byte, body.end_byte)] = (tid, f"{self.mod}.<module>/test:{tid.rsplit('/test:', 1)[1]}")
1225
+ self._callback(body, cid, class_id, scope, qual)
1226
+ return
1227
+ site = self._call(node, cid, class_id, scope)
1228
+ if top and cid == self.top_id and site is not None and not self.is_test_file:
1229
+ if (site.receiver is None and site.name in MAIN_NAMES and site.name in self.fn_names) or site.name in ("listen",) \
1230
+ or (site.name == "render" and "createRoot" in _text(node)):
1231
+ self.entry = True
1232
+ if site is not None and fn is not None and last in LAUNCHERS \
1233
+ and (site.receiver is None or re.search(r"child_process|^cp$|^childProcess$", site.receiver or "")):
1234
+ found = list(_strings(args)) if args is not None else []
1235
+ for a in arg_nodes:
1236
+ if a.type == "identifier" and _text(a) in self.consts:
1237
+ found.extend(self.consts[_text(a)])
1238
+ if self.all_strings is None:
1239
+ self.all_strings = _strings(self.tree.root_node)
1240
+ # A program named by a literal ("git") is that program; only a computed one is looked for among the file's strings.
1241
+ named = bool(arg_nodes) and _unwrap(arg_nodes[0]) is not None and _unwrap(arg_nodes[0]).type == "string"
1242
+ # A program a function of this file computes (spawn(node, [hostEntry(), ...])) is the path that function
1243
+ # builds, or nothing known: the file's other strings name other things.
1244
+ helped = self._helper_paths(node, arg_nodes)
1245
+ if helped is not None:
1246
+ found, named = helped + found, True
1247
+ self.res.spawns.append(Spawn(cid, found, [] if named else self.all_strings,
1248
+ "pipe" in _text(args) or "stdio" in _text(args), node.start_point[0] + 1))
1249
+ for a in arg_nodes:
1250
+ self._reference(a, cid, class_id, scope)
1251
+ # The callee's own sub-expressions (a.b().c(): the inner call; x.y.z(): reads of x.y), then the arguments.
1252
+ if fn is not None and fn.type == "member_expression":
1253
+ obj = fn.child_by_field_name("object")
1254
+ if obj is not None:
1255
+ self._walk(obj, cid, class_id, scope, qual)
1256
+ elif fn is not None and fn.type not in ("identifier", "super"):
1257
+ self._walk(fn, cid, class_id, scope, qual)
1258
+ if args is not None:
1259
+ self._walk_children(args, cid, class_id, scope, qual)
1260
+
1261
+ def _reference(self, node, cid, class_id, scope) -> None:
1262
+ """A function handed over by name: `onClick={save}`, `items.map(render)`, `{ onSave: save }`."""
1263
+ if cid is None:
1264
+ return
1265
+ t = node.type
1266
+ target = node
1267
+ if t == "jsx_expression":
1268
+ target = node.named_children[0] if node.named_children else None
1269
+ elif t == "pair":
1270
+ target = node.child_by_field_name("value")
1271
+ target = _unwrap(target)
1272
+ if target is None:
1273
+ return
1274
+ line, col = target.start_point[0] + 1, target.start_point[1]
1275
+ if target.type in ("identifier", "shorthand_property_identifier"):
1276
+ name = _text(target)
1277
+ if (name in self.fn_names or name in self.imported) and name not in scope and ("=" + name) not in scope \
1278
+ and ("~" + name) not in scope:
1279
+ self.res.calls.append(CallSite(cid, name, None, None, -1, line, class_id, col, ref=True))
1280
+ if cid == self.top_id:
1281
+ self.top_used = True
1282
+ elif target.type == "member_expression" and _text(target.child_by_field_name("object")) == "this" and class_id is not None:
1283
+ name = _text(target.child_by_field_name("property"))
1284
+ if name:
1285
+ self.res.calls.append(CallSite(cid, name, "this", None, -1, line, class_id, col, ref=True))
1286
+
1287
+ def _new(self, node, cid, class_id, scope) -> None:
1288
+ if cid is None:
1289
+ return
1290
+ key = (node.start_byte, node.end_byte)
1291
+ if key in self.done_calls:
1292
+ return
1293
+ self.done_calls[key] = None
1294
+ ctor = _unwrap(node.child_by_field_name("constructor"))
1295
+ args = node.child_by_field_name("arguments")
1296
+ argc = len([a for a in args.named_children if a.type != "comment"]) if args is not None else 0
1297
+ line, col = node.start_point[0] + 1, node.start_point[1]
1298
+ if ctor is None:
1299
+ return
1300
+ if ctor.type == "identifier":
1301
+ self.res.calls.append(CallSite(cid, _text(ctor), None, None, argc, line, class_id, col))
1302
+ elif ctor.type == "member_expression" and ctor.child_by_field_name("object").type == "identifier":
1303
+ self.res.calls.append(CallSite(cid, _text(ctor.child_by_field_name("property")),
1304
+ _text(ctor.child_by_field_name("object")), None, argc, line, class_id, col))
1305
+ if cid == self.top_id:
1306
+ self.top_used = True
1307
+
1308
+ def _jsx(self, node, cid, class_id) -> None:
1309
+ name = node.child_by_field_name("name")
1310
+ if name is None or cid is None:
1311
+ return
1312
+ text = _text(name)
1313
+ line, col = node.start_point[0] + 1, node.start_point[1]
1314
+ if name.type == "identifier" and text[:1].isupper():
1315
+ self.res.calls.append(CallSite(cid, text, None, None, 1, line, class_id, col))
1316
+ elif name.type in ("member_expression", "nested_identifier") and "." in text:
1317
+ head, _, last = text.rpartition(".")
1318
+ if "." not in head and last[:1].isupper():
1319
+ self.res.calls.append(CallSite(cid, last, head, None, 1, line, class_id, col))
1320
+ if cid == self.top_id:
1321
+ self.top_used = True
1322
+
1323
+ def _use(self, node, cid, class_id, scope) -> None:
1324
+ """Record obj.prop as a read or an assignment of a possible field."""
1325
+ parent = node.parent
1326
+ if cid is None or parent is None:
1327
+ return
1328
+ if parent.type == "call_expression" and _same(parent.child_by_field_name("function"), node):
1329
+ return
1330
+ prop = node.child_by_field_name("property")
1331
+ if prop is None or prop.type not in ("property_identifier", "private_property_identifier"):
1332
+ return
1333
+ receiver, rtype, chain = self._receiver(node.child_by_field_name("object"), cid, class_id, scope)
1334
+ if receiver == "?" and chain is None and rtype is None:
1335
+ return
1336
+ if receiver not in ("this", "base", "?") and not receiver.startswith(".") and rtype is None and chain is None:
1337
+ return # an untyped local: nothing says what it is
1338
+ cur, up = node, parent
1339
+ # Through parentheses and casts, and out of a destructuring target: [this.a, this.b] = [b, a] assigns both.
1340
+ while up is not None and (up.type in ("parenthesized_expression", "non_null_expression", "as_expression",
1341
+ "array_pattern", "object_pattern")
1342
+ or up.type == "pair_pattern" and _same(up.child_by_field_name("value"), cur)
1343
+ or up.type == "assignment_pattern" and _same(up.child_by_field_name("left"), cur)):
1344
+ cur, up = up, up.parent
1345
+ access = "r"
1346
+ if up is not None:
1347
+ if up.type == "assignment_expression" and _same(up.child_by_field_name("left"), cur):
1348
+ access = "w"
1349
+ elif up.type == "augmented_assignment_expression" and _same(up.child_by_field_name("left"), cur):
1350
+ access = "rw"
1351
+ elif up.type == "update_expression":
1352
+ access = "rw"
1353
+ elif up.type == "subscript_expression" and _same(up.child_by_field_name("object"), cur):
1354
+ outer = up.parent
1355
+ if outer is not None and outer.type in ("assignment_expression", "augmented_assignment_expression") \
1356
+ and _same(outer.child_by_field_name("left"), up):
1357
+ access = "rw"
1358
+ if access == "r" and up is not None: # this.items.push(x), this.byId[k].add(x)
1359
+ c2, u2 = cur, up
1360
+ if u2.type == "subscript_expression" and _same(u2.child_by_field_name("object"), c2):
1361
+ c2, u2 = u2, u2.parent
1362
+ if u2 is not None and u2.type == "member_expression" and _same(u2.child_by_field_name("object"), c2) \
1363
+ and u2.parent is not None and u2.parent.type == "call_expression" \
1364
+ and _same(u2.parent.child_by_field_name("function"), u2) \
1365
+ and _text(u2.child_by_field_name("property")) in MUTATORS:
1366
+ access = "rw"
1367
+ self.res.field_uses.append(FieldUse(cid, _text(prop), receiver, rtype, access, node.start_point[0] + 1, class_id, chain))
1368
+
1369
+ def _endpoint(self, node, fn, site: CallSite, arg_nodes, cid, line) -> None:
1370
+ """Is this call one end of an HTTP or file channel?"""
1371
+ name = site.name
1372
+ first = _unwrap(arg_nodes[0]) if arg_nodes else None
1373
+ addr = _string(first)
1374
+ if site.receiver is None and name == "fetch" and addr and addr.startswith(("/", "http://", "https://", "{}/")):
1375
+ m = re.search(r"method\s*:\s*['\"`](\w+)['\"`]", _text(arg_nodes[1])) if len(arg_nodes) > 1 else None
1376
+ self.res.endpoints.append(Endpoint("http", "call", cid, addr.replace("{}/", "/", 1) if addr.startswith("{}/") else addr,
1377
+ line, m.group(1).upper() if m else None))
1378
+ return
1379
+ if fn.type == "member_expression" and name in HTTP_VERBS | {"all"} and addr and addr.startswith("/"):
1380
+ recv = _text(fn.child_by_field_name("object"))
1381
+ # On a name that is a client's as well as a server's (api, instance), a name passed after the path is a
1382
+ # handler only when it names a function of the file (declared, or made by a call handed one), or an import:
1383
+ # api.post("/api/login", credentials) sends credentials.
1384
+ named = (lambda a: _text(a) in self.fn_names | self.made_fns | self.imported) if EITHER.search(recv) \
1385
+ or CLIENTS.search(recv) else (lambda a: True)
1386
+ handler = any(_unwrap(a) is not None and (_unwrap(a).type in FUNCS or _unwrap(a).type == "identifier" and named(_unwrap(a)))
1387
+ for a in arg_nodes[1:])
1388
+ method = None if name == "all" else name.upper()
1389
+ last = _unwrap(arg_nodes[-1]) if len(arg_nodes) > 1 else None
1390
+ if SERVERS.search(recv) and (handler or (last is not None and last.type == "object" and self._handler_member(last))):
1391
+ src, written = self._route_handler(last, method, addr, cid, line)
1392
+ self.res.endpoints.append(Endpoint("http", "serve", src, addr, line, method, handler=written))
1393
+ elif CLIENTS.search(recv) or EITHER.search(recv):
1394
+ self.res.endpoints.append(Endpoint("http", "call", cid, addr, line, method))
1395
+ return
1396
+ if fn.type == "member_expression" and name == "route" and first is not None and first.type == "object" \
1397
+ and SERVERS.search(_text(fn.child_by_field_name("object"))) and self._handler_member(first) is not None:
1398
+ # server.route({ method: "GET", url: "/api/x", handler: async (req) => { ... } })
1399
+ url, methods = None, []
1400
+ for m in first.named_children:
1401
+ key = m.child_by_field_name("key") if m.type == "pair" else None
1402
+ kname = (_string(key) if key is not None and key.type == "string" else _text(key)) if key is not None else ""
1403
+ value = _unwrap(m.child_by_field_name("value")) if m.type == "pair" else None
1404
+ if kname in ("url", "path"):
1405
+ url = _string(value)
1406
+ elif kname == "method" and value is not None:
1407
+ methods = [s.upper() for s in ([_string(value)] if value.type in ("string", "template_string") else _strings(value))
1408
+ if s]
1409
+ if url and url.startswith("/"):
1410
+ method = methods[0] if len(methods) == 1 else None
1411
+ src, written = self._route_handler(first, method, url, cid, line, label=", ".join(methods) or None)
1412
+ self.res.endpoints.append(Endpoint("http", "serve", src, url, line, method, handler=written))
1413
+ return
1414
+ role = "read" if name in FS_READ else "write" if name in FS_WRITE else None
1415
+ if role:
1416
+ lits = [s for a in arg_nodes for s in _strings(a)]
1417
+ for a in arg_nodes:
1418
+ if a.type == "identifier" and _text(a) in self.consts:
1419
+ lits.extend(self.consts[_text(a)])
1420
+ self.res.endpoints.append(Endpoint("file", role, cid, "", line, None, lits))
1421
+ if name in ("join", "resolve") and fn.type == "member_expression" and _text(fn.child_by_field_name("object")) in ("path", "posix", "nodePath"):
1422
+ self.res.path_strings.setdefault(cid, []).extend(s for a in arg_nodes for s in _strings(a))
1423
+ return
1424
+ if role:
1425
+ return
1426
+ # A request through a wrapper (apiFetch("/api/x"), api.get(...) on a client the name does not give away)
1427
+ # or a test's server.inject({ method, url }): a path that a route of this program serves, linked only when
1428
+ # one route fits it (see Indexer._resolve_endpoints).
1429
+ url, method = None, None
1430
+ if addr and addr.startswith("/") and addr.count("/") >= 2 and not any(ch.isspace() for ch in addr):
1431
+ url = addr
1432
+ else:
1433
+ for a in arg_nodes[:2]:
1434
+ a = _unwrap(a)
1435
+ if a is not None and a.type == "object":
1436
+ text = _text(a)
1437
+ m = re.search(r"\burl\s*:\s*['\"`](/[^'\"`\s]*)['\"`]", text)
1438
+ if m:
1439
+ url = m.group(1)
1440
+ mm = re.search(r"\bmethod\s*:\s*['\"`](\w+)['\"`]", text)
1441
+ method = mm.group(1).upper() if mm else None
1442
+ break
1443
+ if url:
1444
+ if method is None and len(arg_nodes) > 1:
1445
+ mm = re.search(r"\bmethod\s*:\s*['\"`](\w+)['\"`]", _text(arg_nodes[1]))
1446
+ method = mm.group(1).upper() if mm else None
1447
+ self.res.endpoints.append(Endpoint("http", "maybe", cid, url.split("?", 1)[0], line, method))
1448
+
1449
+ # -- route handlers ----------------------------------------------------------
1450
+ def _handler_member(self, obj):
1451
+ """The `handler` of a route's options object: { handler: async (req) => ... }, { handler(req) { ... } },
1452
+ { handler: listThings } or { handler }. None when it has none."""
1453
+ for m in obj.named_children:
1454
+ if m.type == "method_definition" and self._member_name(m) == "handler":
1455
+ return m
1456
+ if m.type == "shorthand_property_identifier" and _text(m) == "handler":
1457
+ return m
1458
+ if m.type == "pair":
1459
+ key = m.child_by_field_name("key")
1460
+ if key is None or (_string(key) if key.type == "string" else _text(key)) != "handler":
1461
+ continue
1462
+ value = m.child_by_field_name("value")
1463
+ fn = self._fn_of(value)
1464
+ value = fn if fn is not None else _unwrap(value)
1465
+ if value is not None and value.type in FUNCS + ("identifier", "member_expression"):
1466
+ return value
1467
+ return None
1468
+
1469
+ def _route_handler(self, last, method, addr, cid, line, label=None):
1470
+ """What answers a route: (the id of the function that serves it, the handler's name as written when it is
1471
+ given by name). An inline handler becomes its own function, named for its route and nested in the function
1472
+ that registers it, so what it calls is its own and not the registrar's."""
1473
+ target = last
1474
+ if target is not None and target.type == "object":
1475
+ target = self._handler_member(target)
1476
+ if target is None or cid is None:
1477
+ return cid, None
1478
+ if target.type in FUNCS or target.type == "method_definition":
1479
+ route = f"{label or method or 'ALL'} {addr}"
1480
+ base = f"{cid}/route:{route}"
1481
+ hid, n = base, 2
1482
+ while hid in self.ids:
1483
+ hid = f"{base}-{n}"
1484
+ n += 1
1485
+ pnames, required, total, variadic = self._params(target, hid, {}, declare=False)
1486
+ body = target.child_by_field_name("body")
1487
+ head_end = body.start_byte if body is not None else target.end_byte
1488
+ head = _squash(self.src[target.start_byte:head_end].decode("utf8", "replace"))[:200].rstrip("{ ").rstrip("=>").rstrip()
1489
+ self._add(Node(
1490
+ id=hid, kind="callable", name=route, parent_id=cid, language=LANGUAGE, path=self.path,
1491
+ span_start=target.start_point[0] + 1, span_end=target.end_point[0] + 1,
1492
+ attrs={"signature": f"{route}: {head}", "visibility": "private", "is_static": True,
1493
+ "is_async": any(c.type == "async" for c in target.children), "is_virtual": False,
1494
+ "native_kind": "route_handler", "route": route, "registered_at": line,
1495
+ "argc_min": required, "argc_max": 99 if variadic else total, "type_id": None,
1496
+ "params": pnames, "returns": None,
1497
+ "body_line": body.start_point[0] + 1 if body is not None else None}))
1498
+ self.redirect[(target.start_byte, target.end_byte)] = (hid, hid.split(":typescript:", 1)[1])
1499
+ return hid, None
1500
+ if target.type in ("identifier", "shorthand_property_identifier", "member_expression"):
1501
+ return cid, _text(target)
1502
+ return cid, None
1503
+
1504
+
1505
+ def parse(repo: str, rel_path: str, file_id: str, src: bytes, module: str = "", keep_ext: bool = False) -> FileResult:
1506
+ w = _Walker(repo, rel_path, file_id, src)
1507
+ if keep_ext: # see module_path
1508
+ w.mod = module_path(rel_path, True)
1509
+ w.top_id = f"{repo}:typescript:{w.mod}.<module>"
1510
+ res = w.run()
1511
+ channels.extract(LANGUAGE, w.tree, res, file_id, w.consts)
1512
+ return res