code-oracle 0.1.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.
- code_oracle/__init__.py +30 -0
- code_oracle/cli.py +795 -0
- code_oracle/config.py +145 -0
- code_oracle/dataset.py +5325 -0
- code_oracle/dead_code/__init__.py +32 -0
- code_oracle/dead_code/detector.py +379 -0
- code_oracle/dead_code/entrypoints.py +333 -0
- code_oracle/dead_code/models.py +255 -0
- code_oracle/dead_code/semantics.py +416 -0
- code_oracle/decision.py +906 -0
- code_oracle/engine.py +430 -0
- code_oracle/export_onnx.py +436 -0
- code_oracle/hook.py +531 -0
- code_oracle/indexer.py +894 -0
- code_oracle/languages/__init__.py +114 -0
- code_oracle/languages/common.py +127 -0
- code_oracle/languages/go.py +395 -0
- code_oracle/languages/python.py +336 -0
- code_oracle/languages/rust.py +474 -0
- code_oracle/languages/typescript.py +775 -0
- code_oracle/linearizer.py +166 -0
- code_oracle/locator.py +301 -0
- code_oracle/models.py +237 -0
- code_oracle/perf_lint/__init__.py +38 -0
- code_oracle/perf_lint/engine.py +234 -0
- code_oracle/perf_lint/models.py +229 -0
- code_oracle/perf_lint/rules/__init__.py +31 -0
- code_oracle/perf_lint/rules/async_blocking.py +143 -0
- code_oracle/perf_lint/rules/n_plus_one.py +232 -0
- code_oracle/perf_lint/rules/nested_loops.py +137 -0
- code_oracle/perf_lint/rules/unclosed_res.py +494 -0
- code_oracle/perf_lint/visitor.py +299 -0
- code_oracle/server.py +184 -0
- code_oracle/slicer.py +225 -0
- code_oracle/symbolic.py +459 -0
- code_oracle-0.1.0.dist-info/METADATA +225 -0
- code_oracle-0.1.0.dist-info/RECORD +40 -0
- code_oracle-0.1.0.dist-info/WHEEL +4 -0
- code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
- code_oracle-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TypeScript and JavaScript AST extractor using Tree-sitter.
|
|
3
|
+
Supports .ts, .tsx, .js, .jsx, .mjs, and .cjs files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import List, Optional, Set, Tuple
|
|
7
|
+
from tree_sitter import Language, Node, Parser
|
|
8
|
+
import tree_sitter_javascript
|
|
9
|
+
import tree_sitter_typescript
|
|
10
|
+
|
|
11
|
+
from code_oracle.languages.common import extract_preceding_docstring, format_syntax_error, get_node_text
|
|
12
|
+
from code_oracle.models import CallReference, ImportReference, Parameter, Symbol
|
|
13
|
+
|
|
14
|
+
_TS_LANG = Language(tree_sitter_typescript.language_typescript())
|
|
15
|
+
_TSX_LANG = Language(tree_sitter_typescript.language_tsx())
|
|
16
|
+
_JS_LANG = Language(tree_sitter_javascript.language())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_ts_parser(file_path: str = "") -> Parser:
|
|
20
|
+
"""Get the appropriate Tree-sitter parser for TypeScript or JavaScript."""
|
|
21
|
+
lower = file_path.lower()
|
|
22
|
+
if lower.endswith(".tsx"):
|
|
23
|
+
return Parser(_TSX_LANG)
|
|
24
|
+
elif lower.endswith((".js", ".jsx", ".mjs", ".cjs")):
|
|
25
|
+
return Parser(_JS_LANG)
|
|
26
|
+
return Parser(_TS_LANG)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_typescript_syntax(source: str, file_path: str = "") -> Optional[str]:
|
|
30
|
+
"""Validate syntax of TypeScript/JavaScript source."""
|
|
31
|
+
if not source.strip():
|
|
32
|
+
return None
|
|
33
|
+
parser = get_ts_parser(file_path)
|
|
34
|
+
tree = parser.parse(source.encode("utf-8"))
|
|
35
|
+
lang_name = "TSX" if file_path.endswith(".tsx") else ("TypeScript" if file_path.endswith(".ts") else "JavaScript")
|
|
36
|
+
return format_syntax_error(tree.root_node, lang_name)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _extract_parameters(params_node: Node, source_bytes: bytes) -> List[Parameter]:
|
|
40
|
+
"""Extract parameters from formal_parameters node."""
|
|
41
|
+
params: List[Parameter] = []
|
|
42
|
+
for child in params_node.children:
|
|
43
|
+
if child.type in ("(", ")", ","):
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
if child.type == "required_parameter":
|
|
47
|
+
param_name = ""
|
|
48
|
+
annotation = None
|
|
49
|
+
default_val = None
|
|
50
|
+
has_default = False
|
|
51
|
+
is_vararg = False
|
|
52
|
+
for sc in child.children:
|
|
53
|
+
if sc.type == "rest_pattern":
|
|
54
|
+
is_vararg = True
|
|
55
|
+
for ssc in sc.children:
|
|
56
|
+
if ssc.type in ("identifier", "type_identifier"):
|
|
57
|
+
param_name = get_node_text(ssc, source_bytes)
|
|
58
|
+
elif sc.type in ("identifier", "type_identifier"):
|
|
59
|
+
param_name = get_node_text(sc, source_bytes)
|
|
60
|
+
elif sc.type == "type_annotation":
|
|
61
|
+
annotation = get_node_text(sc, source_bytes).lstrip(": ").strip()
|
|
62
|
+
elif sc.type == "=":
|
|
63
|
+
has_default = True
|
|
64
|
+
elif has_default and default_val is None and sc.type not in ("=", " "):
|
|
65
|
+
default_val = get_node_text(sc, source_bytes).strip()
|
|
66
|
+
|
|
67
|
+
params.append(
|
|
68
|
+
Parameter(
|
|
69
|
+
name=param_name,
|
|
70
|
+
annotation=annotation,
|
|
71
|
+
default=default_val,
|
|
72
|
+
has_default=has_default,
|
|
73
|
+
is_vararg=is_vararg,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
elif child.type == "optional_parameter":
|
|
78
|
+
param_name = ""
|
|
79
|
+
annotation = None
|
|
80
|
+
for sc in child.children:
|
|
81
|
+
if sc.type in ("identifier", "type_identifier"):
|
|
82
|
+
param_name = get_node_text(sc, source_bytes)
|
|
83
|
+
elif sc.type == "type_annotation":
|
|
84
|
+
annotation = get_node_text(sc, source_bytes).lstrip(": ").strip()
|
|
85
|
+
|
|
86
|
+
params.append(
|
|
87
|
+
Parameter(
|
|
88
|
+
name=param_name,
|
|
89
|
+
annotation=annotation,
|
|
90
|
+
has_default=True, # Optional parameter doesn't require caller argument
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
elif child.type == "rest_pattern":
|
|
95
|
+
param_name = ""
|
|
96
|
+
for sc in child.children:
|
|
97
|
+
if sc.type in ("identifier", "type_identifier"):
|
|
98
|
+
param_name = get_node_text(sc, source_bytes)
|
|
99
|
+
params.append(
|
|
100
|
+
Parameter(
|
|
101
|
+
name=param_name or "rest",
|
|
102
|
+
is_vararg=True,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
elif child.type == "identifier":
|
|
107
|
+
params.append(
|
|
108
|
+
Parameter(
|
|
109
|
+
name=get_node_text(child, source_bytes),
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return params
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _extract_calls(node: Node, source_bytes: bytes, caller_id: Optional[str] = None) -> List[CallReference]:
|
|
117
|
+
"""Recursively extract function and method calls inside a node."""
|
|
118
|
+
calls: List[CallReference] = []
|
|
119
|
+
|
|
120
|
+
def walk(n: Node):
|
|
121
|
+
if n.type in ("call_expression", "new_expression"):
|
|
122
|
+
callee_name = ""
|
|
123
|
+
args_count = 0
|
|
124
|
+
has_vararg = False
|
|
125
|
+
kwargs: List[str] = []
|
|
126
|
+
|
|
127
|
+
# First child or named child 'function' / 'constructor'
|
|
128
|
+
fn_node = n.child_by_field_name("function") or n.child_by_field_name("constructor")
|
|
129
|
+
if not fn_node and n.children:
|
|
130
|
+
fn_node = n.children[1] if n.type == "new_expression" and len(n.children) > 1 else n.children[0]
|
|
131
|
+
|
|
132
|
+
if fn_node:
|
|
133
|
+
callee_name = get_node_text(fn_node, source_bytes).strip()
|
|
134
|
+
|
|
135
|
+
args_node = n.child_by_field_name("arguments")
|
|
136
|
+
if args_node:
|
|
137
|
+
for arg in args_node.children:
|
|
138
|
+
if arg.type in ("(", ")", ",", "comment", "line_comment", "block_comment") or "comment" in arg.type:
|
|
139
|
+
continue
|
|
140
|
+
args_count += 1
|
|
141
|
+
if arg.type == "spread_element":
|
|
142
|
+
has_vararg = True
|
|
143
|
+
elif arg.type == "object":
|
|
144
|
+
# If passing object literal, collect top-level property keys as kwargs
|
|
145
|
+
for obj_child in arg.children:
|
|
146
|
+
if obj_child.type in ("pair", "shorthand_property_identifier_pair"):
|
|
147
|
+
key_node = obj_child.child_by_field_name("key") or (
|
|
148
|
+
obj_child.children[0] if obj_child.children else None
|
|
149
|
+
)
|
|
150
|
+
if key_node:
|
|
151
|
+
kwargs.append(get_node_text(key_node, source_bytes))
|
|
152
|
+
|
|
153
|
+
if callee_name and callee_name != "require":
|
|
154
|
+
calls.append(
|
|
155
|
+
CallReference(
|
|
156
|
+
callee=callee_name,
|
|
157
|
+
args_count=args_count,
|
|
158
|
+
kwargs=kwargs,
|
|
159
|
+
lineno=n.start_point.row + 1,
|
|
160
|
+
caller=caller_id,
|
|
161
|
+
has_vararg=has_vararg,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
for child in n.children:
|
|
166
|
+
walk(child)
|
|
167
|
+
|
|
168
|
+
walk(node)
|
|
169
|
+
return calls
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def extract_typescript_imports(source: str, file_path: str = "") -> List[ImportReference]:
|
|
173
|
+
"""Extract all import statements and require calls from TypeScript/JavaScript source."""
|
|
174
|
+
if not source.strip():
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
parser = get_ts_parser(file_path)
|
|
178
|
+
source_bytes = source.encode("utf-8")
|
|
179
|
+
tree = parser.parse(source_bytes)
|
|
180
|
+
|
|
181
|
+
imports: List[ImportReference] = []
|
|
182
|
+
|
|
183
|
+
def walk(node: Node):
|
|
184
|
+
if node.type == "import_statement":
|
|
185
|
+
source_node = node.child_by_field_name("source")
|
|
186
|
+
mod_name = ""
|
|
187
|
+
if source_node:
|
|
188
|
+
mod_name = get_node_text(source_node, source_bytes).strip("'\"`")
|
|
189
|
+
lineno = node.start_point.row + 1
|
|
190
|
+
|
|
191
|
+
# Look for import clauses
|
|
192
|
+
clause = None
|
|
193
|
+
for ch in node.children:
|
|
194
|
+
if ch.type == "import_clause":
|
|
195
|
+
clause = ch
|
|
196
|
+
break
|
|
197
|
+
|
|
198
|
+
if not clause:
|
|
199
|
+
# e.g. import './styles.css'
|
|
200
|
+
imports.append(
|
|
201
|
+
ImportReference(
|
|
202
|
+
module=mod_name,
|
|
203
|
+
name="",
|
|
204
|
+
lineno=lineno,
|
|
205
|
+
file_path=file_path,
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
for ch in clause.children:
|
|
211
|
+
if ch.type == "identifier":
|
|
212
|
+
# Default import: import Foo from './mod'
|
|
213
|
+
imports.append(
|
|
214
|
+
ImportReference(
|
|
215
|
+
module=mod_name,
|
|
216
|
+
name=get_node_text(ch, source_bytes),
|
|
217
|
+
lineno=lineno,
|
|
218
|
+
file_path=file_path,
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
elif ch.type == "named_imports":
|
|
222
|
+
for spec in ch.children:
|
|
223
|
+
if spec.type == "import_specifier":
|
|
224
|
+
name_node = spec.child_by_field_name("name")
|
|
225
|
+
alias_node = spec.child_by_field_name("alias")
|
|
226
|
+
if name_node:
|
|
227
|
+
imp_name = get_node_text(name_node, source_bytes)
|
|
228
|
+
asname = get_node_text(alias_node, source_bytes) if alias_node else None
|
|
229
|
+
imports.append(
|
|
230
|
+
ImportReference(
|
|
231
|
+
module=mod_name,
|
|
232
|
+
name=imp_name,
|
|
233
|
+
asname=asname,
|
|
234
|
+
lineno=lineno,
|
|
235
|
+
file_path=file_path,
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
elif ch.type == "namespace_import":
|
|
239
|
+
# import * as Foo from './mod'
|
|
240
|
+
alias_node = None
|
|
241
|
+
for nch in ch.children:
|
|
242
|
+
if nch.type == "identifier":
|
|
243
|
+
alias_node = nch
|
|
244
|
+
alias_name = get_node_text(alias_node, source_bytes) if alias_node else "all"
|
|
245
|
+
imports.append(
|
|
246
|
+
ImportReference(
|
|
247
|
+
module=mod_name,
|
|
248
|
+
name="*",
|
|
249
|
+
asname=alias_name,
|
|
250
|
+
lineno=lineno,
|
|
251
|
+
file_path=file_path,
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
elif node.type == "export_statement" and node.child_by_field_name("source"):
|
|
256
|
+
source_node = node.child_by_field_name("source")
|
|
257
|
+
mod_name = get_node_text(source_node, source_bytes).strip("'\"`") if source_node else ""
|
|
258
|
+
lineno = node.start_point.row + 1
|
|
259
|
+
has_star = any(ch.type == "*" for ch in node.children)
|
|
260
|
+
if has_star:
|
|
261
|
+
imports.append(
|
|
262
|
+
ImportReference(
|
|
263
|
+
module=mod_name,
|
|
264
|
+
name="*",
|
|
265
|
+
lineno=lineno,
|
|
266
|
+
file_path=file_path,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
for ch in node.children:
|
|
271
|
+
if ch.type == "export_clause":
|
|
272
|
+
for spec in ch.children:
|
|
273
|
+
if spec.type == "export_specifier":
|
|
274
|
+
name_node = spec.child_by_field_name("name") or (
|
|
275
|
+
spec.children[0] if spec.children else None
|
|
276
|
+
)
|
|
277
|
+
alias_node = spec.child_by_field_name("alias")
|
|
278
|
+
if name_node:
|
|
279
|
+
imp_name = get_node_text(name_node, source_bytes)
|
|
280
|
+
asname = get_node_text(alias_node, source_bytes) if alias_node else None
|
|
281
|
+
imports.append(
|
|
282
|
+
ImportReference(
|
|
283
|
+
module=mod_name,
|
|
284
|
+
name=imp_name,
|
|
285
|
+
asname=asname,
|
|
286
|
+
lineno=lineno,
|
|
287
|
+
file_path=file_path,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
elif node.type == "call_expression":
|
|
292
|
+
# const mod = require('./mod')
|
|
293
|
+
fn = node.child_by_field_name("function")
|
|
294
|
+
if fn and get_node_text(fn, source_bytes) == "require":
|
|
295
|
+
args = node.child_by_field_name("arguments")
|
|
296
|
+
if args and args.children:
|
|
297
|
+
for arg in args.children:
|
|
298
|
+
if arg.type == "string":
|
|
299
|
+
mod_name = get_node_text(arg, source_bytes).strip("'\"`")
|
|
300
|
+
# Determine identifier if assigned
|
|
301
|
+
parent = node.parent
|
|
302
|
+
asname = None
|
|
303
|
+
if parent and parent.type == "variable_declarator":
|
|
304
|
+
id_node = parent.child_by_field_name("name")
|
|
305
|
+
if id_node:
|
|
306
|
+
asname = get_node_text(id_node, source_bytes)
|
|
307
|
+
imports.append(
|
|
308
|
+
ImportReference(
|
|
309
|
+
module=mod_name,
|
|
310
|
+
name=asname or mod_name.split("/")[-1],
|
|
311
|
+
asname=asname,
|
|
312
|
+
lineno=node.start_point.row + 1,
|
|
313
|
+
file_path=file_path,
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
for child in node.children:
|
|
318
|
+
walk(child)
|
|
319
|
+
|
|
320
|
+
walk(tree.root_node)
|
|
321
|
+
return imports
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def extract_typescript_symbols(source: str, file_path: str = "") -> List[Symbol]:
|
|
325
|
+
"""Parse TypeScript/JavaScript source into AST and extract symbol entities."""
|
|
326
|
+
if not source.strip():
|
|
327
|
+
return []
|
|
328
|
+
|
|
329
|
+
parser = get_ts_parser(file_path)
|
|
330
|
+
source_bytes = source.encode("utf-8")
|
|
331
|
+
tree = parser.parse(source_bytes)
|
|
332
|
+
|
|
333
|
+
symbols: List[Symbol] = []
|
|
334
|
+
|
|
335
|
+
# Collect exported names from export clauses, default exports, and CommonJS
|
|
336
|
+
exported_names: Set[str] = set()
|
|
337
|
+
for top_child in tree.root_node.children:
|
|
338
|
+
if top_child.type in ("export_statement", "export_default_statement"):
|
|
339
|
+
for sc in top_child.children:
|
|
340
|
+
if sc.type == "export_clause":
|
|
341
|
+
for spec in sc.children:
|
|
342
|
+
if spec.type == "export_specifier":
|
|
343
|
+
name_node = spec.child_by_field_name("name") or (spec.children[0] if spec.children else None)
|
|
344
|
+
if name_node:
|
|
345
|
+
exported_names.add(get_node_text(name_node, source_bytes).strip())
|
|
346
|
+
elif sc.type == "identifier" and any(c.type == "default" for c in top_child.children):
|
|
347
|
+
exported_names.add(get_node_text(sc, source_bytes).strip())
|
|
348
|
+
elif top_child.type == "expression_statement":
|
|
349
|
+
for expr in top_child.children:
|
|
350
|
+
if expr.type == "assignment_expression":
|
|
351
|
+
left = expr.child_by_field_name("left")
|
|
352
|
+
right = expr.child_by_field_name("right")
|
|
353
|
+
if left and right:
|
|
354
|
+
left_text = get_node_text(left, source_bytes).strip()
|
|
355
|
+
if left_text == "module.exports" and right.type == "object":
|
|
356
|
+
for obj_child in right.children:
|
|
357
|
+
if obj_child.type in ("pair", "shorthand_property_identifier_pair"):
|
|
358
|
+
key_node = obj_child.child_by_field_name("key") or (obj_child.children[0] if obj_child.children else None)
|
|
359
|
+
if key_node:
|
|
360
|
+
exported_names.add(get_node_text(key_node, source_bytes).strip())
|
|
361
|
+
elif left_text.startswith("exports."):
|
|
362
|
+
prop = left_text.split(".", 1)[1].strip()
|
|
363
|
+
if prop:
|
|
364
|
+
exported_names.add(prop)
|
|
365
|
+
|
|
366
|
+
def process_node(node: Node, parent_qualname: Optional[str] = None):
|
|
367
|
+
# Unwrap export and ambient statements
|
|
368
|
+
target_node = node
|
|
369
|
+
is_exported = False
|
|
370
|
+
if node.type in ("export_statement", "export_default_statement"):
|
|
371
|
+
is_exported = True
|
|
372
|
+
for ch in node.children:
|
|
373
|
+
if ch.type in (
|
|
374
|
+
"function_declaration",
|
|
375
|
+
"class_declaration",
|
|
376
|
+
"interface_declaration",
|
|
377
|
+
"type_alias_declaration",
|
|
378
|
+
"enum_declaration",
|
|
379
|
+
"lexical_declaration",
|
|
380
|
+
"variable_declaration",
|
|
381
|
+
"internal_module",
|
|
382
|
+
"module",
|
|
383
|
+
):
|
|
384
|
+
target_node = ch
|
|
385
|
+
break
|
|
386
|
+
elif node.type == "ambient_declaration":
|
|
387
|
+
for ch in node.children:
|
|
388
|
+
if ch.type in (
|
|
389
|
+
"function_declaration",
|
|
390
|
+
"class_declaration",
|
|
391
|
+
"interface_declaration",
|
|
392
|
+
"type_alias_declaration",
|
|
393
|
+
"enum_declaration",
|
|
394
|
+
"lexical_declaration",
|
|
395
|
+
"variable_declaration",
|
|
396
|
+
"internal_module",
|
|
397
|
+
"module",
|
|
398
|
+
):
|
|
399
|
+
target_node = ch
|
|
400
|
+
break
|
|
401
|
+
|
|
402
|
+
if target_node.type == "expression_statement":
|
|
403
|
+
for ch in target_node.children:
|
|
404
|
+
if ch.type in ("internal_module", "module"):
|
|
405
|
+
target_node = ch
|
|
406
|
+
break
|
|
407
|
+
|
|
408
|
+
docstring = extract_preceding_docstring(node, source_bytes)
|
|
409
|
+
visibility = "public" if is_exported else "internal"
|
|
410
|
+
|
|
411
|
+
if target_node.type == "function_declaration":
|
|
412
|
+
name_node = target_node.child_by_field_name("name")
|
|
413
|
+
if not name_node:
|
|
414
|
+
if is_exported:
|
|
415
|
+
fn_name = "default"
|
|
416
|
+
else:
|
|
417
|
+
return
|
|
418
|
+
else:
|
|
419
|
+
fn_name = get_node_text(name_node, source_bytes)
|
|
420
|
+
qualname = f"{parent_qualname}.{fn_name}" if parent_qualname else fn_name
|
|
421
|
+
if not is_exported and (fn_name in exported_names or qualname in exported_names):
|
|
422
|
+
is_exported = True
|
|
423
|
+
visibility = "public"
|
|
424
|
+
sym_id = f"{file_path}::{qualname}"
|
|
425
|
+
|
|
426
|
+
is_async = any(ch.type == "async" for ch in target_node.children)
|
|
427
|
+
params_node = target_node.child_by_field_name("parameters")
|
|
428
|
+
params = _extract_parameters(params_node, source_bytes) if params_node else []
|
|
429
|
+
|
|
430
|
+
ret_node = target_node.child_by_field_name("return_type")
|
|
431
|
+
ret_type = get_node_text(ret_node, source_bytes).lstrip(": ").strip() if ret_node else None
|
|
432
|
+
|
|
433
|
+
# Calculate arity
|
|
434
|
+
pos_params = [p for p in params if not p.is_vararg]
|
|
435
|
+
min_args = len([p for p in pos_params if not p.has_default])
|
|
436
|
+
max_args = None if any(p.is_vararg for p in params) else len(pos_params)
|
|
437
|
+
|
|
438
|
+
# Calls inside body
|
|
439
|
+
body_node = target_node.child_by_field_name("body")
|
|
440
|
+
calls = _extract_calls(body_node, source_bytes, caller_id=sym_id) if body_node else []
|
|
441
|
+
|
|
442
|
+
prefix = "async function" if is_async else "function"
|
|
443
|
+
param_strs = [p.name + (f": {p.annotation}" if p.annotation else "") for p in params]
|
|
444
|
+
ret_suffix = f": {ret_type}" if ret_type else ""
|
|
445
|
+
signature = f"{prefix} {fn_name}({', '.join(param_strs)}){ret_suffix}"
|
|
446
|
+
|
|
447
|
+
symbol = Symbol(
|
|
448
|
+
name=fn_name,
|
|
449
|
+
qualname=qualname,
|
|
450
|
+
file_path=file_path,
|
|
451
|
+
kind="async_function" if is_async else "function",
|
|
452
|
+
lineno=target_node.start_point.row + 1,
|
|
453
|
+
end_lineno=target_node.end_point.row + 1,
|
|
454
|
+
signature=signature,
|
|
455
|
+
params=params,
|
|
456
|
+
min_args=min_args,
|
|
457
|
+
max_args=max_args,
|
|
458
|
+
return_type=ret_type,
|
|
459
|
+
calls=calls,
|
|
460
|
+
is_method=False,
|
|
461
|
+
is_static=False,
|
|
462
|
+
docstring=docstring,
|
|
463
|
+
is_exported=is_exported,
|
|
464
|
+
visibility=visibility,
|
|
465
|
+
)
|
|
466
|
+
symbols.append(symbol)
|
|
467
|
+
|
|
468
|
+
elif target_node.type in ("lexical_declaration", "variable_declaration"):
|
|
469
|
+
# Handle: const fn = (a, b) => ... or let fn = function(...) ...
|
|
470
|
+
is_const = any(ch.type == "const" for ch in target_node.children)
|
|
471
|
+
for decl in target_node.children:
|
|
472
|
+
if decl.type == "variable_declarator":
|
|
473
|
+
name_node = decl.child_by_field_name("name")
|
|
474
|
+
val_node = decl.child_by_field_name("value")
|
|
475
|
+
if name_node and val_node and val_node.type in ("arrow_function", "function_expression"):
|
|
476
|
+
fn_name = get_node_text(name_node, source_bytes)
|
|
477
|
+
qualname = f"{parent_qualname}.{fn_name}" if parent_qualname else fn_name
|
|
478
|
+
sym_id = f"{file_path}::{qualname}"
|
|
479
|
+
|
|
480
|
+
is_async = any(ch.type == "async" for ch in val_node.children)
|
|
481
|
+
params_node = val_node.child_by_field_name("parameters")
|
|
482
|
+
params = _extract_parameters(params_node, source_bytes) if params_node else []
|
|
483
|
+
if not params_node:
|
|
484
|
+
# Single param arrow function e.g. x => x * 2
|
|
485
|
+
param_id = val_node.child_by_field_name("parameter")
|
|
486
|
+
if param_id:
|
|
487
|
+
params = [Parameter(name=get_node_text(param_id, source_bytes))]
|
|
488
|
+
|
|
489
|
+
ret_node = val_node.child_by_field_name("return_type")
|
|
490
|
+
ret_type = get_node_text(ret_node, source_bytes).lstrip(": ").strip() if ret_node else None
|
|
491
|
+
|
|
492
|
+
pos_params = [p for p in params if not p.is_vararg]
|
|
493
|
+
min_args = len([p for p in pos_params if not p.has_default])
|
|
494
|
+
max_args = None if any(p.is_vararg for p in params) else len(pos_params)
|
|
495
|
+
|
|
496
|
+
body_node = val_node.child_by_field_name("body")
|
|
497
|
+
calls = _extract_calls(body_node, source_bytes, caller_id=sym_id) if body_node else []
|
|
498
|
+
|
|
499
|
+
param_strs = [p.name + (f": {p.annotation}" if p.annotation else "") for p in params]
|
|
500
|
+
signature = f"const {fn_name} = ({', '.join(param_strs)}) => ..."
|
|
501
|
+
|
|
502
|
+
symbol = Symbol(
|
|
503
|
+
name=fn_name,
|
|
504
|
+
qualname=qualname,
|
|
505
|
+
file_path=file_path,
|
|
506
|
+
kind="async_function" if is_async else "function",
|
|
507
|
+
lineno=target_node.start_point.row + 1,
|
|
508
|
+
end_lineno=target_node.end_point.row + 1,
|
|
509
|
+
signature=signature,
|
|
510
|
+
params=params,
|
|
511
|
+
min_args=min_args,
|
|
512
|
+
max_args=max_args,
|
|
513
|
+
return_type=ret_type,
|
|
514
|
+
calls=calls,
|
|
515
|
+
is_method=False,
|
|
516
|
+
is_static=False,
|
|
517
|
+
docstring=docstring,
|
|
518
|
+
is_exported=is_exported,
|
|
519
|
+
visibility=visibility,
|
|
520
|
+
)
|
|
521
|
+
symbols.append(symbol)
|
|
522
|
+
elif name_node:
|
|
523
|
+
# Top-level constant or variable
|
|
524
|
+
var_name = get_node_text(name_node, source_bytes)
|
|
525
|
+
if var_name and var_name.isidentifier():
|
|
526
|
+
qualname = f"{parent_qualname}.{var_name}" if parent_qualname else var_name
|
|
527
|
+
kind = "constant" if is_const else "variable"
|
|
528
|
+
sig_prefix = "const" if is_const else "let"
|
|
529
|
+
symbols.append(
|
|
530
|
+
Symbol(
|
|
531
|
+
name=var_name,
|
|
532
|
+
qualname=qualname,
|
|
533
|
+
file_path=file_path,
|
|
534
|
+
kind=kind,
|
|
535
|
+
lineno=target_node.start_point.row + 1,
|
|
536
|
+
end_lineno=target_node.end_point.row + 1,
|
|
537
|
+
signature=f"{sig_prefix} {var_name}",
|
|
538
|
+
min_args=0,
|
|
539
|
+
max_args=0,
|
|
540
|
+
docstring=docstring,
|
|
541
|
+
is_exported=is_exported,
|
|
542
|
+
visibility=visibility,
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
elif target_node.type == "class_declaration":
|
|
547
|
+
name_node = target_node.child_by_field_name("name")
|
|
548
|
+
if not name_node:
|
|
549
|
+
if is_exported:
|
|
550
|
+
class_name = "default"
|
|
551
|
+
else:
|
|
552
|
+
return
|
|
553
|
+
else:
|
|
554
|
+
class_name = get_node_text(name_node, source_bytes)
|
|
555
|
+
qualname = f"{parent_qualname}.{class_name}" if parent_qualname else class_name
|
|
556
|
+
if not is_exported and (class_name in exported_names or qualname in exported_names):
|
|
557
|
+
is_exported = True
|
|
558
|
+
visibility = "public"
|
|
559
|
+
sym_id = f"{file_path}::{qualname}"
|
|
560
|
+
|
|
561
|
+
# Base classes & interfaces
|
|
562
|
+
bases: List[str] = []
|
|
563
|
+
heritage_node = None
|
|
564
|
+
for ch in target_node.children:
|
|
565
|
+
if ch.type == "class_heritage":
|
|
566
|
+
heritage_node = ch
|
|
567
|
+
break
|
|
568
|
+
|
|
569
|
+
if heritage_node:
|
|
570
|
+
for h_child in heritage_node.children:
|
|
571
|
+
if h_child.type in ("extends_clause", "implements_clause"):
|
|
572
|
+
for base_ch in h_child.children:
|
|
573
|
+
if base_ch.type in ("identifier", "type_identifier"):
|
|
574
|
+
bases.append(get_node_text(base_ch, source_bytes))
|
|
575
|
+
|
|
576
|
+
class_body = target_node.child_by_field_name("body")
|
|
577
|
+
class_calls = _extract_calls(class_body, source_bytes, caller_id=sym_id) if class_body else []
|
|
578
|
+
|
|
579
|
+
bases_str = f" extends {', '.join(bases)}" if bases else ""
|
|
580
|
+
signature = f"class {class_name}{bases_str}"
|
|
581
|
+
|
|
582
|
+
class_sym = Symbol(
|
|
583
|
+
name=class_name,
|
|
584
|
+
qualname=qualname,
|
|
585
|
+
file_path=file_path,
|
|
586
|
+
kind="class",
|
|
587
|
+
lineno=target_node.start_point.row + 1,
|
|
588
|
+
end_lineno=target_node.end_point.row + 1,
|
|
589
|
+
signature=signature,
|
|
590
|
+
calls=class_calls,
|
|
591
|
+
bases=bases,
|
|
592
|
+
docstring=docstring,
|
|
593
|
+
is_exported=is_exported,
|
|
594
|
+
visibility=visibility,
|
|
595
|
+
)
|
|
596
|
+
symbols.append(class_sym)
|
|
597
|
+
|
|
598
|
+
# Process methods inside class_body
|
|
599
|
+
if class_body:
|
|
600
|
+
for member in class_body.children:
|
|
601
|
+
if member.type == "method_definition":
|
|
602
|
+
m_name_node = member.child_by_field_name("name")
|
|
603
|
+
if not m_name_node:
|
|
604
|
+
continue
|
|
605
|
+
m_name = get_node_text(m_name_node, source_bytes)
|
|
606
|
+
m_qualname = f"{qualname}.{m_name}"
|
|
607
|
+
m_sym_id = f"{file_path}::{m_qualname}"
|
|
608
|
+
|
|
609
|
+
is_static = any(ch.type == "static" for ch in member.children)
|
|
610
|
+
is_async = any(ch.type == "async" for ch in member.children)
|
|
611
|
+
params_node = member.child_by_field_name("parameters")
|
|
612
|
+
raw_params = _extract_parameters(params_node, source_bytes) if params_node else []
|
|
613
|
+
|
|
614
|
+
# Prepend receiver 'this' if non-static method
|
|
615
|
+
if not is_static:
|
|
616
|
+
params = [Parameter(name="this", annotation=class_name)] + raw_params
|
|
617
|
+
is_method = True
|
|
618
|
+
else:
|
|
619
|
+
params = raw_params
|
|
620
|
+
is_method = False
|
|
621
|
+
|
|
622
|
+
ret_node = member.child_by_field_name("return_type")
|
|
623
|
+
ret_type = get_node_text(ret_node, source_bytes).lstrip(": ").strip() if ret_node else None
|
|
624
|
+
|
|
625
|
+
pos_params = [p for p in raw_params if not p.is_vararg]
|
|
626
|
+
min_args = len([p for p in pos_params if not p.has_default])
|
|
627
|
+
max_args = None if any(p.is_vararg for p in raw_params) else len(pos_params)
|
|
628
|
+
|
|
629
|
+
body_node = member.child_by_field_name("body")
|
|
630
|
+
calls = _extract_calls(body_node, source_bytes, caller_id=m_sym_id) if body_node else []
|
|
631
|
+
|
|
632
|
+
m_prefix = ("static " if is_static else "") + ("async " if is_async else "")
|
|
633
|
+
param_strs = [p.name + (f": {p.annotation}" if p.annotation else "") for p in raw_params]
|
|
634
|
+
ret_suffix = f": {ret_type}" if ret_type else ""
|
|
635
|
+
m_sig = f"{m_prefix}{m_name}({', '.join(param_strs)}){ret_suffix}"
|
|
636
|
+
|
|
637
|
+
m_doc = extract_preceding_docstring(member, source_bytes)
|
|
638
|
+
m_is_priv = any(ch.type == "accessibility_modifier" and get_node_text(ch, source_bytes) == "private" for ch in member.children) or m_name.startswith("#")
|
|
639
|
+
m_is_prot = any(ch.type == "accessibility_modifier" and get_node_text(ch, source_bytes) == "protected" for ch in member.children)
|
|
640
|
+
if m_is_priv:
|
|
641
|
+
m_vis = "private"
|
|
642
|
+
m_exp = False
|
|
643
|
+
elif m_is_prot:
|
|
644
|
+
m_vis = "internal"
|
|
645
|
+
m_exp = False
|
|
646
|
+
else:
|
|
647
|
+
m_vis = "public" if is_exported else "internal"
|
|
648
|
+
m_exp = is_exported
|
|
649
|
+
|
|
650
|
+
method_sym = Symbol(
|
|
651
|
+
name=m_name,
|
|
652
|
+
qualname=m_qualname,
|
|
653
|
+
file_path=file_path,
|
|
654
|
+
kind="method" if not is_static else "function",
|
|
655
|
+
lineno=member.start_point.row + 1,
|
|
656
|
+
end_lineno=member.end_point.row + 1,
|
|
657
|
+
signature=m_sig,
|
|
658
|
+
params=params,
|
|
659
|
+
min_args=min_args,
|
|
660
|
+
max_args=max_args,
|
|
661
|
+
return_type=ret_type,
|
|
662
|
+
calls=calls,
|
|
663
|
+
is_method=is_method,
|
|
664
|
+
is_static=is_static,
|
|
665
|
+
docstring=m_doc,
|
|
666
|
+
is_exported=m_exp,
|
|
667
|
+
visibility=m_vis,
|
|
668
|
+
)
|
|
669
|
+
symbols.append(method_sym)
|
|
670
|
+
|
|
671
|
+
elif target_node.type == "interface_declaration":
|
|
672
|
+
name_node = target_node.child_by_field_name("name")
|
|
673
|
+
if name_node:
|
|
674
|
+
if_name = get_node_text(name_node, source_bytes)
|
|
675
|
+
qualname = f"{parent_qualname}.{if_name}" if parent_qualname else if_name
|
|
676
|
+
symbols.append(
|
|
677
|
+
Symbol(
|
|
678
|
+
name=if_name,
|
|
679
|
+
qualname=qualname,
|
|
680
|
+
file_path=file_path,
|
|
681
|
+
kind="interface",
|
|
682
|
+
lineno=target_node.start_point.row + 1,
|
|
683
|
+
end_lineno=target_node.end_point.row + 1,
|
|
684
|
+
signature=f"interface {if_name}",
|
|
685
|
+
docstring=docstring,
|
|
686
|
+
is_exported=is_exported,
|
|
687
|
+
visibility=visibility,
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
elif target_node.type == "type_alias_declaration":
|
|
692
|
+
name_node = target_node.child_by_field_name("name")
|
|
693
|
+
if name_node:
|
|
694
|
+
t_name = get_node_text(name_node, source_bytes)
|
|
695
|
+
qualname = f"{parent_qualname}.{t_name}" if parent_qualname else t_name
|
|
696
|
+
symbols.append(
|
|
697
|
+
Symbol(
|
|
698
|
+
name=t_name,
|
|
699
|
+
qualname=qualname,
|
|
700
|
+
file_path=file_path,
|
|
701
|
+
kind="type_alias",
|
|
702
|
+
lineno=target_node.start_point.row + 1,
|
|
703
|
+
end_lineno=target_node.end_point.row + 1,
|
|
704
|
+
signature=f"type {t_name}",
|
|
705
|
+
docstring=docstring,
|
|
706
|
+
is_exported=is_exported,
|
|
707
|
+
visibility=visibility,
|
|
708
|
+
)
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
elif target_node.type == "enum_declaration":
|
|
712
|
+
name_node = target_node.child_by_field_name("name")
|
|
713
|
+
if name_node:
|
|
714
|
+
enum_name = get_node_text(name_node, source_bytes)
|
|
715
|
+
qualname = f"{parent_qualname}.{enum_name}" if parent_qualname else enum_name
|
|
716
|
+
symbols.append(
|
|
717
|
+
Symbol(
|
|
718
|
+
name=enum_name,
|
|
719
|
+
qualname=qualname,
|
|
720
|
+
file_path=file_path,
|
|
721
|
+
kind="enum",
|
|
722
|
+
lineno=target_node.start_point.row + 1,
|
|
723
|
+
end_lineno=target_node.end_point.row + 1,
|
|
724
|
+
signature=f"enum {enum_name}",
|
|
725
|
+
min_args=0,
|
|
726
|
+
max_args=0,
|
|
727
|
+
docstring=docstring,
|
|
728
|
+
is_exported=is_exported,
|
|
729
|
+
visibility=visibility,
|
|
730
|
+
)
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
elif target_node.type in ("internal_module", "module"):
|
|
734
|
+
name_node = target_node.child_by_field_name("name")
|
|
735
|
+
ns_name = get_node_text(name_node, source_bytes).strip("'\"`") if name_node else None
|
|
736
|
+
ns_qualname = f"{parent_qualname}.{ns_name}" if parent_qualname and ns_name else ns_name
|
|
737
|
+
body_node = target_node.child_by_field_name("body") or next(
|
|
738
|
+
(c for c in target_node.children if c.type == "statement_block"), None
|
|
739
|
+
)
|
|
740
|
+
if body_node:
|
|
741
|
+
for b_child in body_node.children:
|
|
742
|
+
process_node(b_child, parent_qualname=ns_qualname)
|
|
743
|
+
|
|
744
|
+
for child in tree.root_node.children:
|
|
745
|
+
process_node(child)
|
|
746
|
+
|
|
747
|
+
# Post-process symbols that were exported via separate export clauses
|
|
748
|
+
for s in symbols:
|
|
749
|
+
if s.name in exported_names or s.qualname in exported_names:
|
|
750
|
+
s.is_exported = True
|
|
751
|
+
s.visibility = "public"
|
|
752
|
+
|
|
753
|
+
# Extract module-level calls
|
|
754
|
+
all_calls = _extract_calls(tree.root_node, source_bytes, caller_id=f"{file_path}::<module>")
|
|
755
|
+
module_calls = [
|
|
756
|
+
c for c in all_calls
|
|
757
|
+
if not any(s.lineno <= c.lineno <= s.end_lineno for s in symbols if s.kind != "module")
|
|
758
|
+
]
|
|
759
|
+
if module_calls:
|
|
760
|
+
line_count = len(source.splitlines()) or 1
|
|
761
|
+
module_sym = Symbol(
|
|
762
|
+
name="<module>",
|
|
763
|
+
qualname="<module>",
|
|
764
|
+
file_path=file_path,
|
|
765
|
+
kind="module",
|
|
766
|
+
lineno=1,
|
|
767
|
+
end_lineno=line_count,
|
|
768
|
+
signature=f"// module {file_path}",
|
|
769
|
+
calls=module_calls,
|
|
770
|
+
is_exported=True,
|
|
771
|
+
visibility="public",
|
|
772
|
+
)
|
|
773
|
+
symbols.append(module_sym)
|
|
774
|
+
|
|
775
|
+
return symbols
|