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.
Files changed (43) hide show
  1. codemble/__init__.py +4 -0
  2. codemble/adapters/__init__.py +38 -0
  3. codemble/adapters/base.py +199 -0
  4. codemble/adapters/discovery.py +190 -0
  5. codemble/adapters/project.py +206 -0
  6. codemble/adapters/python_ast.py +766 -0
  7. codemble/adapters/typescript_tree_sitter.py +1263 -0
  8. codemble/checks/__init__.py +19 -0
  9. codemble/checks/service.py +380 -0
  10. codemble/cli.py +161 -0
  11. codemble/graph/__init__.py +17 -0
  12. codemble/graph/finalize.py +91 -0
  13. codemble/graph/layout.py +132 -0
  14. codemble/lens/__init__.py +18 -0
  15. codemble/lens/javascript_typescript.py +82 -0
  16. codemble/lens/python.py +71 -0
  17. codemble/llm/__init__.py +6 -0
  18. codemble/llm/providers.py +137 -0
  19. codemble/llm/study.py +439 -0
  20. codemble/progress/__init__.py +9 -0
  21. codemble/progress/store.py +160 -0
  22. codemble/server/__init__.py +6 -0
  23. codemble/server/app.py +273 -0
  24. codemble/server/runtime.py +68 -0
  25. codemble/web_dist/assets/index-DOBVd_-M.css +1 -0
  26. codemble/web_dist/assets/index-cIG6GGIB.js +5238 -0
  27. codemble/web_dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  28. codemble/web_dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  29. codemble/web_dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  30. codemble/web_dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  31. codemble/web_dist/assets/shippori-mincho-latin-500-normal-C-QwvIb3.woff +0 -0
  32. codemble/web_dist/assets/shippori-mincho-latin-500-normal-XI1O8euf.woff2 +0 -0
  33. codemble/web_dist/assets/shippori-mincho-latin-700-normal-CkoCYOiI.woff +0 -0
  34. codemble/web_dist/assets/shippori-mincho-latin-700-normal-DHcmzUO5.woff2 +0 -0
  35. codemble/web_dist/assets/zen-kaku-gothic-new-latin-400-normal-BEdayliK.woff2 +0 -0
  36. codemble/web_dist/assets/zen-kaku-gothic-new-latin-400-normal-CPSmNJAU.woff +0 -0
  37. codemble/web_dist/index.html +18 -0
  38. codemble-0.3.0.dist-info/METADATA +417 -0
  39. codemble-0.3.0.dist-info/RECORD +43 -0
  40. codemble-0.3.0.dist-info/WHEEL +4 -0
  41. codemble-0.3.0.dist-info/entry_points.txt +2 -0
  42. codemble-0.3.0.dist-info/licenses/LICENSE +202 -0
  43. 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"]