codemble 0.3.0__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.
- codemble/__init__.py +4 -0
- codemble/adapters/__init__.py +38 -0
- codemble/adapters/base.py +199 -0
- codemble/adapters/discovery.py +190 -0
- codemble/adapters/project.py +206 -0
- codemble/adapters/python_ast.py +766 -0
- codemble/adapters/typescript_tree_sitter.py +1263 -0
- codemble/checks/__init__.py +19 -0
- codemble/checks/service.py +380 -0
- codemble/cli.py +161 -0
- codemble/graph/__init__.py +17 -0
- codemble/graph/finalize.py +91 -0
- codemble/graph/layout.py +132 -0
- codemble/lens/__init__.py +18 -0
- codemble/lens/javascript_typescript.py +82 -0
- codemble/lens/python.py +71 -0
- codemble/llm/__init__.py +6 -0
- codemble/llm/providers.py +137 -0
- codemble/llm/study.py +439 -0
- codemble/progress/__init__.py +9 -0
- codemble/progress/store.py +160 -0
- codemble/server/__init__.py +6 -0
- codemble/server/app.py +273 -0
- codemble/server/runtime.py +68 -0
- codemble/web_dist/assets/index-DOBVd_-M.css +1 -0
- codemble/web_dist/assets/index-cIG6GGIB.js +5238 -0
- codemble/web_dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- codemble/web_dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- codemble/web_dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- codemble/web_dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- codemble/web_dist/assets/shippori-mincho-latin-500-normal-C-QwvIb3.woff +0 -0
- codemble/web_dist/assets/shippori-mincho-latin-500-normal-XI1O8euf.woff2 +0 -0
- codemble/web_dist/assets/shippori-mincho-latin-700-normal-CkoCYOiI.woff +0 -0
- codemble/web_dist/assets/shippori-mincho-latin-700-normal-DHcmzUO5.woff2 +0 -0
- codemble/web_dist/assets/zen-kaku-gothic-new-latin-400-normal-BEdayliK.woff2 +0 -0
- codemble/web_dist/assets/zen-kaku-gothic-new-latin-400-normal-CPSmNJAU.woff +0 -0
- codemble/web_dist/index.html +18 -0
- codemble-0.3.0.dist-info/METADATA +417 -0
- codemble-0.3.0.dist-info/RECORD +43 -0
- codemble-0.3.0.dist-info/WHEEL +4 -0
- codemble-0.3.0.dist-info/entry_points.txt +2 -0
- codemble-0.3.0.dist-info/licenses/LICENSE +202 -0
- codemble-0.3.0.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,1263 @@
|
|
|
1
|
+
"""Tree-sitter JavaScript/TypeScript implementation of the language seam."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from dataclasses import dataclass, replace
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import AbstractSet, Iterable
|
|
10
|
+
|
|
11
|
+
import tree_sitter_javascript
|
|
12
|
+
import tree_sitter_typescript
|
|
13
|
+
from tree_sitter import Language, Node as SyntaxNode, Parser, Tree
|
|
14
|
+
|
|
15
|
+
from codemble.adapters.base import (
|
|
16
|
+
AdapterParseError,
|
|
17
|
+
ConceptAnnotation,
|
|
18
|
+
Edge,
|
|
19
|
+
Graph,
|
|
20
|
+
Node,
|
|
21
|
+
)
|
|
22
|
+
from codemble.adapters.discovery import SourceDiscoveryError, discover_source_files
|
|
23
|
+
from codemble.graph.finalize import GraphFinalizationError, finalize_graph
|
|
24
|
+
|
|
25
|
+
_JAVASCRIPT_EXTENSIONS = frozenset({".js", ".jsx", ".mjs", ".cjs"})
|
|
26
|
+
_TYPESCRIPT_EXTENSIONS = frozenset({".ts", ".tsx", ".mts", ".cts"})
|
|
27
|
+
_ALL_EXTENSIONS = _JAVASCRIPT_EXTENSIONS | _TYPESCRIPT_EXTENSIONS
|
|
28
|
+
_GENERATED_DIRECTORIES = frozenset(
|
|
29
|
+
{
|
|
30
|
+
".next",
|
|
31
|
+
".nuxt",
|
|
32
|
+
".output",
|
|
33
|
+
"build",
|
|
34
|
+
"coverage",
|
|
35
|
+
"dist",
|
|
36
|
+
"out",
|
|
37
|
+
"storybook-static",
|
|
38
|
+
"web_dist",
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
_DEFINITION_TYPES = frozenset(
|
|
42
|
+
{
|
|
43
|
+
"class",
|
|
44
|
+
"class_declaration",
|
|
45
|
+
"function_declaration",
|
|
46
|
+
"function_expression",
|
|
47
|
+
"generator_function",
|
|
48
|
+
"generator_function_declaration",
|
|
49
|
+
"method_definition",
|
|
50
|
+
"arrow_function",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
_STARTUP_FILE_STEMS = frozenset({"app", "cli", "index", "main", "server"})
|
|
54
|
+
|
|
55
|
+
_JS_LANGUAGE = Language(tree_sitter_javascript.language())
|
|
56
|
+
_TS_LANGUAGE = Language(tree_sitter_typescript.language_typescript())
|
|
57
|
+
_TSX_LANGUAGE = Language(tree_sitter_typescript.language_tsx())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class JavaScriptTypeScriptParseError(AdapterParseError):
|
|
61
|
+
"""JavaScript/TypeScript source could not be mapped safely."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class _ParsedFile:
|
|
66
|
+
path: Path
|
|
67
|
+
project_root: Path
|
|
68
|
+
relative_path: str
|
|
69
|
+
module_id: str
|
|
70
|
+
language: str
|
|
71
|
+
raw: bytes
|
|
72
|
+
source: str
|
|
73
|
+
digest: str
|
|
74
|
+
tree: Tree
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class _Definition:
|
|
79
|
+
node_id: str
|
|
80
|
+
syntax: SyntaxNode
|
|
81
|
+
parent_id: str
|
|
82
|
+
module_id: str
|
|
83
|
+
enclosing_class_id: str | None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True, slots=True)
|
|
87
|
+
class _ResolvedModule:
|
|
88
|
+
module_id: str
|
|
89
|
+
certain: bool
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True, slots=True)
|
|
93
|
+
class _ImportBinding:
|
|
94
|
+
local_name: str
|
|
95
|
+
imported_name: str | None
|
|
96
|
+
targets: tuple[_ResolvedModule, ...]
|
|
97
|
+
external_specifier: str | None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True, slots=True)
|
|
101
|
+
class _SyntaxEvidenceIndex:
|
|
102
|
+
"""Reusable ownership and lookup evidence derived from one syntax parse."""
|
|
103
|
+
|
|
104
|
+
parsed_files: tuple[_ParsedFile, ...]
|
|
105
|
+
definitions: tuple[_Definition, ...]
|
|
106
|
+
nodes: tuple[Node, ...]
|
|
107
|
+
parsed_by_relative: dict[str, _ParsedFile]
|
|
108
|
+
parsed_by_module: dict[str, _ParsedFile]
|
|
109
|
+
definitions_by_module: dict[str, tuple[_Definition, ...]]
|
|
110
|
+
node_by_id: dict[str, Node]
|
|
111
|
+
children_by_parent: dict[str, tuple[Node, ...]]
|
|
112
|
+
nodes_by_module_name: dict[tuple[str, str], tuple[Node, ...]]
|
|
113
|
+
nested_ranges_by_owner: dict[str, frozenset[tuple[int, int]]]
|
|
114
|
+
local_bindings_by_owner: dict[str, frozenset[str]]
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def build(
|
|
118
|
+
cls,
|
|
119
|
+
parsed_files: tuple[_ParsedFile, ...],
|
|
120
|
+
definitions: tuple[_Definition, ...],
|
|
121
|
+
nodes: tuple[Node, ...],
|
|
122
|
+
) -> _SyntaxEvidenceIndex:
|
|
123
|
+
parsed_by_relative = {
|
|
124
|
+
parsed.relative_path: parsed for parsed in parsed_files
|
|
125
|
+
}
|
|
126
|
+
parsed_by_module = {parsed.module_id: parsed for parsed in parsed_files}
|
|
127
|
+
definitions_by_id = {
|
|
128
|
+
definition.node_id: definition for definition in definitions
|
|
129
|
+
}
|
|
130
|
+
definitions_by_module_lists: dict[str, list[_Definition]] = defaultdict(list)
|
|
131
|
+
nested_ranges: dict[str, set[tuple[int, int]]] = defaultdict(set)
|
|
132
|
+
for definition in definitions:
|
|
133
|
+
definitions_by_module_lists[definition.module_id].append(definition)
|
|
134
|
+
syntax_range = (definition.syntax.start_byte, definition.syntax.end_byte)
|
|
135
|
+
nested_ranges[definition.module_id].add(syntax_range)
|
|
136
|
+
ancestor = definition.parent_id
|
|
137
|
+
while ancestor in definitions_by_id:
|
|
138
|
+
nested_ranges[ancestor].add(syntax_range)
|
|
139
|
+
ancestor = definitions_by_id[ancestor].parent_id
|
|
140
|
+
|
|
141
|
+
node_by_id = {node.id: node for node in nodes}
|
|
142
|
+
children_by_parent, nodes_by_module_name = cls._node_lookups(
|
|
143
|
+
definitions,
|
|
144
|
+
node_by_id,
|
|
145
|
+
)
|
|
146
|
+
frozen_ranges = {
|
|
147
|
+
owner: frozenset(ranges) for owner, ranges in nested_ranges.items()
|
|
148
|
+
}
|
|
149
|
+
local_bindings_by_owner = {
|
|
150
|
+
definition.node_id: frozenset(
|
|
151
|
+
_local_binding_names(
|
|
152
|
+
definition.syntax,
|
|
153
|
+
parsed_by_module[definition.module_id].raw,
|
|
154
|
+
frozen_ranges.get(definition.node_id, frozenset()),
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
for definition in definitions
|
|
158
|
+
}
|
|
159
|
+
return cls(
|
|
160
|
+
parsed_files=parsed_files,
|
|
161
|
+
definitions=definitions,
|
|
162
|
+
nodes=nodes,
|
|
163
|
+
parsed_by_relative=parsed_by_relative,
|
|
164
|
+
parsed_by_module=parsed_by_module,
|
|
165
|
+
definitions_by_module={
|
|
166
|
+
module_id: tuple(module_definitions)
|
|
167
|
+
for module_id, module_definitions in definitions_by_module_lists.items()
|
|
168
|
+
},
|
|
169
|
+
node_by_id=node_by_id,
|
|
170
|
+
children_by_parent=children_by_parent,
|
|
171
|
+
nodes_by_module_name=nodes_by_module_name,
|
|
172
|
+
nested_ranges_by_owner=frozen_ranges,
|
|
173
|
+
local_bindings_by_owner=local_bindings_by_owner,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def with_nodes(self, nodes: tuple[Node, ...]) -> _SyntaxEvidenceIndex:
|
|
177
|
+
"""Refresh node metadata without rebuilding syntax ownership evidence."""
|
|
178
|
+
|
|
179
|
+
node_by_id = {node.id: node for node in nodes}
|
|
180
|
+
children_by_parent, nodes_by_module_name = self._node_lookups(
|
|
181
|
+
self.definitions,
|
|
182
|
+
node_by_id,
|
|
183
|
+
)
|
|
184
|
+
return replace(
|
|
185
|
+
self,
|
|
186
|
+
nodes=nodes,
|
|
187
|
+
node_by_id=node_by_id,
|
|
188
|
+
children_by_parent=children_by_parent,
|
|
189
|
+
nodes_by_module_name=nodes_by_module_name,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _node_lookups(
|
|
194
|
+
definitions: tuple[_Definition, ...],
|
|
195
|
+
node_by_id: dict[str, Node],
|
|
196
|
+
) -> tuple[
|
|
197
|
+
dict[str, tuple[Node, ...]],
|
|
198
|
+
dict[tuple[str, str], tuple[Node, ...]],
|
|
199
|
+
]:
|
|
200
|
+
children: dict[str, list[Node]] = defaultdict(list)
|
|
201
|
+
module_names: dict[tuple[str, str], list[Node]] = defaultdict(list)
|
|
202
|
+
for definition in definitions:
|
|
203
|
+
node = node_by_id[definition.node_id]
|
|
204
|
+
children[definition.parent_id].append(node)
|
|
205
|
+
module_names[(definition.module_id, node.name)].append(node)
|
|
206
|
+
return (
|
|
207
|
+
{parent: tuple(nodes) for parent, nodes in children.items()},
|
|
208
|
+
{key: tuple(nodes) for key, nodes in module_names.items()},
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class JavaScriptTypeScriptAdapter:
|
|
213
|
+
"""Map JavaScript, JSX, TypeScript, and TSX into one deterministic graph."""
|
|
214
|
+
|
|
215
|
+
language = "javascript-typescript"
|
|
216
|
+
file_extensions = _ALL_EXTENSIONS
|
|
217
|
+
ignored_directories = _GENERATED_DIRECTORIES
|
|
218
|
+
|
|
219
|
+
def discover(self, path: Path) -> tuple[Path, tuple[Path, ...]]:
|
|
220
|
+
"""Return the exact JS/TS source scope accepted by this adapter."""
|
|
221
|
+
|
|
222
|
+
normalized = path.expanduser().resolve()
|
|
223
|
+
try:
|
|
224
|
+
discovery = discover_source_files(
|
|
225
|
+
normalized,
|
|
226
|
+
self.file_extensions,
|
|
227
|
+
ignored_directories=self.ignored_directories,
|
|
228
|
+
)
|
|
229
|
+
except SourceDiscoveryError as error:
|
|
230
|
+
raise JavaScriptTypeScriptParseError(str(error)) from error
|
|
231
|
+
if not discovery.files:
|
|
232
|
+
if normalized.is_file():
|
|
233
|
+
raise JavaScriptTypeScriptParseError(
|
|
234
|
+
f"expected a JavaScript/TypeScript file or directory: {normalized}"
|
|
235
|
+
)
|
|
236
|
+
raise JavaScriptTypeScriptParseError(
|
|
237
|
+
f"no JavaScript/TypeScript files found under: {normalized}"
|
|
238
|
+
)
|
|
239
|
+
return discovery.root, discovery.files
|
|
240
|
+
|
|
241
|
+
def parse(self, path: Path, *, entrypoint: str | None = None) -> Graph:
|
|
242
|
+
"""Parse ``path`` using official tree-sitter grammar wheels."""
|
|
243
|
+
|
|
244
|
+
project_root, files = self.discover(path)
|
|
245
|
+
return self.parse_files(project_root, files, entrypoint=entrypoint)
|
|
246
|
+
|
|
247
|
+
def parse_files(
|
|
248
|
+
self,
|
|
249
|
+
project_root: Path,
|
|
250
|
+
files: tuple[Path, ...],
|
|
251
|
+
*,
|
|
252
|
+
entrypoint: str | None = None,
|
|
253
|
+
) -> Graph:
|
|
254
|
+
"""Parse JS/TS files already owned by this adapter."""
|
|
255
|
+
|
|
256
|
+
parsed_files = tuple(_parse_file(file, project_root) for file in files)
|
|
257
|
+
|
|
258
|
+
nodes: list[Node] = []
|
|
259
|
+
definitions: list[_Definition] = []
|
|
260
|
+
for parsed in parsed_files:
|
|
261
|
+
nodes.append(_module_node(parsed))
|
|
262
|
+
file_nodes, file_definitions = _collect_definitions(parsed)
|
|
263
|
+
nodes.extend(file_nodes)
|
|
264
|
+
definitions.extend(file_definitions)
|
|
265
|
+
|
|
266
|
+
index = _SyntaxEvidenceIndex.build(
|
|
267
|
+
parsed_files,
|
|
268
|
+
tuple(definitions),
|
|
269
|
+
tuple(nodes),
|
|
270
|
+
)
|
|
271
|
+
entrypoint_ranks = _entrypoint_ranks(index)
|
|
272
|
+
ranked_nodes = tuple(
|
|
273
|
+
replace(node, entrypoint_rank=entrypoint_ranks.get(node.id))
|
|
274
|
+
for node in index.nodes
|
|
275
|
+
)
|
|
276
|
+
index = index.with_nodes(ranked_nodes)
|
|
277
|
+
|
|
278
|
+
import_edges: set[Edge] = set()
|
|
279
|
+
bindings_by_module: dict[str, list[_ImportBinding]] = defaultdict(list)
|
|
280
|
+
for parsed in parsed_files:
|
|
281
|
+
edges, bindings = _imports_for_file(parsed, index.parsed_by_relative)
|
|
282
|
+
import_edges.update(edges)
|
|
283
|
+
bindings_by_module[parsed.module_id].extend(bindings)
|
|
284
|
+
|
|
285
|
+
call_edges = _call_edges(index, bindings_by_module)
|
|
286
|
+
all_edges = [*import_edges, *call_edges]
|
|
287
|
+
annotations = _concept_annotations(index)
|
|
288
|
+
draft = Graph(
|
|
289
|
+
nodes=index.nodes,
|
|
290
|
+
edges=tuple(all_edges),
|
|
291
|
+
entrypoint_candidates=(),
|
|
292
|
+
project_root=str(project_root),
|
|
293
|
+
file_hashes={
|
|
294
|
+
parsed.relative_path: parsed.digest for parsed in parsed_files
|
|
295
|
+
},
|
|
296
|
+
concept_annotations=annotations,
|
|
297
|
+
partial_files=tuple(
|
|
298
|
+
parsed.relative_path
|
|
299
|
+
for parsed in parsed_files
|
|
300
|
+
if parsed.tree.root_node.has_error
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
try:
|
|
304
|
+
return finalize_graph(draft, entrypoint=entrypoint)
|
|
305
|
+
except GraphFinalizationError as error:
|
|
306
|
+
raise JavaScriptTypeScriptParseError(str(error)) from error
|
|
307
|
+
|
|
308
|
+
def concepts(self, node: Node, source: str) -> list[ConceptAnnotation]:
|
|
309
|
+
"""Return only tree-sitter-proven concepts owned by ``node``."""
|
|
310
|
+
|
|
311
|
+
if node.partial or node.language not in {"javascript", "typescript"}:
|
|
312
|
+
return []
|
|
313
|
+
raw = source.encode("utf-8")
|
|
314
|
+
path = Path(node.file)
|
|
315
|
+
parsed = _ParsedFile(
|
|
316
|
+
path=path,
|
|
317
|
+
project_root=Path("."),
|
|
318
|
+
relative_path=node.file,
|
|
319
|
+
module_id=node.region,
|
|
320
|
+
language=node.language,
|
|
321
|
+
raw=raw,
|
|
322
|
+
source=source,
|
|
323
|
+
digest=hashlib.sha256(raw).hexdigest(),
|
|
324
|
+
tree=Parser(_language_for(path.suffix.lower())).parse(raw),
|
|
325
|
+
)
|
|
326
|
+
module_node = _module_node(parsed)
|
|
327
|
+
file_nodes, definitions = _collect_definitions(parsed)
|
|
328
|
+
index = _SyntaxEvidenceIndex.build(
|
|
329
|
+
(parsed,),
|
|
330
|
+
tuple(definitions),
|
|
331
|
+
(module_node, *file_nodes),
|
|
332
|
+
)
|
|
333
|
+
if node.id not in index.node_by_id:
|
|
334
|
+
return []
|
|
335
|
+
return [
|
|
336
|
+
annotation
|
|
337
|
+
for annotation in _concept_annotations(index)
|
|
338
|
+
if annotation.node_id == node.id
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _parse_file(path: Path, project_root: Path) -> _ParsedFile:
|
|
343
|
+
raw = path.read_bytes()
|
|
344
|
+
relative = path.relative_to(project_root).as_posix()
|
|
345
|
+
language = "javascript" if path.suffix.lower() in _JAVASCRIPT_EXTENSIONS else "typescript"
|
|
346
|
+
parser = Parser(_language_for(path.suffix.lower()))
|
|
347
|
+
return _ParsedFile(
|
|
348
|
+
path=path,
|
|
349
|
+
project_root=project_root,
|
|
350
|
+
relative_path=relative,
|
|
351
|
+
module_id=f"{language}:{relative}",
|
|
352
|
+
language=language,
|
|
353
|
+
raw=raw,
|
|
354
|
+
source=raw.decode("utf-8", errors="replace"),
|
|
355
|
+
digest=hashlib.sha256(raw).hexdigest(),
|
|
356
|
+
tree=parser.parse(raw),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _language_for(extension: str) -> Language:
|
|
361
|
+
if extension == ".tsx":
|
|
362
|
+
return _TSX_LANGUAGE
|
|
363
|
+
if extension in _TYPESCRIPT_EXTENSIONS:
|
|
364
|
+
return _TS_LANGUAGE
|
|
365
|
+
return _JS_LANGUAGE
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _module_node(parsed: _ParsedFile) -> Node:
|
|
369
|
+
line_count = max(1, len(parsed.source.splitlines()))
|
|
370
|
+
return Node(
|
|
371
|
+
id=parsed.module_id,
|
|
372
|
+
kind="module",
|
|
373
|
+
name=Path(parsed.relative_path).stem,
|
|
374
|
+
language=parsed.language,
|
|
375
|
+
file=parsed.relative_path,
|
|
376
|
+
lineno=1,
|
|
377
|
+
end_lineno=line_count,
|
|
378
|
+
loc=line_count,
|
|
379
|
+
region=parsed.module_id,
|
|
380
|
+
partial=parsed.tree.root_node.has_error,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _collect_definitions(
|
|
385
|
+
parsed: _ParsedFile,
|
|
386
|
+
) -> tuple[list[Node], list[_Definition]]:
|
|
387
|
+
nodes: list[Node] = []
|
|
388
|
+
definitions: list[_Definition] = []
|
|
389
|
+
used_ids: set[str] = {parsed.module_id}
|
|
390
|
+
|
|
391
|
+
def add_definition(
|
|
392
|
+
syntax: SyntaxNode,
|
|
393
|
+
name: str,
|
|
394
|
+
kind: str,
|
|
395
|
+
qualname: tuple[str, ...],
|
|
396
|
+
parent_id: str,
|
|
397
|
+
enclosing_class_id: str | None,
|
|
398
|
+
) -> tuple[str, tuple[str, ...]]:
|
|
399
|
+
next_qualname = (*qualname, name)
|
|
400
|
+
base_id = f"{parsed.module_id}::{'.'.join(next_qualname)}"
|
|
401
|
+
node_id = _unique_node_id(base_id, syntax, used_ids)
|
|
402
|
+
used_ids.add(node_id)
|
|
403
|
+
lineno, end_lineno = _line_span(syntax)
|
|
404
|
+
nodes.append(
|
|
405
|
+
Node(
|
|
406
|
+
id=node_id,
|
|
407
|
+
kind=kind, # type: ignore[arg-type]
|
|
408
|
+
name=name,
|
|
409
|
+
language=parsed.language,
|
|
410
|
+
file=parsed.relative_path,
|
|
411
|
+
lineno=lineno,
|
|
412
|
+
end_lineno=end_lineno,
|
|
413
|
+
loc=end_lineno - lineno + 1,
|
|
414
|
+
region=parsed.module_id,
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
definitions.append(
|
|
418
|
+
_Definition(
|
|
419
|
+
node_id=node_id,
|
|
420
|
+
syntax=syntax,
|
|
421
|
+
parent_id=parent_id,
|
|
422
|
+
module_id=parsed.module_id,
|
|
423
|
+
enclosing_class_id=enclosing_class_id,
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
return node_id, next_qualname
|
|
427
|
+
|
|
428
|
+
def visit(
|
|
429
|
+
container: SyntaxNode,
|
|
430
|
+
qualname: tuple[str, ...],
|
|
431
|
+
parent_id: str,
|
|
432
|
+
enclosing_class_id: str | None,
|
|
433
|
+
) -> None:
|
|
434
|
+
for child in container.named_children:
|
|
435
|
+
if child.has_error:
|
|
436
|
+
continue
|
|
437
|
+
if child.type in {
|
|
438
|
+
"function_declaration",
|
|
439
|
+
"generator_function_declaration",
|
|
440
|
+
}:
|
|
441
|
+
name = _field_text(child, "name", parsed.raw)
|
|
442
|
+
if name:
|
|
443
|
+
node_id, child_qualname = add_definition(
|
|
444
|
+
child,
|
|
445
|
+
name,
|
|
446
|
+
"function",
|
|
447
|
+
qualname,
|
|
448
|
+
parent_id,
|
|
449
|
+
enclosing_class_id,
|
|
450
|
+
)
|
|
451
|
+
visit(child, child_qualname, node_id, enclosing_class_id)
|
|
452
|
+
continue
|
|
453
|
+
if child.type == "class_declaration":
|
|
454
|
+
name = _field_text(child, "name", parsed.raw)
|
|
455
|
+
if name:
|
|
456
|
+
node_id, child_qualname = add_definition(
|
|
457
|
+
child,
|
|
458
|
+
name,
|
|
459
|
+
"class",
|
|
460
|
+
qualname,
|
|
461
|
+
parent_id,
|
|
462
|
+
enclosing_class_id,
|
|
463
|
+
)
|
|
464
|
+
visit(child, child_qualname, node_id, node_id)
|
|
465
|
+
continue
|
|
466
|
+
if child.type == "method_definition" and enclosing_class_id:
|
|
467
|
+
name = _field_text(child, "name", parsed.raw)
|
|
468
|
+
if name:
|
|
469
|
+
node_id, child_qualname = add_definition(
|
|
470
|
+
child,
|
|
471
|
+
name,
|
|
472
|
+
"function",
|
|
473
|
+
qualname,
|
|
474
|
+
parent_id,
|
|
475
|
+
enclosing_class_id,
|
|
476
|
+
)
|
|
477
|
+
visit(child, child_qualname, node_id, enclosing_class_id)
|
|
478
|
+
continue
|
|
479
|
+
if child.type == "variable_declarator":
|
|
480
|
+
value = child.child_by_field_name("value")
|
|
481
|
+
name = _field_text(child, "name", parsed.raw)
|
|
482
|
+
if (
|
|
483
|
+
value is not None
|
|
484
|
+
and not value.has_error
|
|
485
|
+
and name
|
|
486
|
+
and value.type in _DEFINITION_TYPES
|
|
487
|
+
):
|
|
488
|
+
kind = "class" if value.type in {"class", "class_declaration"} else "function"
|
|
489
|
+
node_id, child_qualname = add_definition(
|
|
490
|
+
value,
|
|
491
|
+
name,
|
|
492
|
+
kind,
|
|
493
|
+
qualname,
|
|
494
|
+
parent_id,
|
|
495
|
+
enclosing_class_id,
|
|
496
|
+
)
|
|
497
|
+
visit(value, child_qualname, node_id, enclosing_class_id)
|
|
498
|
+
continue
|
|
499
|
+
visit(child, qualname, parent_id, enclosing_class_id)
|
|
500
|
+
|
|
501
|
+
visit(parsed.tree.root_node, (), parsed.module_id, None)
|
|
502
|
+
return nodes, definitions
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _unique_node_id(base_id: str, syntax: SyntaxNode, used_ids: set[str]) -> str:
|
|
506
|
+
if base_id not in used_ids:
|
|
507
|
+
return base_id
|
|
508
|
+
lineno, _ = _line_span(syntax)
|
|
509
|
+
candidate = f"{base_id}@{lineno}"
|
|
510
|
+
counter = 2
|
|
511
|
+
while candidate in used_ids:
|
|
512
|
+
candidate = f"{base_id}@{lineno}-{counter}"
|
|
513
|
+
counter += 1
|
|
514
|
+
return candidate
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _line_span(syntax: SyntaxNode) -> tuple[int, int]:
|
|
518
|
+
lineno = syntax.start_point.row + 1
|
|
519
|
+
end_lineno = syntax.end_point.row + (1 if syntax.end_point.column else 0)
|
|
520
|
+
return lineno, max(lineno, end_lineno)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _field_text(syntax: SyntaxNode, field: str, raw: bytes) -> str | None:
|
|
524
|
+
child = syntax.child_by_field_name(field)
|
|
525
|
+
if child is None or child.has_error or child.type not in {
|
|
526
|
+
"identifier",
|
|
527
|
+
"property_identifier",
|
|
528
|
+
"private_property_identifier",
|
|
529
|
+
"type_identifier",
|
|
530
|
+
}:
|
|
531
|
+
return None
|
|
532
|
+
return _node_text(child, raw)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _node_text(syntax: SyntaxNode, raw: bytes) -> str:
|
|
536
|
+
return raw[syntax.start_byte : syntax.end_byte].decode("utf-8", errors="replace")
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _walk(syntax: SyntaxNode) -> Iterable[SyntaxNode]:
|
|
540
|
+
for child in syntax.named_children:
|
|
541
|
+
yield child
|
|
542
|
+
yield from _walk(child)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _walk_owned(
|
|
546
|
+
syntax: SyntaxNode,
|
|
547
|
+
nested_definition_ranges: AbstractSet[tuple[int, int]],
|
|
548
|
+
) -> Iterable[SyntaxNode]:
|
|
549
|
+
for child in syntax.named_children:
|
|
550
|
+
if (child.start_byte, child.end_byte) in nested_definition_ranges:
|
|
551
|
+
continue
|
|
552
|
+
yield child
|
|
553
|
+
yield from _walk_owned(child, nested_definition_ranges)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _concept_annotations(
|
|
557
|
+
index: _SyntaxEvidenceIndex,
|
|
558
|
+
) -> tuple[ConceptAnnotation, ...]:
|
|
559
|
+
annotations: set[ConceptAnnotation] = set()
|
|
560
|
+
for parsed in index.parsed_files:
|
|
561
|
+
if parsed.tree.root_node.has_error:
|
|
562
|
+
continue
|
|
563
|
+
annotations.update(
|
|
564
|
+
_concepts_for_owner(
|
|
565
|
+
index.node_by_id[parsed.module_id],
|
|
566
|
+
parsed.tree.root_node,
|
|
567
|
+
parsed,
|
|
568
|
+
index.nested_ranges_by_owner.get(parsed.module_id, frozenset()),
|
|
569
|
+
include_owner=False,
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
for definition in index.definitions:
|
|
573
|
+
annotations.update(
|
|
574
|
+
_concepts_for_owner(
|
|
575
|
+
index.node_by_id[definition.node_id],
|
|
576
|
+
definition.syntax,
|
|
577
|
+
index.parsed_by_module[definition.module_id],
|
|
578
|
+
index.nested_ranges_by_owner.get(definition.node_id, frozenset()),
|
|
579
|
+
include_owner=True,
|
|
580
|
+
)
|
|
581
|
+
)
|
|
582
|
+
return tuple(
|
|
583
|
+
sorted(
|
|
584
|
+
annotations,
|
|
585
|
+
key=lambda item: (
|
|
586
|
+
item.language,
|
|
587
|
+
item.node_id,
|
|
588
|
+
item.lineno,
|
|
589
|
+
item.concept,
|
|
590
|
+
item.end_lineno,
|
|
591
|
+
),
|
|
592
|
+
)
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _concepts_for_owner(
|
|
597
|
+
owner: Node,
|
|
598
|
+
syntax: SyntaxNode,
|
|
599
|
+
parsed: _ParsedFile,
|
|
600
|
+
nested_definition_ranges: AbstractSet[tuple[int, int]],
|
|
601
|
+
*,
|
|
602
|
+
include_owner: bool,
|
|
603
|
+
) -> set[ConceptAnnotation]:
|
|
604
|
+
candidates: Iterable[SyntaxNode]
|
|
605
|
+
walked = _walk_owned(syntax, nested_definition_ranges)
|
|
606
|
+
candidates = (syntax, *walked) if include_owner else walked
|
|
607
|
+
annotations: set[ConceptAnnotation] = set()
|
|
608
|
+
source_lines = parsed.source.splitlines()
|
|
609
|
+
for candidate in candidates:
|
|
610
|
+
if candidate.has_error:
|
|
611
|
+
continue
|
|
612
|
+
for concept in _concepts_for_syntax(candidate):
|
|
613
|
+
lineno, end_lineno = _line_span(candidate)
|
|
614
|
+
snippet = (
|
|
615
|
+
source_lines[lineno - 1].strip()
|
|
616
|
+
if 0 < lineno <= len(source_lines)
|
|
617
|
+
else ""
|
|
618
|
+
)
|
|
619
|
+
annotations.add(
|
|
620
|
+
ConceptAnnotation(
|
|
621
|
+
node_id=owner.id,
|
|
622
|
+
language=owner.language,
|
|
623
|
+
concept=concept,
|
|
624
|
+
lineno=lineno,
|
|
625
|
+
end_lineno=end_lineno,
|
|
626
|
+
snippet=snippet[:240],
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
return annotations
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _concepts_for_syntax(syntax: SyntaxNode) -> tuple[str, ...]:
|
|
633
|
+
concepts: list[str] = []
|
|
634
|
+
if syntax.type in {
|
|
635
|
+
"function_declaration",
|
|
636
|
+
"function_expression",
|
|
637
|
+
"generator_function",
|
|
638
|
+
"generator_function_declaration",
|
|
639
|
+
"method_definition",
|
|
640
|
+
"arrow_function",
|
|
641
|
+
} and any(child.type == "async" for child in syntax.children):
|
|
642
|
+
concepts.append("async-await")
|
|
643
|
+
if syntax.type == "await_expression":
|
|
644
|
+
concepts.append("async-await")
|
|
645
|
+
if syntax.type == "arrow_function":
|
|
646
|
+
concepts.append("arrow-function")
|
|
647
|
+
if syntax.type in {"object_pattern", "array_pattern"}:
|
|
648
|
+
concepts.append("destructuring")
|
|
649
|
+
if syntax.type == "optional_chain":
|
|
650
|
+
concepts.append("optional-chaining")
|
|
651
|
+
if syntax.type == "binary_expression" and any(
|
|
652
|
+
child.type == "??" for child in syntax.children
|
|
653
|
+
):
|
|
654
|
+
concepts.append("nullish-coalescing")
|
|
655
|
+
if syntax.type in {"import_statement", "export_statement"}:
|
|
656
|
+
concepts.append("module-syntax")
|
|
657
|
+
if syntax.type == "type_annotation":
|
|
658
|
+
concepts.append("type-annotation")
|
|
659
|
+
if syntax.type == "interface_declaration":
|
|
660
|
+
concepts.append("interface")
|
|
661
|
+
if syntax.type in {"type_parameters", "type_arguments"}:
|
|
662
|
+
concepts.append("generic")
|
|
663
|
+
if syntax.type in {"jsx_element", "jsx_self_closing_element", "jsx_fragment"}:
|
|
664
|
+
concepts.append("jsx")
|
|
665
|
+
return tuple(concepts)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _imports_for_file(
|
|
669
|
+
parsed: _ParsedFile,
|
|
670
|
+
parsed_by_relative: dict[str, _ParsedFile],
|
|
671
|
+
) -> tuple[list[Edge], list[_ImportBinding]]:
|
|
672
|
+
edges: list[Edge] = []
|
|
673
|
+
bindings: list[_ImportBinding] = []
|
|
674
|
+
root = parsed.tree.root_node
|
|
675
|
+
for syntax in _walk(root):
|
|
676
|
+
if syntax.has_error:
|
|
677
|
+
continue
|
|
678
|
+
if syntax.type in {"import_statement", "export_statement"}:
|
|
679
|
+
source_node = syntax.child_by_field_name("source")
|
|
680
|
+
specifier = _string_value(source_node, parsed.raw)
|
|
681
|
+
if specifier is None:
|
|
682
|
+
continue
|
|
683
|
+
resolved = _resolve_modules(parsed, specifier, parsed_by_relative)
|
|
684
|
+
edges.extend(_import_edges(parsed, syntax, specifier, resolved))
|
|
685
|
+
if syntax.type == "import_statement":
|
|
686
|
+
bindings.extend(
|
|
687
|
+
_bindings_from_import(syntax, parsed.raw, specifier, resolved)
|
|
688
|
+
)
|
|
689
|
+
if syntax.type in {"call_expression", "new_expression"}:
|
|
690
|
+
function = syntax.child_by_field_name("function")
|
|
691
|
+
if function is None and syntax.type == "new_expression":
|
|
692
|
+
function = syntax.child_by_field_name("constructor")
|
|
693
|
+
if function is None or function.type not in {"identifier", "import"}:
|
|
694
|
+
continue
|
|
695
|
+
function_name = _node_text(function, parsed.raw)
|
|
696
|
+
if function_name not in {"require", "import"}:
|
|
697
|
+
continue
|
|
698
|
+
specifier = _first_string_argument(syntax, parsed.raw)
|
|
699
|
+
if specifier is None:
|
|
700
|
+
continue
|
|
701
|
+
resolved = _resolve_modules(parsed, specifier, parsed_by_relative)
|
|
702
|
+
edges.extend(_import_edges(parsed, syntax, specifier, resolved))
|
|
703
|
+
if function_name == "require" and _is_module_binding(syntax):
|
|
704
|
+
binding = _binding_from_require(
|
|
705
|
+
syntax, parsed.raw, specifier, resolved
|
|
706
|
+
)
|
|
707
|
+
if binding is not None:
|
|
708
|
+
bindings.extend(binding)
|
|
709
|
+
return edges, bindings
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _is_module_binding(syntax: SyntaxNode) -> bool:
|
|
713
|
+
parent = syntax.parent
|
|
714
|
+
allowed = {"export_statement", "lexical_declaration", "variable_declaration", "variable_declarator"}
|
|
715
|
+
while parent is not None and parent.type != "program":
|
|
716
|
+
if parent.type not in allowed:
|
|
717
|
+
return False
|
|
718
|
+
parent = parent.parent
|
|
719
|
+
return parent is not None
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _string_value(syntax: SyntaxNode | None, raw: bytes) -> str | None:
|
|
723
|
+
if syntax is None or syntax.type != "string" or syntax.has_error:
|
|
724
|
+
return None
|
|
725
|
+
value = _node_text(syntax, raw)
|
|
726
|
+
if len(value) < 2 or value[0] not in {'"', "'"} or value[-1] != value[0]:
|
|
727
|
+
return None
|
|
728
|
+
unquoted = value[1:-1]
|
|
729
|
+
if "\\" in unquoted:
|
|
730
|
+
return None
|
|
731
|
+
return unquoted
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _first_string_argument(syntax: SyntaxNode, raw: bytes) -> str | None:
|
|
735
|
+
arguments = syntax.child_by_field_name("arguments")
|
|
736
|
+
if arguments is None:
|
|
737
|
+
return None
|
|
738
|
+
first = arguments.named_child(0)
|
|
739
|
+
return _string_value(first, raw)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _resolve_modules(
|
|
743
|
+
parsed: _ParsedFile,
|
|
744
|
+
specifier: str,
|
|
745
|
+
parsed_by_relative: dict[str, _ParsedFile],
|
|
746
|
+
) -> tuple[_ResolvedModule, ...]:
|
|
747
|
+
if not specifier.startswith("."):
|
|
748
|
+
return ()
|
|
749
|
+
requested = (parsed.path.parent / specifier).resolve()
|
|
750
|
+
project_root = parsed.project_root
|
|
751
|
+
if not requested.is_relative_to(project_root):
|
|
752
|
+
return ()
|
|
753
|
+
relative = requested.relative_to(project_root).as_posix()
|
|
754
|
+
candidates: list[tuple[str, bool]] = []
|
|
755
|
+
|
|
756
|
+
if relative in parsed_by_relative:
|
|
757
|
+
candidates.append((relative, True))
|
|
758
|
+
suffix = Path(relative).suffix.lower()
|
|
759
|
+
if suffix:
|
|
760
|
+
substitutions = _extension_substitutions(relative, suffix)
|
|
761
|
+
candidates.extend((candidate, False) for candidate in substitutions)
|
|
762
|
+
else:
|
|
763
|
+
for extension in sorted(_ALL_EXTENSIONS):
|
|
764
|
+
candidates.append((f"{relative}{extension}", False))
|
|
765
|
+
candidates.append((f"{relative}/index{extension}", False))
|
|
766
|
+
|
|
767
|
+
resolved: dict[str, bool] = {}
|
|
768
|
+
for candidate, certain in candidates:
|
|
769
|
+
target = parsed_by_relative.get(candidate)
|
|
770
|
+
if target is None:
|
|
771
|
+
continue
|
|
772
|
+
resolved[target.module_id] = resolved.get(target.module_id, False) or certain
|
|
773
|
+
return tuple(
|
|
774
|
+
_ResolvedModule(module_id, certain)
|
|
775
|
+
for module_id, certain in sorted(resolved.items())
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _extension_substitutions(relative: str, suffix: str) -> tuple[str, ...]:
|
|
780
|
+
stem = relative[: -len(suffix)]
|
|
781
|
+
if suffix in {".js", ".jsx"}:
|
|
782
|
+
return tuple(f"{stem}{extension}" for extension in (".ts", ".tsx"))
|
|
783
|
+
if suffix == ".mjs":
|
|
784
|
+
return (f"{stem}.mts",)
|
|
785
|
+
if suffix == ".cjs":
|
|
786
|
+
return (f"{stem}.cts",)
|
|
787
|
+
return ()
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _import_edges(
|
|
791
|
+
parsed: _ParsedFile,
|
|
792
|
+
syntax: SyntaxNode,
|
|
793
|
+
specifier: str,
|
|
794
|
+
resolved: tuple[_ResolvedModule, ...],
|
|
795
|
+
) -> list[Edge]:
|
|
796
|
+
lineno = syntax.start_point.row + 1
|
|
797
|
+
if resolved:
|
|
798
|
+
return [
|
|
799
|
+
Edge(
|
|
800
|
+
src=parsed.module_id,
|
|
801
|
+
dst=target.module_id,
|
|
802
|
+
kind="import",
|
|
803
|
+
certain=target.certain,
|
|
804
|
+
lineno=lineno,
|
|
805
|
+
)
|
|
806
|
+
for target in resolved
|
|
807
|
+
]
|
|
808
|
+
return [
|
|
809
|
+
Edge(
|
|
810
|
+
src=parsed.module_id,
|
|
811
|
+
dst=f"external:{specifier}",
|
|
812
|
+
kind="import",
|
|
813
|
+
certain=True,
|
|
814
|
+
lineno=lineno,
|
|
815
|
+
external=True,
|
|
816
|
+
)
|
|
817
|
+
]
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _bindings_from_import(
|
|
821
|
+
syntax: SyntaxNode,
|
|
822
|
+
raw: bytes,
|
|
823
|
+
specifier: str,
|
|
824
|
+
resolved: tuple[_ResolvedModule, ...],
|
|
825
|
+
) -> list[_ImportBinding]:
|
|
826
|
+
clause = next(
|
|
827
|
+
(child for child in syntax.named_children if child.type == "import_clause"),
|
|
828
|
+
None,
|
|
829
|
+
)
|
|
830
|
+
if clause is None:
|
|
831
|
+
return []
|
|
832
|
+
external = None if resolved else specifier
|
|
833
|
+
bindings: list[_ImportBinding] = []
|
|
834
|
+
for child in clause.named_children:
|
|
835
|
+
if child.type == "identifier":
|
|
836
|
+
bindings.append(
|
|
837
|
+
_ImportBinding(_node_text(child, raw), "default", resolved, external)
|
|
838
|
+
)
|
|
839
|
+
elif child.type == "namespace_import":
|
|
840
|
+
identifier = next(
|
|
841
|
+
(item for item in child.named_children if item.type == "identifier"),
|
|
842
|
+
None,
|
|
843
|
+
)
|
|
844
|
+
if identifier is not None:
|
|
845
|
+
bindings.append(
|
|
846
|
+
_ImportBinding(_node_text(identifier, raw), None, resolved, external)
|
|
847
|
+
)
|
|
848
|
+
elif child.type == "named_imports":
|
|
849
|
+
for item in child.named_children:
|
|
850
|
+
if item.type != "import_specifier":
|
|
851
|
+
continue
|
|
852
|
+
name = item.child_by_field_name("name")
|
|
853
|
+
alias = item.child_by_field_name("alias")
|
|
854
|
+
if name is None:
|
|
855
|
+
continue
|
|
856
|
+
imported_name = _node_text(name, raw)
|
|
857
|
+
local_name = _node_text(alias or name, raw)
|
|
858
|
+
bindings.append(
|
|
859
|
+
_ImportBinding(local_name, imported_name, resolved, external)
|
|
860
|
+
)
|
|
861
|
+
return bindings
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _binding_from_require(
|
|
865
|
+
syntax: SyntaxNode,
|
|
866
|
+
raw: bytes,
|
|
867
|
+
specifier: str,
|
|
868
|
+
resolved: tuple[_ResolvedModule, ...],
|
|
869
|
+
) -> list[_ImportBinding] | None:
|
|
870
|
+
parent = syntax.parent
|
|
871
|
+
if parent is None or parent.type != "variable_declarator":
|
|
872
|
+
return None
|
|
873
|
+
value = parent.child_by_field_name("value")
|
|
874
|
+
name = parent.child_by_field_name("name")
|
|
875
|
+
if value != syntax or name is None:
|
|
876
|
+
return None
|
|
877
|
+
external = None if resolved else specifier
|
|
878
|
+
if name.type == "identifier":
|
|
879
|
+
return [_ImportBinding(_node_text(name, raw), None, resolved, external)]
|
|
880
|
+
if name.type != "object_pattern":
|
|
881
|
+
return None
|
|
882
|
+
bindings: list[_ImportBinding] = []
|
|
883
|
+
for child in name.named_children:
|
|
884
|
+
if child.type == "shorthand_property_identifier_pattern":
|
|
885
|
+
imported = _node_text(child, raw)
|
|
886
|
+
bindings.append(_ImportBinding(imported, imported, resolved, external))
|
|
887
|
+
elif child.type == "pair_pattern":
|
|
888
|
+
key = child.child_by_field_name("key")
|
|
889
|
+
value_node = child.child_by_field_name("value")
|
|
890
|
+
if key is not None and value_node is not None and value_node.type == "identifier":
|
|
891
|
+
bindings.append(
|
|
892
|
+
_ImportBinding(
|
|
893
|
+
_node_text(value_node, raw),
|
|
894
|
+
_node_text(key, raw),
|
|
895
|
+
resolved,
|
|
896
|
+
external,
|
|
897
|
+
)
|
|
898
|
+
)
|
|
899
|
+
return bindings
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _call_edges(
|
|
903
|
+
index: _SyntaxEvidenceIndex,
|
|
904
|
+
bindings_by_module: dict[str, list[_ImportBinding]],
|
|
905
|
+
) -> list[Edge]:
|
|
906
|
+
edges: list[Edge] = []
|
|
907
|
+
for definition in index.definitions:
|
|
908
|
+
parsed = index.parsed_by_module[definition.module_id]
|
|
909
|
+
binding_map = {
|
|
910
|
+
binding.local_name: binding
|
|
911
|
+
for binding in bindings_by_module[definition.module_id]
|
|
912
|
+
}
|
|
913
|
+
for syntax in _walk_owned(
|
|
914
|
+
definition.syntax,
|
|
915
|
+
index.nested_ranges_by_owner.get(definition.node_id, frozenset()),
|
|
916
|
+
):
|
|
917
|
+
if syntax.has_error or syntax.type not in {"call_expression", "new_expression"}:
|
|
918
|
+
continue
|
|
919
|
+
if _is_import_loader_call(syntax, parsed.raw):
|
|
920
|
+
continue
|
|
921
|
+
edges.extend(
|
|
922
|
+
_resolve_call(
|
|
923
|
+
definition,
|
|
924
|
+
syntax,
|
|
925
|
+
parsed.raw,
|
|
926
|
+
binding_map,
|
|
927
|
+
index,
|
|
928
|
+
index.local_bindings_by_owner[definition.node_id],
|
|
929
|
+
)
|
|
930
|
+
)
|
|
931
|
+
return edges
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _is_import_loader_call(syntax: SyntaxNode, raw: bytes) -> bool:
|
|
935
|
+
function = syntax.child_by_field_name("function")
|
|
936
|
+
if function is None:
|
|
937
|
+
return False
|
|
938
|
+
return function.type == "import" or (
|
|
939
|
+
function.type == "identifier" and _node_text(function, raw) == "require"
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _resolve_call(
|
|
944
|
+
definition: _Definition,
|
|
945
|
+
syntax: SyntaxNode,
|
|
946
|
+
raw: bytes,
|
|
947
|
+
bindings: dict[str, _ImportBinding],
|
|
948
|
+
index: _SyntaxEvidenceIndex,
|
|
949
|
+
local_binding_names: frozenset[str],
|
|
950
|
+
) -> list[Edge]:
|
|
951
|
+
target = syntax.child_by_field_name("function")
|
|
952
|
+
if target is None and syntax.type == "new_expression":
|
|
953
|
+
target = syntax.child_by_field_name("constructor")
|
|
954
|
+
lineno = syntax.start_point.row + 1
|
|
955
|
+
if target is None:
|
|
956
|
+
return [_dynamic_call_edge(definition.node_id, lineno)]
|
|
957
|
+
|
|
958
|
+
if target.type == "identifier":
|
|
959
|
+
name = _node_text(target, raw)
|
|
960
|
+
nested = [
|
|
961
|
+
node
|
|
962
|
+
for node in index.children_by_parent.get(definition.node_id, ())
|
|
963
|
+
if node.name == name
|
|
964
|
+
]
|
|
965
|
+
if nested:
|
|
966
|
+
return [
|
|
967
|
+
_call_edge(
|
|
968
|
+
definition.node_id,
|
|
969
|
+
candidate.id,
|
|
970
|
+
lineno,
|
|
971
|
+
certain=len(nested) == 1,
|
|
972
|
+
)
|
|
973
|
+
for candidate in sorted(nested, key=lambda node: node.id)
|
|
974
|
+
]
|
|
975
|
+
if name in local_binding_names:
|
|
976
|
+
return [
|
|
977
|
+
Edge(
|
|
978
|
+
definition.node_id,
|
|
979
|
+
f"unresolved:{definition.node_id}:{name}",
|
|
980
|
+
"call",
|
|
981
|
+
certain=False,
|
|
982
|
+
lineno=lineno,
|
|
983
|
+
)
|
|
984
|
+
]
|
|
985
|
+
local = _local_call_candidates(
|
|
986
|
+
definition,
|
|
987
|
+
name,
|
|
988
|
+
index,
|
|
989
|
+
)
|
|
990
|
+
if local:
|
|
991
|
+
return [
|
|
992
|
+
_call_edge(
|
|
993
|
+
definition.node_id,
|
|
994
|
+
candidate.id,
|
|
995
|
+
lineno,
|
|
996
|
+
certain=len(local) == 1,
|
|
997
|
+
)
|
|
998
|
+
for candidate in local
|
|
999
|
+
]
|
|
1000
|
+
binding = bindings.get(name)
|
|
1001
|
+
if binding is not None:
|
|
1002
|
+
return _binding_call_edges(
|
|
1003
|
+
definition.node_id,
|
|
1004
|
+
binding,
|
|
1005
|
+
binding.imported_name,
|
|
1006
|
+
lineno,
|
|
1007
|
+
index,
|
|
1008
|
+
)
|
|
1009
|
+
return [
|
|
1010
|
+
Edge(
|
|
1011
|
+
definition.node_id,
|
|
1012
|
+
f"unresolved:{definition.module_id}:{name}",
|
|
1013
|
+
"call",
|
|
1014
|
+
certain=False,
|
|
1015
|
+
lineno=lineno,
|
|
1016
|
+
)
|
|
1017
|
+
]
|
|
1018
|
+
|
|
1019
|
+
if target.type == "member_expression":
|
|
1020
|
+
object_node = target.child_by_field_name("object")
|
|
1021
|
+
property_node = target.child_by_field_name("property")
|
|
1022
|
+
if property_node is None:
|
|
1023
|
+
return [_dynamic_call_edge(definition.node_id, lineno)]
|
|
1024
|
+
name = _node_text(property_node, raw)
|
|
1025
|
+
if object_node is not None and object_node.type in {"this", "super"}:
|
|
1026
|
+
candidates = [
|
|
1027
|
+
node
|
|
1028
|
+
for node in index.children_by_parent.get(
|
|
1029
|
+
definition.enclosing_class_id or "", ()
|
|
1030
|
+
)
|
|
1031
|
+
if node.name == name
|
|
1032
|
+
]
|
|
1033
|
+
if candidates:
|
|
1034
|
+
return [
|
|
1035
|
+
_call_edge(
|
|
1036
|
+
definition.node_id,
|
|
1037
|
+
candidate.id,
|
|
1038
|
+
lineno,
|
|
1039
|
+
certain=len(candidates) == 1,
|
|
1040
|
+
)
|
|
1041
|
+
for candidate in sorted(candidates, key=lambda node: node.id)
|
|
1042
|
+
]
|
|
1043
|
+
if object_node is not None and object_node.type == "identifier":
|
|
1044
|
+
object_name = _node_text(object_node, raw)
|
|
1045
|
+
binding = None if object_name in local_binding_names else bindings.get(object_name)
|
|
1046
|
+
if binding is not None and binding.imported_name is None:
|
|
1047
|
+
return _binding_call_edges(
|
|
1048
|
+
definition.node_id,
|
|
1049
|
+
binding,
|
|
1050
|
+
name,
|
|
1051
|
+
lineno,
|
|
1052
|
+
index,
|
|
1053
|
+
)
|
|
1054
|
+
possible = index.nodes_by_module_name.get((definition.module_id, name), ())
|
|
1055
|
+
if possible:
|
|
1056
|
+
return [
|
|
1057
|
+
_call_edge(definition.node_id, node.id, lineno, certain=False)
|
|
1058
|
+
for node in sorted(possible, key=lambda node: node.id)
|
|
1059
|
+
]
|
|
1060
|
+
dotted = _node_text(target, raw)
|
|
1061
|
+
return [
|
|
1062
|
+
Edge(
|
|
1063
|
+
definition.node_id,
|
|
1064
|
+
f"external:{dotted}",
|
|
1065
|
+
"call",
|
|
1066
|
+
certain=False,
|
|
1067
|
+
lineno=lineno,
|
|
1068
|
+
external=True,
|
|
1069
|
+
)
|
|
1070
|
+
]
|
|
1071
|
+
|
|
1072
|
+
return [_dynamic_call_edge(definition.node_id, lineno)]
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def _local_call_candidates(
|
|
1076
|
+
definition: _Definition,
|
|
1077
|
+
name: str,
|
|
1078
|
+
index: _SyntaxEvidenceIndex,
|
|
1079
|
+
) -> list[Node]:
|
|
1080
|
+
siblings = [
|
|
1081
|
+
node
|
|
1082
|
+
for node in index.children_by_parent.get(definition.parent_id, ())
|
|
1083
|
+
if node.name == name
|
|
1084
|
+
]
|
|
1085
|
+
if siblings:
|
|
1086
|
+
return sorted(siblings, key=lambda node: node.id)
|
|
1087
|
+
return sorted(
|
|
1088
|
+
index.nodes_by_module_name.get((definition.module_id, name), ()),
|
|
1089
|
+
key=lambda node: node.id,
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def _local_binding_names(
|
|
1094
|
+
syntax: SyntaxNode,
|
|
1095
|
+
raw: bytes,
|
|
1096
|
+
nested_definition_ranges: AbstractSet[tuple[int, int]],
|
|
1097
|
+
) -> set[str]:
|
|
1098
|
+
names: set[str] = set()
|
|
1099
|
+
for field in ("parameter", "parameters"):
|
|
1100
|
+
parameter_node = syntax.child_by_field_name(field)
|
|
1101
|
+
if parameter_node is not None:
|
|
1102
|
+
names.update(_identifier_texts(parameter_node, raw))
|
|
1103
|
+
for child in _walk_owned(syntax, nested_definition_ranges):
|
|
1104
|
+
if child.type == "variable_declarator":
|
|
1105
|
+
pattern = child.child_by_field_name("name")
|
|
1106
|
+
if pattern is not None:
|
|
1107
|
+
names.update(_identifier_texts(pattern, raw))
|
|
1108
|
+
elif child.type == "catch_clause":
|
|
1109
|
+
parameter = child.child_by_field_name("parameter")
|
|
1110
|
+
if parameter is not None:
|
|
1111
|
+
names.update(_identifier_texts(parameter, raw))
|
|
1112
|
+
return names
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def _identifier_texts(syntax: SyntaxNode, raw: bytes) -> set[str]:
|
|
1116
|
+
nodes = (syntax, *_walk(syntax))
|
|
1117
|
+
return {
|
|
1118
|
+
_node_text(node, raw)
|
|
1119
|
+
for node in nodes
|
|
1120
|
+
if node.type in {"identifier", "shorthand_property_identifier_pattern"}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def _binding_call_edges(
|
|
1125
|
+
src: str,
|
|
1126
|
+
binding: _ImportBinding,
|
|
1127
|
+
imported_name: str | None,
|
|
1128
|
+
lineno: int,
|
|
1129
|
+
index: _SyntaxEvidenceIndex,
|
|
1130
|
+
) -> list[Edge]:
|
|
1131
|
+
if binding.external_specifier is not None:
|
|
1132
|
+
suffix = f".{imported_name}" if imported_name else ""
|
|
1133
|
+
return [
|
|
1134
|
+
Edge(
|
|
1135
|
+
src,
|
|
1136
|
+
f"external:{binding.external_specifier}{suffix}",
|
|
1137
|
+
"call",
|
|
1138
|
+
certain=False,
|
|
1139
|
+
lineno=lineno,
|
|
1140
|
+
external=True,
|
|
1141
|
+
)
|
|
1142
|
+
]
|
|
1143
|
+
candidates: list[tuple[Node, bool]] = []
|
|
1144
|
+
if imported_name and imported_name != "default":
|
|
1145
|
+
for target in binding.targets:
|
|
1146
|
+
candidates.extend(
|
|
1147
|
+
(node, target.certain)
|
|
1148
|
+
for node in index.nodes_by_module_name.get(
|
|
1149
|
+
(target.module_id, imported_name),
|
|
1150
|
+
(),
|
|
1151
|
+
)
|
|
1152
|
+
)
|
|
1153
|
+
if candidates:
|
|
1154
|
+
unambiguous = len(candidates) == 1
|
|
1155
|
+
return [
|
|
1156
|
+
_call_edge(
|
|
1157
|
+
src,
|
|
1158
|
+
node.id,
|
|
1159
|
+
lineno,
|
|
1160
|
+
certain=unambiguous and path_certain,
|
|
1161
|
+
)
|
|
1162
|
+
for node, path_certain in sorted(candidates, key=lambda item: item[0].id)
|
|
1163
|
+
]
|
|
1164
|
+
target_names = ",".join(target.module_id for target in binding.targets)
|
|
1165
|
+
suffix = imported_name or "namespace"
|
|
1166
|
+
return [
|
|
1167
|
+
Edge(
|
|
1168
|
+
src,
|
|
1169
|
+
f"unresolved:{target_names}:{suffix}",
|
|
1170
|
+
"call",
|
|
1171
|
+
certain=False,
|
|
1172
|
+
lineno=lineno,
|
|
1173
|
+
)
|
|
1174
|
+
]
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def _call_edge(src: str, dst: str, lineno: int, certain: bool) -> Edge:
|
|
1178
|
+
return Edge(src, dst, "call", certain=certain, lineno=lineno)
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def _dynamic_call_edge(src: str, lineno: int) -> Edge:
|
|
1182
|
+
return Edge(
|
|
1183
|
+
src,
|
|
1184
|
+
f"external:dynamic-call@{lineno}",
|
|
1185
|
+
"call",
|
|
1186
|
+
certain=False,
|
|
1187
|
+
lineno=lineno,
|
|
1188
|
+
external=True,
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def _entrypoint_ranks(
|
|
1193
|
+
index: _SyntaxEvidenceIndex,
|
|
1194
|
+
) -> dict[str, int]:
|
|
1195
|
+
ranks: dict[str, int] = {}
|
|
1196
|
+
for parsed in index.parsed_files:
|
|
1197
|
+
for definition in index.definitions_by_module.get(parsed.module_id, ()):
|
|
1198
|
+
if index.node_by_id[definition.node_id].name == "main":
|
|
1199
|
+
ranks[definition.node_id] = 1
|
|
1200
|
+
module_rank: int | None = None
|
|
1201
|
+
direct_children = [
|
|
1202
|
+
child for child in parsed.tree.root_node.named_children if not child.has_error
|
|
1203
|
+
]
|
|
1204
|
+
for child in direct_children:
|
|
1205
|
+
if child.type == "if_statement" and _is_entrypoint_guard(child, parsed.raw):
|
|
1206
|
+
module_rank = 0
|
|
1207
|
+
break
|
|
1208
|
+
if module_rank is None and _has_top_level_startup_call(
|
|
1209
|
+
parsed,
|
|
1210
|
+
index.nested_ranges_by_owner.get(parsed.module_id, frozenset()),
|
|
1211
|
+
):
|
|
1212
|
+
module_rank = 2
|
|
1213
|
+
if module_rank is None and parsed.path.stem.lower() in _STARTUP_FILE_STEMS:
|
|
1214
|
+
module_rank = 3
|
|
1215
|
+
if module_rank is not None:
|
|
1216
|
+
ranks[parsed.module_id] = module_rank
|
|
1217
|
+
return ranks
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def _is_entrypoint_guard(syntax: SyntaxNode, raw: bytes) -> bool:
|
|
1221
|
+
condition = syntax.child_by_field_name("condition")
|
|
1222
|
+
if condition is None:
|
|
1223
|
+
return False
|
|
1224
|
+
normalized = "".join(_node_text(condition, raw).split()).strip("()")
|
|
1225
|
+
return normalized in {
|
|
1226
|
+
"require.main===module",
|
|
1227
|
+
"module===require.main",
|
|
1228
|
+
"require.main==module",
|
|
1229
|
+
"module==require.main",
|
|
1230
|
+
"import.meta.main",
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def _has_top_level_startup_call(
|
|
1235
|
+
parsed: _ParsedFile,
|
|
1236
|
+
nested_definition_ranges: AbstractSet[tuple[int, int]],
|
|
1237
|
+
) -> bool:
|
|
1238
|
+
for syntax in _walk_owned(parsed.tree.root_node, nested_definition_ranges):
|
|
1239
|
+
if syntax.has_error or syntax.type != "call_expression":
|
|
1240
|
+
continue
|
|
1241
|
+
function = syntax.child_by_field_name("function")
|
|
1242
|
+
if function is None:
|
|
1243
|
+
continue
|
|
1244
|
+
if function.type == "identifier" and _node_text(function, parsed.raw) == "main":
|
|
1245
|
+
return True
|
|
1246
|
+
if function.type != "member_expression":
|
|
1247
|
+
continue
|
|
1248
|
+
object_node = function.child_by_field_name("object")
|
|
1249
|
+
property_node = function.child_by_field_name("property")
|
|
1250
|
+
if object_node is None or property_node is None:
|
|
1251
|
+
continue
|
|
1252
|
+
property_name = _node_text(property_node, parsed.raw)
|
|
1253
|
+
object_text = _node_text(object_node, parsed.raw)
|
|
1254
|
+
if property_name == "listen" or (
|
|
1255
|
+
property_name == "serve" and object_text in {"Bun", "Deno"}
|
|
1256
|
+
) or (
|
|
1257
|
+
property_name == "render" and object_text.startswith("createRoot(")
|
|
1258
|
+
):
|
|
1259
|
+
return True
|
|
1260
|
+
return False
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
__all__ = ["JavaScriptTypeScriptAdapter", "JavaScriptTypeScriptParseError"]
|