repograph-codeintel 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.
- repograph/__init__.py +3 -0
- repograph/__main__.py +3 -0
- repograph/agent_setup.py +224 -0
- repograph/cli.py +476 -0
- repograph/config.py +145 -0
- repograph/extract.py +616 -0
- repograph/impact.py +558 -0
- repograph/indexer.py +184 -0
- repograph/mcp_server.py +130 -0
- repograph/query.py +485 -0
- repograph/resolve.py +755 -0
- repograph/scanner.py +107 -0
- repograph/store.py +287 -0
- repograph/test_gate.py +169 -0
- repograph_codeintel-0.1.0.dist-info/METADATA +295 -0
- repograph_codeintel-0.1.0.dist-info/RECORD +20 -0
- repograph_codeintel-0.1.0.dist-info/WHEEL +5 -0
- repograph_codeintel-0.1.0.dist-info/entry_points.txt +2 -0
- repograph_codeintel-0.1.0.dist-info/licenses/LICENSE +202 -0
- repograph_codeintel-0.1.0.dist-info/top_level.txt +1 -0
repograph/extract.py
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
"""Tree-sitter based extraction of symbols, imports, references and routes.
|
|
2
|
+
|
|
3
|
+
Extraction is deliberately *syntactic*. It never executes repository code and
|
|
4
|
+
never guesses types. Every extracted item keeps the exact source line span so
|
|
5
|
+
later layers can attach evidence.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
|
|
13
|
+
from tree_sitter import Language, Node, Parser
|
|
14
|
+
|
|
15
|
+
import tree_sitter_javascript as tsjs
|
|
16
|
+
import tree_sitter_python as tspy
|
|
17
|
+
import tree_sitter_typescript as tsts
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Symbol:
|
|
22
|
+
name: str
|
|
23
|
+
qualified_name: str
|
|
24
|
+
kind: str # function | class | method | interface | type | enum | model
|
|
25
|
+
start_line: int
|
|
26
|
+
end_line: int
|
|
27
|
+
start_col: int
|
|
28
|
+
end_col: int
|
|
29
|
+
tags: list[str] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Import:
|
|
34
|
+
raw: str
|
|
35
|
+
module: str
|
|
36
|
+
level: int
|
|
37
|
+
names: list[tuple[str, str]] # (imported name, local binding); ("*", "*") for star
|
|
38
|
+
line: int
|
|
39
|
+
kind: str = "static" # static | require | dynamic | reexport
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Ref:
|
|
44
|
+
name: str
|
|
45
|
+
line: int
|
|
46
|
+
col: int
|
|
47
|
+
kind: str # call | attr_call | inherits | new
|
|
48
|
+
receiver: str | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class Route:
|
|
53
|
+
method: str
|
|
54
|
+
path: str
|
|
55
|
+
handler: str
|
|
56
|
+
line: int
|
|
57
|
+
framework: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ParseResult:
|
|
62
|
+
symbols: list[Symbol] = field(default_factory=list)
|
|
63
|
+
imports: list[Import] = field(default_factory=list)
|
|
64
|
+
refs: list[Ref] = field(default_factory=list)
|
|
65
|
+
routes: list[Route] = field(default_factory=list)
|
|
66
|
+
errors: list[str] = field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@lru_cache(maxsize=8)
|
|
70
|
+
def _parser(lang: str) -> Parser:
|
|
71
|
+
if lang == "python":
|
|
72
|
+
language = Language(tspy.language())
|
|
73
|
+
elif lang in ("typescript", "tsx"):
|
|
74
|
+
language = Language(tsts.language_tsx() if lang == "tsx" else tsts.language_typescript())
|
|
75
|
+
elif lang in ("javascript", "jsx"):
|
|
76
|
+
language = Language(tsjs.language())
|
|
77
|
+
else: # pragma: no cover - guarded by scanner
|
|
78
|
+
raise ValueError(f"unsupported language: {lang}")
|
|
79
|
+
return Parser(language)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _text(node: Node) -> str:
|
|
83
|
+
return node.text.decode("utf-8", "replace")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _field(node: Node, name: str) -> Node | None:
|
|
87
|
+
return node.child_by_field_name(name)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
ATTR_KINDS = {
|
|
91
|
+
"decorated_definition",
|
|
92
|
+
"export_statement",
|
|
93
|
+
"expression_statement",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# --------------------------------------------------------------------------
|
|
98
|
+
# Python
|
|
99
|
+
# --------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
PY_VERBS = {"get", "post", "put", "patch", "delete", "head", "options"}
|
|
102
|
+
PY_MODEL_BASES = {"Base", "Model", "DeclarativeBase", "Document", "models.Model"}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _string_value(node: Node) -> str | None:
|
|
106
|
+
"""First string literal inside a node, without quotes."""
|
|
107
|
+
if node.type in ("string", "string_fragment"):
|
|
108
|
+
if node.type == "string":
|
|
109
|
+
for c in node.named_children:
|
|
110
|
+
if c.type == "string_content":
|
|
111
|
+
return _text(c)
|
|
112
|
+
return _text(node).strip("'\"")
|
|
113
|
+
return _text(node)
|
|
114
|
+
for c in node.named_children:
|
|
115
|
+
v = _string_value(c)
|
|
116
|
+
if v is not None:
|
|
117
|
+
return v
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _py_routes(decorators: list[Node], handler: str) -> list[Route]:
|
|
122
|
+
routes: list[Route] = []
|
|
123
|
+
for dec in decorators:
|
|
124
|
+
target = dec.named_children[0] if dec.named_children else None
|
|
125
|
+
if target is None:
|
|
126
|
+
continue
|
|
127
|
+
if target.type == "call":
|
|
128
|
+
fn = _field(target, "function")
|
|
129
|
+
args = _field(target, "arguments")
|
|
130
|
+
if fn is None or args is None:
|
|
131
|
+
continue
|
|
132
|
+
path = None
|
|
133
|
+
method = None
|
|
134
|
+
if fn.type == "attribute":
|
|
135
|
+
attr = _field(fn, "attribute")
|
|
136
|
+
name = _text(attr) if attr else ""
|
|
137
|
+
if name in PY_VERBS:
|
|
138
|
+
method = name.upper()
|
|
139
|
+
elif name == "route":
|
|
140
|
+
method = "GET"
|
|
141
|
+
for arg in args.named_children:
|
|
142
|
+
if arg.type == "keyword_argument":
|
|
143
|
+
k = _field(arg, "name")
|
|
144
|
+
v = _field(arg, "value")
|
|
145
|
+
if k is not None and _text(k) == "methods" and v is not None:
|
|
146
|
+
methods = [_string_value(x) for x in v.named_children]
|
|
147
|
+
first = next((m.upper() for m in methods if m), None)
|
|
148
|
+
method = first or method
|
|
149
|
+
else:
|
|
150
|
+
continue
|
|
151
|
+
path = _string_value(args)
|
|
152
|
+
elif fn.type == "identifier" and _text(fn) == "route":
|
|
153
|
+
method, path = "GET", _string_value(args)
|
|
154
|
+
if method and path is not None:
|
|
155
|
+
routes.append(Route(method, path, handler, dec.start_point[0] + 1, "python-web"))
|
|
156
|
+
return routes
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _walk_python(node: Node, out: ParseResult, scope: list[str], in_class: bool = False) -> None:
|
|
160
|
+
for child in node.named_children:
|
|
161
|
+
typ = child.type
|
|
162
|
+
|
|
163
|
+
if typ == "decorated_definition":
|
|
164
|
+
decorators = [c for c in child.children if c.type == "decorator"]
|
|
165
|
+
definition = _field(child, "definition")
|
|
166
|
+
if definition is not None:
|
|
167
|
+
_walk_python_definition(definition, out, scope, decorators, in_class)
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
if typ == "import_statement":
|
|
171
|
+
_py_import(child, out)
|
|
172
|
+
continue
|
|
173
|
+
if typ == "import_from_statement":
|
|
174
|
+
_py_import_from(child, out)
|
|
175
|
+
continue
|
|
176
|
+
if typ == "call":
|
|
177
|
+
fn = _field(child, "function")
|
|
178
|
+
if fn is not None:
|
|
179
|
+
if fn.type == "identifier":
|
|
180
|
+
name = _text(fn)
|
|
181
|
+
if name == "__import__":
|
|
182
|
+
args = _field(child, "arguments")
|
|
183
|
+
module = _string_value(args) if args is not None else None
|
|
184
|
+
out.imports.append(Import(_text(child), module or "", 0, [("*", "*")],
|
|
185
|
+
child.start_point[0] + 1, "dynamic"))
|
|
186
|
+
else:
|
|
187
|
+
out.refs.append(Ref(name, child.start_point[0] + 1, fn.start_point[1], "call"))
|
|
188
|
+
elif fn.type == "attribute":
|
|
189
|
+
attr = _field(fn, "attribute")
|
|
190
|
+
if attr is not None:
|
|
191
|
+
attr_name = _text(attr)
|
|
192
|
+
if attr_name == "import_module":
|
|
193
|
+
args = _field(child, "arguments")
|
|
194
|
+
module = _string_value(args) if args is not None else None
|
|
195
|
+
out.imports.append(Import(_text(child), module or "", 0, [("*", "*")],
|
|
196
|
+
child.start_point[0] + 1, "dynamic"))
|
|
197
|
+
else:
|
|
198
|
+
obj = _field(fn, "object")
|
|
199
|
+
receiver = _text(obj) if obj is not None and obj.type == "identifier" else None
|
|
200
|
+
out.refs.append(Ref(attr_name, child.start_point[0] + 1,
|
|
201
|
+
attr.start_point[1], "attr_call", receiver))
|
|
202
|
+
_walk_python(child, out, scope, in_class)
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
_walk_python_definition(child, out, scope, [], in_class)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _walk_python_definition(
|
|
209
|
+
node: Node, out: ParseResult, scope: list[str], decorators: list[Node], in_class: bool = False
|
|
210
|
+
) -> None:
|
|
211
|
+
typ = node.type
|
|
212
|
+
if typ == "class_definition":
|
|
213
|
+
name_node = _field(node, "name")
|
|
214
|
+
if name_node is None:
|
|
215
|
+
return
|
|
216
|
+
name = _text(name_node)
|
|
217
|
+
qname = ".".join([*scope, name])
|
|
218
|
+
bases = _field(node, "superclasses")
|
|
219
|
+
tags: list[str] = []
|
|
220
|
+
if bases is not None:
|
|
221
|
+
for b in bases.named_children:
|
|
222
|
+
base = _text(b)
|
|
223
|
+
out.refs.append(Ref(base.split(".")[-1], b.start_point[0] + 1, b.start_point[1], "inherits"))
|
|
224
|
+
if base in PY_MODEL_BASES or base.endswith("Base") or base.endswith("Model"):
|
|
225
|
+
tags.append("db_model")
|
|
226
|
+
body = _field(node, "body")
|
|
227
|
+
out.symbols.append(
|
|
228
|
+
Symbol(name, qname, "class", node.start_point[0] + 1, node.end_point[0] + 1,
|
|
229
|
+
node.start_point[1], node.end_point[1], tags)
|
|
230
|
+
)
|
|
231
|
+
if body is not None:
|
|
232
|
+
for c in body.named_children:
|
|
233
|
+
if c.type == "decorated_definition":
|
|
234
|
+
decs = [x for x in c.children if x.type == "decorator"]
|
|
235
|
+
d = _field(c, "definition")
|
|
236
|
+
if d is not None:
|
|
237
|
+
_walk_python_definition(d, out, [*scope, name], decs, True)
|
|
238
|
+
elif c.type in ("function_definition", "class_definition"):
|
|
239
|
+
_walk_python_definition(c, out, [*scope, name], [], True)
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
if typ in ("function_definition", "lambda"):
|
|
243
|
+
name_node = _field(node, "name")
|
|
244
|
+
if name_node is None:
|
|
245
|
+
return
|
|
246
|
+
name = _text(name_node)
|
|
247
|
+
qname = ".".join([*scope, name])
|
|
248
|
+
out.symbols.append(
|
|
249
|
+
Symbol(name, qname, "method" if in_class else "function", node.start_point[0] + 1,
|
|
250
|
+
node.end_point[0] + 1, node.start_point[1], node.end_point[1])
|
|
251
|
+
)
|
|
252
|
+
out.routes.extend(_py_routes(decorators, name))
|
|
253
|
+
body = _field(node, "body")
|
|
254
|
+
if body is not None:
|
|
255
|
+
_walk_python(body, out, [*scope, name], False)
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
_walk_python(node, out, scope)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _py_import(node: Node, out: ParseResult) -> None:
|
|
262
|
+
for name_node in [c for c in node.children if c.type in ("dotted_name", "aliased_import")]:
|
|
263
|
+
if name_node.type == "aliased_import":
|
|
264
|
+
mod = _field(name_node, "name")
|
|
265
|
+
alias = _field(name_node, "alias")
|
|
266
|
+
if mod is None:
|
|
267
|
+
continue
|
|
268
|
+
module = _text(mod)
|
|
269
|
+
local = _text(alias) if alias is not None else module.split(".")[0]
|
|
270
|
+
else:
|
|
271
|
+
module = _text(name_node)
|
|
272
|
+
local = module.split(".")[0]
|
|
273
|
+
out.imports.append(
|
|
274
|
+
Import(_text(node), module, 0, [(module, local)], node.start_point[0] + 1)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _py_import_from(node: Node, out: ParseResult) -> None:
|
|
279
|
+
module_node = _field(node, "module_name")
|
|
280
|
+
module = ""
|
|
281
|
+
level = 0
|
|
282
|
+
if module_node is not None:
|
|
283
|
+
if module_node.type == "relative_import":
|
|
284
|
+
prefix = next((c for c in module_node.named_children if c.type == "import_prefix"), None)
|
|
285
|
+
level = _text(prefix).count(".") if prefix is not None else 1
|
|
286
|
+
dotted = next((c for c in module_node.named_children if c.type == "dotted_name"), None)
|
|
287
|
+
module = _text(dotted) if dotted is not None else ""
|
|
288
|
+
else:
|
|
289
|
+
module = _text(module_node)
|
|
290
|
+
|
|
291
|
+
names: list[tuple[str, str]] = []
|
|
292
|
+
|
|
293
|
+
def collect(n: Node) -> None:
|
|
294
|
+
for child in n.named_children:
|
|
295
|
+
if module_node is not None and (child.start_byte, child.end_byte) == (
|
|
296
|
+
module_node.start_byte,
|
|
297
|
+
module_node.end_byte,
|
|
298
|
+
):
|
|
299
|
+
continue
|
|
300
|
+
if child.type == "dotted_name":
|
|
301
|
+
name = _text(child)
|
|
302
|
+
names.append((name, name.split(".")[0]))
|
|
303
|
+
elif child.type == "aliased_import":
|
|
304
|
+
name_node = _field(child, "name")
|
|
305
|
+
alias = _field(child, "alias")
|
|
306
|
+
if name_node is not None:
|
|
307
|
+
names.append((_text(name_node), _text(alias) if alias is not None
|
|
308
|
+
else _text(name_node).split(".")[0]))
|
|
309
|
+
elif child.type == "wildcard_import":
|
|
310
|
+
names.append(("*", "*"))
|
|
311
|
+
else:
|
|
312
|
+
collect(child)
|
|
313
|
+
|
|
314
|
+
collect(node)
|
|
315
|
+
out.imports.append(Import(_text(node), module, level, names, node.start_point[0] + 1))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# --------------------------------------------------------------------------
|
|
319
|
+
# TypeScript / JavaScript
|
|
320
|
+
# --------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
TS_HTTP = {"Get", "Post", "Put", "Patch", "Delete", "Head", "Options"}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _ts_class_prefix(node: Node) -> str:
|
|
326
|
+
prefix = ""
|
|
327
|
+
for c in node.children:
|
|
328
|
+
if c.type == "decorator":
|
|
329
|
+
target = c.named_children[0] if c.named_children else None
|
|
330
|
+
if target is not None and target.type == "call_expression":
|
|
331
|
+
fn = _field(target, "function")
|
|
332
|
+
args = _field(target, "arguments")
|
|
333
|
+
if fn is not None and _text(fn) == "Controller" and args is not None:
|
|
334
|
+
prefix = _string_value(args) or ""
|
|
335
|
+
return prefix
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _ts_routes_from(decorators: list[Node], handler: str, prefix: str) -> list[Route]:
|
|
339
|
+
routes: list[Route] = []
|
|
340
|
+
for c in decorators:
|
|
341
|
+
target = c.named_children[0] if c.named_children else None
|
|
342
|
+
if target is None or target.type != "call_expression":
|
|
343
|
+
continue
|
|
344
|
+
fn = _field(target, "function")
|
|
345
|
+
args = _field(target, "arguments")
|
|
346
|
+
if fn is None or args is None:
|
|
347
|
+
continue
|
|
348
|
+
name = _text(fn)
|
|
349
|
+
if name not in TS_HTTP and name != "Route":
|
|
350
|
+
continue
|
|
351
|
+
path = _string_value(args) or ""
|
|
352
|
+
method = "GET" if name == "Route" else name.upper()
|
|
353
|
+
routes.append(Route(method, (prefix + path) if path else prefix, handler,
|
|
354
|
+
c.start_point[0] + 1, "typescript-decorator"))
|
|
355
|
+
return routes
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _ts_import(node: Node, out: ParseResult, kind: str = "static") -> None:
|
|
359
|
+
source = _field(node, "source")
|
|
360
|
+
if source is None:
|
|
361
|
+
return
|
|
362
|
+
module = _string_value(source)
|
|
363
|
+
if module is None:
|
|
364
|
+
return
|
|
365
|
+
names: list[tuple[str, str]] = []
|
|
366
|
+
clause = next((c for c in node.named_children if c.type == "import_clause"), None)
|
|
367
|
+
if clause is not None:
|
|
368
|
+
for c in clause.named_children:
|
|
369
|
+
if c.type == "identifier":
|
|
370
|
+
names.append(("default", _text(c)))
|
|
371
|
+
elif c.type == "namespace_import":
|
|
372
|
+
local = next((x for x in c.named_children if x.type == "identifier"), None)
|
|
373
|
+
if local is not None:
|
|
374
|
+
names.append(("*", _text(local)))
|
|
375
|
+
elif c.type == "named_imports":
|
|
376
|
+
for spec in c.named_children:
|
|
377
|
+
if spec.type != "import_specifier":
|
|
378
|
+
continue
|
|
379
|
+
n = _field(spec, "name")
|
|
380
|
+
a = _field(spec, "alias")
|
|
381
|
+
if n is not None:
|
|
382
|
+
names.append((_text(n), _text(a) if a is not None else _text(n)))
|
|
383
|
+
if not names:
|
|
384
|
+
names = [("*", "*")]
|
|
385
|
+
out.imports.append(Import(_text(node), module, 0, names, node.start_point[0] + 1, kind))
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _ts_require(node: Node, out: ParseResult) -> None:
|
|
389
|
+
declarator = node.parent
|
|
390
|
+
if declarator is None or declarator.type != "variable_declarator":
|
|
391
|
+
return
|
|
392
|
+
value = _field(declarator, "value")
|
|
393
|
+
if value is None:
|
|
394
|
+
return
|
|
395
|
+
call = value
|
|
396
|
+
if call.type != "call_expression":
|
|
397
|
+
return
|
|
398
|
+
fn = _field(call, "function")
|
|
399
|
+
if fn is None or _text(fn) != "require":
|
|
400
|
+
return
|
|
401
|
+
args = _field(call, "arguments")
|
|
402
|
+
module = _string_value(args) if args is not None else None
|
|
403
|
+
if module is None:
|
|
404
|
+
return
|
|
405
|
+
name_node = _field(declarator, "name")
|
|
406
|
+
names: list[tuple[str, str]] = []
|
|
407
|
+
if name_node is not None:
|
|
408
|
+
if name_node.type == "identifier":
|
|
409
|
+
names.append(("default", _text(name_node)))
|
|
410
|
+
elif name_node.type == "object_pattern":
|
|
411
|
+
for p in name_node.named_children:
|
|
412
|
+
if p.type == "shorthand_property_identifier_pattern":
|
|
413
|
+
names.append((_text(p), _text(p)))
|
|
414
|
+
elif p.type == "pair_pattern":
|
|
415
|
+
k = _field(p, "key")
|
|
416
|
+
v = _field(p, "value")
|
|
417
|
+
if k is not None and v is not None:
|
|
418
|
+
names.append((_text(k), _text(v)))
|
|
419
|
+
if not names:
|
|
420
|
+
names = [("default", module.split("/")[-1])]
|
|
421
|
+
out.imports.append(Import(_text(node), module, 0, names, node.start_point[0] + 1, "require"))
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _ts_walk(node: Node, out: ParseResult, scope: list[str]) -> None:
|
|
425
|
+
for child in node.named_children:
|
|
426
|
+
typ = child.type
|
|
427
|
+
|
|
428
|
+
if typ == "import_statement":
|
|
429
|
+
_ts_import(child, out)
|
|
430
|
+
continue
|
|
431
|
+
if typ == "export_statement":
|
|
432
|
+
if _field(child, "source") is not None:
|
|
433
|
+
_ts_import(child, out, "reexport")
|
|
434
|
+
decl = _field(child, "declaration")
|
|
435
|
+
if decl is not None:
|
|
436
|
+
_ts_definition(decl, out, scope)
|
|
437
|
+
continue
|
|
438
|
+
if typ == "call_expression":
|
|
439
|
+
fn = _field(child, "function")
|
|
440
|
+
if fn is not None:
|
|
441
|
+
if fn.type == "import":
|
|
442
|
+
args = _field(child, "arguments")
|
|
443
|
+
module = _string_value(args) if args is not None else None
|
|
444
|
+
out.imports.append(Import(_text(child), module or "", 0, [("*", "*")],
|
|
445
|
+
child.start_point[0] + 1, "dynamic"))
|
|
446
|
+
elif fn.type == "identifier":
|
|
447
|
+
name = _text(fn)
|
|
448
|
+
if name == "require":
|
|
449
|
+
_ts_require(child, out)
|
|
450
|
+
elif name == "import":
|
|
451
|
+
args = _field(child, "arguments")
|
|
452
|
+
module = _string_value(args) if args is not None else None
|
|
453
|
+
out.imports.append(Import(_text(child), module or "", 0, [("*", "*")],
|
|
454
|
+
child.start_point[0] + 1, "dynamic"))
|
|
455
|
+
else:
|
|
456
|
+
out.refs.append(Ref(name, child.start_point[0] + 1, fn.start_point[1], "call"))
|
|
457
|
+
elif fn.type == "member_expression":
|
|
458
|
+
prop = _field(fn, "property")
|
|
459
|
+
obj = _field(fn, "object")
|
|
460
|
+
receiver = None
|
|
461
|
+
if obj is not None:
|
|
462
|
+
if obj.type in ("identifier", "this"):
|
|
463
|
+
receiver = _text(obj)
|
|
464
|
+
elif obj.type == "new_expression":
|
|
465
|
+
ctor = _field(obj, "constructor")
|
|
466
|
+
if ctor is not None:
|
|
467
|
+
receiver = _text(ctor).split(".")[-1]
|
|
468
|
+
if prop is not None:
|
|
469
|
+
out.refs.append(Ref(_text(prop), child.start_point[0] + 1,
|
|
470
|
+
prop.start_point[1], "attr_call", receiver))
|
|
471
|
+
_ts_walk(child, out, scope)
|
|
472
|
+
continue
|
|
473
|
+
if typ == "new_expression":
|
|
474
|
+
ctor = _field(child, "constructor")
|
|
475
|
+
if ctor is not None:
|
|
476
|
+
out.refs.append(Ref(_text(ctor).split(".")[-1], child.start_point[0] + 1,
|
|
477
|
+
ctor.start_point[1], "new"))
|
|
478
|
+
_ts_walk(child, out, scope)
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
_ts_definition(child, out, scope)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _ts_definition(node: Node, out: ParseResult, scope: list[str]) -> None:
|
|
485
|
+
typ = node.type
|
|
486
|
+
if typ in ("function_declaration", "generator_function_declaration"):
|
|
487
|
+
name_node = _field(node, "name")
|
|
488
|
+
if name_node is None:
|
|
489
|
+
return
|
|
490
|
+
name = _text(name_node)
|
|
491
|
+
out.symbols.append(
|
|
492
|
+
Symbol(name, ".".join([*scope, name]), "function", node.start_point[0] + 1,
|
|
493
|
+
node.end_point[0] + 1, node.start_point[1], node.end_point[1])
|
|
494
|
+
)
|
|
495
|
+
body = _field(node, "body")
|
|
496
|
+
if body is not None:
|
|
497
|
+
_ts_walk(body, out, [*scope, name])
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
if typ in ("lexical_declaration", "variable_declaration"):
|
|
501
|
+
for d in node.named_children:
|
|
502
|
+
if d.type != "variable_declarator":
|
|
503
|
+
continue
|
|
504
|
+
name_node = _field(d, "name")
|
|
505
|
+
value = _field(d, "value")
|
|
506
|
+
if name_node is None or value is None:
|
|
507
|
+
continue
|
|
508
|
+
if value.type not in ("arrow_function", "function_expression", "function"):
|
|
509
|
+
_ts_walk(d, out, scope)
|
|
510
|
+
continue
|
|
511
|
+
name = _text(name_node)
|
|
512
|
+
out.symbols.append(
|
|
513
|
+
Symbol(name, ".".join([*scope, name]), "function", node.start_point[0] + 1,
|
|
514
|
+
node.end_point[0] + 1, node.start_point[1], node.end_point[1])
|
|
515
|
+
)
|
|
516
|
+
body = _field(value, "body")
|
|
517
|
+
if body is not None:
|
|
518
|
+
_ts_walk(body, out, [*scope, name])
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
if typ == "class_declaration":
|
|
522
|
+
name_node = _field(node, "name")
|
|
523
|
+
if name_node is None:
|
|
524
|
+
return
|
|
525
|
+
name = _text(name_node)
|
|
526
|
+
prefix = _ts_class_prefix(node)
|
|
527
|
+
heritage = next((c for c in node.named_children if c.type == "class_heritage"), None)
|
|
528
|
+
if heritage is not None:
|
|
529
|
+
for c in heritage.named_children:
|
|
530
|
+
if c.type == "extends_clause":
|
|
531
|
+
for v in c.named_children:
|
|
532
|
+
out.refs.append(Ref(_text(v).split(".")[-1], v.start_point[0] + 1,
|
|
533
|
+
v.start_point[1], "inherits"))
|
|
534
|
+
out.symbols.append(
|
|
535
|
+
Symbol(name, ".".join([*scope, name]), "class", node.start_point[0] + 1,
|
|
536
|
+
node.end_point[0] + 1, node.start_point[1], node.end_point[1])
|
|
537
|
+
)
|
|
538
|
+
body = _field(node, "body")
|
|
539
|
+
if body is not None:
|
|
540
|
+
pending: list[Node] = []
|
|
541
|
+
for c in body.named_children:
|
|
542
|
+
if c.type == "decorator":
|
|
543
|
+
pending.append(c)
|
|
544
|
+
continue
|
|
545
|
+
if c.type == "method_definition":
|
|
546
|
+
mn = _field(c, "name")
|
|
547
|
+
if mn is None:
|
|
548
|
+
continue
|
|
549
|
+
mname = _text(mn)
|
|
550
|
+
out.symbols.append(
|
|
551
|
+
Symbol(mname, f"{'.'.join([*scope, name])}.{mname}", "method",
|
|
552
|
+
c.start_point[0] + 1, c.end_point[0] + 1,
|
|
553
|
+
c.start_point[1], c.end_point[1])
|
|
554
|
+
)
|
|
555
|
+
out.routes.extend(_ts_routes_from(pending, f"{name}.{mname}", prefix))
|
|
556
|
+
pending = []
|
|
557
|
+
mb = _field(c, "body")
|
|
558
|
+
if mb is not None:
|
|
559
|
+
_ts_walk(mb, out, [*scope, name, mname])
|
|
560
|
+
elif c.type == "class_declaration":
|
|
561
|
+
_ts_definition(c, out, [*scope, name])
|
|
562
|
+
elif c.type == "method_signature":
|
|
563
|
+
pending = []
|
|
564
|
+
out.routes.extend(_ts_routes_from([c for c in node.children if c.type == "decorator"],
|
|
565
|
+
name, prefix))
|
|
566
|
+
return
|
|
567
|
+
|
|
568
|
+
if typ in ("interface_declaration", "type_alias_declaration", "enum_declaration"):
|
|
569
|
+
name_node = _field(node, "name")
|
|
570
|
+
if name_node is None:
|
|
571
|
+
return
|
|
572
|
+
kind = {"interface_declaration": "interface", "type_alias_declaration": "type",
|
|
573
|
+
"enum_declaration": "enum"}[typ]
|
|
574
|
+
name = _text(name_node)
|
|
575
|
+
out.symbols.append(
|
|
576
|
+
Symbol(name, ".".join([*scope, name]), kind, node.start_point[0] + 1,
|
|
577
|
+
node.end_point[0] + 1, node.start_point[1], node.end_point[1])
|
|
578
|
+
)
|
|
579
|
+
return
|
|
580
|
+
|
|
581
|
+
# Container nodes (blocks, statements, arguments) are walked so calls and
|
|
582
|
+
# requires nested inside them are not missed.
|
|
583
|
+
if typ not in ATTR_KINDS:
|
|
584
|
+
_ts_walk(node, out, scope)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
# --------------------------------------------------------------------------
|
|
588
|
+
# Prisma
|
|
589
|
+
# --------------------------------------------------------------------------
|
|
590
|
+
|
|
591
|
+
def _parse_prisma(text: str) -> ParseResult:
|
|
592
|
+
import re
|
|
593
|
+
|
|
594
|
+
out = ParseResult()
|
|
595
|
+
for i, line in enumerate(text.splitlines(), start=1):
|
|
596
|
+
m = re.match(r"\s*model\s+([A-Za-z_][A-Za-z0-9_]*)", line)
|
|
597
|
+
if m:
|
|
598
|
+
out.symbols.append(Symbol(m.group(1), m.group(1), "model", i, i, 0, len(line), ["db_model"]))
|
|
599
|
+
return out
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def extract(rel_path: str, lang: str, source: bytes) -> ParseResult:
|
|
603
|
+
if lang == "prisma":
|
|
604
|
+
return _parse_prisma(source.decode("utf-8", "replace"))
|
|
605
|
+
if lang == "sql":
|
|
606
|
+
return ParseResult()
|
|
607
|
+
parser = _parser(lang)
|
|
608
|
+
tree = parser.parse(source)
|
|
609
|
+
out = ParseResult()
|
|
610
|
+
if tree.root_node.has_error:
|
|
611
|
+
out.errors.append("syntax errors present; extraction is partial")
|
|
612
|
+
if lang == "python":
|
|
613
|
+
_walk_python(tree.root_node, out, [])
|
|
614
|
+
else:
|
|
615
|
+
_ts_walk(tree.root_node, out, [])
|
|
616
|
+
return out
|