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,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
|