rust-analyzer-db 0.2.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.
- rust_analyzer/__init__.py +1 -0
- rust_analyzer/cli.py +663 -0
- rust_analyzer/db.py +596 -0
- rust_analyzer/exceptions.py +41 -0
- rust_analyzer/extractor.py +640 -0
- rust_analyzer/graph.py +297 -0
- rust_analyzer/logging.py +52 -0
- rust_analyzer/mcp_server.py +516 -0
- rust_analyzer/static/style.css +573 -0
- rust_analyzer/static/vis-network.min.js +34 -0
- rust_analyzer/templates/api_surface.html +85 -0
- rust_analyzer/templates/base.html +82 -0
- rust_analyzer/templates/complexity.html +108 -0
- rust_analyzer/templates/dashboard.html +170 -0
- rust_analyzer/templates/deps.html +63 -0
- rust_analyzer/templates/graph.html +99 -0
- rust_analyzer/templates/items.html +117 -0
- rust_analyzer/templates/search.html +91 -0
- rust_analyzer/web.py +392 -0
- rust_analyzer_db-0.2.0.dist-info/METADATA +302 -0
- rust_analyzer_db-0.2.0.dist-info/RECORD +24 -0
- rust_analyzer_db-0.2.0.dist-info/WHEEL +4 -0
- rust_analyzer_db-0.2.0.dist-info/entry_points.txt +2 -0
- rust_analyzer_db-0.2.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
"""Extracts Rust code entities using tree-sitter and computes static metrics.
|
|
2
|
+
|
|
3
|
+
Extracts: structs, enums, traits, impls, functions/methods, consts, statics,
|
|
4
|
+
mods, macros, type aliases, unions, use declarations, and extern crate
|
|
5
|
+
declarations. Also computes cyclomatic complexity, line counts, and extracts
|
|
6
|
+
generic parameters and lifetime annotations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import tree_sitter_rust as tsrust
|
|
16
|
+
from tree_sitter import Language, Node, Parser
|
|
17
|
+
|
|
18
|
+
from .exceptions import ParseError
|
|
19
|
+
from .logging import get_logger
|
|
20
|
+
|
|
21
|
+
log = get_logger("extractor")
|
|
22
|
+
|
|
23
|
+
RUST_LANGUAGE = Language(tsrust.language())
|
|
24
|
+
|
|
25
|
+
# tree-sitter node types that represent top-level Rust items
|
|
26
|
+
TOP_LEVEL_KINDS: dict[str, str] = {
|
|
27
|
+
"struct_item": "struct",
|
|
28
|
+
"enum_item": "enum",
|
|
29
|
+
"trait_item": "trait",
|
|
30
|
+
"impl_item": "impl",
|
|
31
|
+
"function_item": "function",
|
|
32
|
+
"function_signature_item": "function_sig",
|
|
33
|
+
"const_item": "const",
|
|
34
|
+
"static_item": "static",
|
|
35
|
+
"mod_item": "mod",
|
|
36
|
+
"macro_definition": "macro",
|
|
37
|
+
"type_item": "type_alias",
|
|
38
|
+
"union_item": "union",
|
|
39
|
+
"use_declaration": "use",
|
|
40
|
+
"extern_crate_declaration": "extern_crate",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
COMMENT_TYPES = frozenset({"line_comment", "block_comment"})
|
|
44
|
+
ATTRIBUTE_TYPES = frozenset({"attribute_item", "inner_attribute_item"})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Data classes
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class GenericParam:
|
|
53
|
+
"""A single generic type parameter."""
|
|
54
|
+
|
|
55
|
+
name: str
|
|
56
|
+
bounds: list[str] = field(default_factory=list)
|
|
57
|
+
default: Optional[str] = None
|
|
58
|
+
|
|
59
|
+
def to_signature(self) -> str:
|
|
60
|
+
parts = [self.name]
|
|
61
|
+
if self.bounds:
|
|
62
|
+
parts.append(f": {', '.join(self.bounds)}")
|
|
63
|
+
if self.default:
|
|
64
|
+
parts.append(f"= {self.default}")
|
|
65
|
+
return "".join(parts)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class LifetimeParam:
|
|
70
|
+
"""A lifetime parameter (e.g. ``'a``)."""
|
|
71
|
+
|
|
72
|
+
name: str
|
|
73
|
+
bounds: list[str] = field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class ComplexityMetrics:
|
|
78
|
+
"""Static metrics computed from a function/method body."""
|
|
79
|
+
|
|
80
|
+
cyclomatic: int = 1
|
|
81
|
+
lines_of_code: int = 0
|
|
82
|
+
cognitive: int = 0
|
|
83
|
+
nesting_depth: int = 0
|
|
84
|
+
num_branches: int = 0
|
|
85
|
+
num_function_calls: int = 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class ExtractedItem:
|
|
90
|
+
"""A single extracted code entity."""
|
|
91
|
+
|
|
92
|
+
kind: str
|
|
93
|
+
name: str
|
|
94
|
+
start_line: int
|
|
95
|
+
end_line: int
|
|
96
|
+
source: str
|
|
97
|
+
target: Optional[str] = None
|
|
98
|
+
trait_name: Optional[str] = None
|
|
99
|
+
visibility: Optional[str] = None
|
|
100
|
+
signature: Optional[str] = None
|
|
101
|
+
doc: Optional[str] = None
|
|
102
|
+
attributes: Optional[str] = None
|
|
103
|
+
children: list[ExtractedItem] = field(default_factory=list)
|
|
104
|
+
body_node: Optional[Node] = None # transient, not persisted
|
|
105
|
+
# new fields
|
|
106
|
+
generic_params: list[GenericParam] = field(default_factory=list)
|
|
107
|
+
lifetime_params: list[LifetimeParam] = field(default_factory=list)
|
|
108
|
+
complexity: Optional[ComplexityMetrics] = None
|
|
109
|
+
is_pub: bool = False
|
|
110
|
+
is_const_fn: bool = False
|
|
111
|
+
is_async: bool = False
|
|
112
|
+
is_unsafe: bool = False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class UseDeclaration:
|
|
117
|
+
"""A ``use`` statement."""
|
|
118
|
+
|
|
119
|
+
path: str
|
|
120
|
+
alias: Optional[str] = None
|
|
121
|
+
is_glob: bool = False
|
|
122
|
+
start_line: int = 0
|
|
123
|
+
end_line: int = 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class ExternCrate:
|
|
128
|
+
"""An ``extern crate`` declaration."""
|
|
129
|
+
|
|
130
|
+
name: str
|
|
131
|
+
alias: Optional[str] = None
|
|
132
|
+
start_line: int = 0
|
|
133
|
+
end_line: int = 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class CallEdge:
|
|
138
|
+
"""A detected function/method call in a body."""
|
|
139
|
+
|
|
140
|
+
callee_name: str
|
|
141
|
+
receiver: Optional[str]
|
|
142
|
+
line: int
|
|
143
|
+
is_method_call: bool
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# Helpers
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
def _text(node: Optional[Node], src: bytes) -> Optional[str]:
|
|
151
|
+
if node is None:
|
|
152
|
+
return None
|
|
153
|
+
return src[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _leading_metadata(node: Node, src: bytes) -> tuple[Optional[str], Optional[str]]:
|
|
157
|
+
"""Walk backwards over preceding sibling comment/attribute nodes."""
|
|
158
|
+
doc_lines: list[str] = []
|
|
159
|
+
attr_lines: list[str] = []
|
|
160
|
+
collected: list[Node] = []
|
|
161
|
+
sib = node.prev_sibling
|
|
162
|
+
while sib is not None and (sib.type in COMMENT_TYPES or sib.type in ATTRIBUTE_TYPES):
|
|
163
|
+
collected.append(sib)
|
|
164
|
+
sib = sib.prev_sibling
|
|
165
|
+
collected.reverse()
|
|
166
|
+
for n in collected:
|
|
167
|
+
text = _text(n, src)
|
|
168
|
+
if text is None:
|
|
169
|
+
continue
|
|
170
|
+
if n.type in COMMENT_TYPES:
|
|
171
|
+
if text.startswith("///") or text.startswith("//!") or text.startswith("/**"):
|
|
172
|
+
doc_lines.append(text)
|
|
173
|
+
elif n.type in ATTRIBUTE_TYPES:
|
|
174
|
+
attr_lines.append(text)
|
|
175
|
+
doc = "\n".join(doc_lines) if doc_lines else None
|
|
176
|
+
attrs = "\n".join(attr_lines) if attr_lines else None
|
|
177
|
+
return doc, attrs
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _visibility(node: Node, src: bytes) -> Optional[str]:
|
|
181
|
+
for child in node.children:
|
|
182
|
+
if child.type == "visibility_modifier":
|
|
183
|
+
return _text(child, src)
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _function_signature(node: Node, src: bytes) -> str:
|
|
188
|
+
body = node.child_by_field_name("body")
|
|
189
|
+
end = body.start_byte if body is not None else node.end_byte
|
|
190
|
+
sig = src[node.start_byte:end].decode("utf-8", errors="replace")
|
|
191
|
+
return sig.strip().rstrip("{").strip()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _has_modifier(node: Node, modifier: str) -> bool:
|
|
195
|
+
"""Check if a function node has a modifier like 'async', 'const', 'unsafe'."""
|
|
196
|
+
for child in node.children:
|
|
197
|
+
if child.type == modifier:
|
|
198
|
+
return True
|
|
199
|
+
if child.type == "function_modifiers":
|
|
200
|
+
mod_text = _text(child, None)
|
|
201
|
+
if mod_text and modifier in mod_text:
|
|
202
|
+
return True
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
# Generic / lifetime extraction
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
def _extract_generic_params(node: Node, src: bytes) -> tuple[list[GenericParam], list[LifetimeParam]]:
|
|
211
|
+
"""Extract generic type parameters and lifetime parameters from a node's
|
|
212
|
+
optional ``type_parameters`` child (the ``<...>`` block)."""
|
|
213
|
+
generics: list[GenericParam] = []
|
|
214
|
+
lifetimes: list[LifetimeParam] = []
|
|
215
|
+
|
|
216
|
+
params_node = node.child_by_field_name("type_parameters")
|
|
217
|
+
if params_node is None:
|
|
218
|
+
return generics, lifetimes
|
|
219
|
+
|
|
220
|
+
for child in params_node.named_children:
|
|
221
|
+
if child.type == "type_parameter":
|
|
222
|
+
name_node = child.child_by_field_name("name")
|
|
223
|
+
name = _text(name_node, src) or "?"
|
|
224
|
+
bounds: list[str] = []
|
|
225
|
+
default: Optional[str] = None
|
|
226
|
+
for c in child.named_children:
|
|
227
|
+
if c.type == "trait_bounds":
|
|
228
|
+
for bound in c.named_children:
|
|
229
|
+
bound_text = _text(bound, src)
|
|
230
|
+
if bound_text:
|
|
231
|
+
bounds.append(bound_text.strip())
|
|
232
|
+
elif c.type == "type_bound":
|
|
233
|
+
bound_text = _text(c, src)
|
|
234
|
+
if bound_text:
|
|
235
|
+
bounds.append(bound_text.strip())
|
|
236
|
+
elif c.type == "type":
|
|
237
|
+
default = _text(c, src)
|
|
238
|
+
generics.append(GenericParam(name=name, bounds=bounds, default=default))
|
|
239
|
+
elif child.type in ("lifetime", "lifetime_parameter"):
|
|
240
|
+
name = _text(child, src) or "'?"
|
|
241
|
+
lifetimes.append(LifetimeParam(name=name))
|
|
242
|
+
elif child.type == "const_parameter":
|
|
243
|
+
name_node = child.child_by_field_name("name")
|
|
244
|
+
name = _text(name_node, src) or "?"
|
|
245
|
+
generics.append(GenericParam(name=name))
|
|
246
|
+
|
|
247
|
+
return generics, lifetimes
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# Complexity metrics
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
_BRANCH_NODES = frozenset({
|
|
255
|
+
"if_expression",
|
|
256
|
+
"if_statement",
|
|
257
|
+
"match_expression",
|
|
258
|
+
"match_arm",
|
|
259
|
+
"loop_expression",
|
|
260
|
+
"while_expression",
|
|
261
|
+
"for_expression",
|
|
262
|
+
"if_let_expression",
|
|
263
|
+
"while_let_expression",
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
_FUNCTION_CALL_NODES = frozenset({"call_expression", "method_call_expression"})
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _compute_complexity(body_node: Optional[Node], src: bytes) -> ComplexityMetrics:
|
|
270
|
+
"""Compute cyclomatic complexity and other metrics for a function body."""
|
|
271
|
+
metrics = ComplexityMetrics()
|
|
272
|
+
if body_node is None:
|
|
273
|
+
return metrics
|
|
274
|
+
|
|
275
|
+
lines = src[body_node.start_byte:body_node.end_byte].decode("utf-8", errors="replace")
|
|
276
|
+
metrics.lines_of_code = lines.count("\n") + 1
|
|
277
|
+
|
|
278
|
+
max_depth = 0
|
|
279
|
+
|
|
280
|
+
def _walk(node: Node, depth: int) -> None:
|
|
281
|
+
nonlocal max_depth
|
|
282
|
+
if depth > max_depth:
|
|
283
|
+
max_depth = depth
|
|
284
|
+
if node.type in _BRANCH_NODES and node.type != "match_arm":
|
|
285
|
+
metrics.cyclomatic += 1
|
|
286
|
+
metrics.num_branches += 1
|
|
287
|
+
if node.type in _FUNCTION_CALL_NODES:
|
|
288
|
+
metrics.num_function_calls += 1
|
|
289
|
+
if node.type == "binary_expression":
|
|
290
|
+
op = node.child_by_field_name("operator")
|
|
291
|
+
if op is not None:
|
|
292
|
+
op_text = _text(op, src) or ""
|
|
293
|
+
if op_text in ("&&", "||"):
|
|
294
|
+
metrics.cyclomatic += 1
|
|
295
|
+
for child in node.children:
|
|
296
|
+
_walk(child, depth + 1)
|
|
297
|
+
|
|
298
|
+
_walk(body_node, 0)
|
|
299
|
+
metrics.nesting_depth = max_depth
|
|
300
|
+
# Cognitive complexity: penalize nesting on top of branching
|
|
301
|
+
metrics.cognitive = metrics.cyclomatic + max_depth
|
|
302
|
+
return metrics
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
# Item extraction
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
def _extract_function(node: Node, src: bytes, kind: str) -> ExtractedItem:
|
|
310
|
+
name_node = node.child_by_field_name("name")
|
|
311
|
+
name = _text(name_node, src) or "<anonymous>"
|
|
312
|
+
doc, attrs = _leading_metadata(node, src)
|
|
313
|
+
generics, lifetimes = _extract_generic_params(node, src)
|
|
314
|
+
body_node = node.child_by_field_name("body")
|
|
315
|
+
complexity = _compute_complexity(body_node, src)
|
|
316
|
+
return ExtractedItem(
|
|
317
|
+
kind=kind,
|
|
318
|
+
name=name,
|
|
319
|
+
start_line=node.start_point[0] + 1,
|
|
320
|
+
end_line=node.end_point[0] + 1,
|
|
321
|
+
source=_text(node, src) or "",
|
|
322
|
+
visibility=_visibility(node, src),
|
|
323
|
+
signature=_function_signature(node, src),
|
|
324
|
+
doc=doc,
|
|
325
|
+
attributes=attrs,
|
|
326
|
+
body_node=body_node,
|
|
327
|
+
generic_params=generics,
|
|
328
|
+
lifetime_params=lifetimes,
|
|
329
|
+
complexity=complexity,
|
|
330
|
+
is_pub=_visibility(node, src) is not None,
|
|
331
|
+
is_const_fn=_has_modifier(node, "const"),
|
|
332
|
+
is_async=_has_modifier(node, "async"),
|
|
333
|
+
is_unsafe=_has_modifier(node, "unsafe"),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _extract_simple(node: Node, src: bytes, kind: str) -> ExtractedItem:
|
|
338
|
+
name_node = node.child_by_field_name("name")
|
|
339
|
+
name = _text(name_node, src) or "<anonymous>"
|
|
340
|
+
doc, attrs = _leading_metadata(node, src)
|
|
341
|
+
generics, lifetimes = _extract_generic_params(node, src)
|
|
342
|
+
return ExtractedItem(
|
|
343
|
+
kind=kind,
|
|
344
|
+
name=name,
|
|
345
|
+
start_line=node.start_point[0] + 1,
|
|
346
|
+
end_line=node.end_point[0] + 1,
|
|
347
|
+
source=_text(node, src) or "",
|
|
348
|
+
visibility=_visibility(node, src),
|
|
349
|
+
doc=doc,
|
|
350
|
+
attributes=attrs,
|
|
351
|
+
generic_params=generics,
|
|
352
|
+
lifetime_params=lifetimes,
|
|
353
|
+
is_pub=_visibility(node, src) is not None,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _extract_impl(node: Node, src: bytes) -> ExtractedItem:
|
|
358
|
+
type_node = node.child_by_field_name("type")
|
|
359
|
+
trait_node = node.child_by_field_name("trait")
|
|
360
|
+
target = _text(type_node, src) or "<unknown>"
|
|
361
|
+
trait_name = _text(trait_node, src)
|
|
362
|
+
doc, attrs = _leading_metadata(node, src)
|
|
363
|
+
generics, lifetimes = _extract_generic_params(node, src)
|
|
364
|
+
display_name = f"impl {trait_name + ' for ' if trait_name else ''}{target}"
|
|
365
|
+
item = ExtractedItem(
|
|
366
|
+
kind="impl",
|
|
367
|
+
name=display_name,
|
|
368
|
+
start_line=node.start_point[0] + 1,
|
|
369
|
+
end_line=node.end_point[0] + 1,
|
|
370
|
+
source=_text(node, src) or "",
|
|
371
|
+
target=target,
|
|
372
|
+
trait_name=trait_name,
|
|
373
|
+
doc=doc,
|
|
374
|
+
attributes=attrs,
|
|
375
|
+
generic_params=generics,
|
|
376
|
+
lifetime_params=lifetimes,
|
|
377
|
+
)
|
|
378
|
+
body = node.child_by_field_name("body")
|
|
379
|
+
if body is not None:
|
|
380
|
+
for child in body.named_children:
|
|
381
|
+
if child.type in ("function_item", "function_signature_item"):
|
|
382
|
+
method = _extract_function(
|
|
383
|
+
child,
|
|
384
|
+
src,
|
|
385
|
+
"method" if child.type == "function_item" else "method_sig",
|
|
386
|
+
)
|
|
387
|
+
method.target = target
|
|
388
|
+
method.trait_name = trait_name
|
|
389
|
+
item.children.append(method)
|
|
390
|
+
elif child.type == "const_item":
|
|
391
|
+
c = _extract_simple(child, src, "assoc_const")
|
|
392
|
+
c.target = target
|
|
393
|
+
item.children.append(c)
|
|
394
|
+
elif child.type == "type_item":
|
|
395
|
+
t = _extract_simple(child, src, "assoc_type")
|
|
396
|
+
t.target = target
|
|
397
|
+
item.children.append(t)
|
|
398
|
+
return item
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _extract_trait(node: Node, src: bytes) -> ExtractedItem:
|
|
402
|
+
item = _extract_simple(node, src, "trait")
|
|
403
|
+
body = node.child_by_field_name("body")
|
|
404
|
+
if body is not None:
|
|
405
|
+
for child in body.named_children:
|
|
406
|
+
if child.type in ("function_item", "function_signature_item"):
|
|
407
|
+
method = _extract_function(
|
|
408
|
+
child,
|
|
409
|
+
src,
|
|
410
|
+
"method" if child.type == "function_item" else "method_sig",
|
|
411
|
+
)
|
|
412
|
+
method.target = item.name
|
|
413
|
+
item.children.append(method)
|
|
414
|
+
return item
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _extract_mod(node: Node, src: bytes) -> ExtractedItem:
|
|
418
|
+
item = _extract_simple(node, src, "mod")
|
|
419
|
+
body = node.child_by_field_name("body")
|
|
420
|
+
if body is not None:
|
|
421
|
+
for child in body.named_children:
|
|
422
|
+
item.children.extend(_extract_top_level_node(child, src))
|
|
423
|
+
return item
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _extract_top_level_node(node: Node, src: bytes) -> list[ExtractedItem]:
|
|
427
|
+
t = node.type
|
|
428
|
+
if t == "impl_item":
|
|
429
|
+
return [_extract_impl(node, src)]
|
|
430
|
+
if t == "trait_item":
|
|
431
|
+
return [_extract_trait(node, src)]
|
|
432
|
+
if t == "mod_item":
|
|
433
|
+
if node.child_by_field_name("body") is not None:
|
|
434
|
+
return [_extract_mod(node, src)]
|
|
435
|
+
return [_extract_simple(node, src, "mod_decl")]
|
|
436
|
+
if t in ("function_item", "function_signature_item"):
|
|
437
|
+
return [_extract_function(node, src, TOP_LEVEL_KINDS[t])]
|
|
438
|
+
if t in TOP_LEVEL_KINDS:
|
|
439
|
+
return [_extract_simple(node, src, TOP_LEVEL_KINDS[t])]
|
|
440
|
+
return []
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# ---------------------------------------------------------------------------
|
|
444
|
+
# Use / extern crate extraction
|
|
445
|
+
# ---------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
def _use_path_text(node: Node, src: bytes) -> str:
|
|
448
|
+
"""Recursively extract the full path text from a use_declaration's
|
|
449
|
+
scoped_identifier tree."""
|
|
450
|
+
if node.type == "scoped_identifier":
|
|
451
|
+
path_child = node.child_by_field_name("path")
|
|
452
|
+
name_child = node.child_by_field_name("name")
|
|
453
|
+
if path_child is not None and name_child is not None:
|
|
454
|
+
return _use_path_text(path_child, src) + "::" + (_text(name_child, src) or "")
|
|
455
|
+
return _text(node, src) or ""
|
|
456
|
+
return _text(node, src) or ""
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _extract_use_declarations(tree_root: Node, src: bytes) -> list[UseDeclaration]:
|
|
460
|
+
"""Extract all ``use`` declarations from the file."""
|
|
461
|
+
results: list[UseDeclaration] = []
|
|
462
|
+
|
|
463
|
+
def _walk(node: Node) -> None:
|
|
464
|
+
if node.type == "use_declaration":
|
|
465
|
+
# The child is typically a scoped_identifier or use_wildcard
|
|
466
|
+
inner = None
|
|
467
|
+
for child in node.named_children:
|
|
468
|
+
if child.type in ("scoped_identifier", "use_wildcard", "use_as_clause", "identifier"):
|
|
469
|
+
inner = child
|
|
470
|
+
break
|
|
471
|
+
if inner is None:
|
|
472
|
+
for child in node.children:
|
|
473
|
+
if child.is_named:
|
|
474
|
+
inner = child
|
|
475
|
+
break
|
|
476
|
+
|
|
477
|
+
text = ""
|
|
478
|
+
is_glob = False
|
|
479
|
+
alias: Optional[str] = None
|
|
480
|
+
|
|
481
|
+
if inner is not None:
|
|
482
|
+
if inner.type == "use_as_clause":
|
|
483
|
+
alias_node = inner.child_by_field_name("alias")
|
|
484
|
+
alias = _text(alias_node, src)
|
|
485
|
+
# The path part is the first named child
|
|
486
|
+
for child in inner.named_children:
|
|
487
|
+
if child != alias_node:
|
|
488
|
+
text = _use_path_text(child, src)
|
|
489
|
+
break
|
|
490
|
+
elif inner.type == "use_wildcard":
|
|
491
|
+
text = _use_path_text(inner, src)
|
|
492
|
+
is_glob = True
|
|
493
|
+
else:
|
|
494
|
+
text = _use_path_text(inner, src)
|
|
495
|
+
|
|
496
|
+
results.append(UseDeclaration(
|
|
497
|
+
path=text,
|
|
498
|
+
alias=alias,
|
|
499
|
+
is_glob=is_glob,
|
|
500
|
+
start_line=node.start_point[0] + 1,
|
|
501
|
+
end_line=node.end_point[0] + 1,
|
|
502
|
+
))
|
|
503
|
+
return
|
|
504
|
+
for child in node.children:
|
|
505
|
+
_walk(child)
|
|
506
|
+
|
|
507
|
+
_walk(tree_root)
|
|
508
|
+
return results
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _extract_extern_crates(tree_root: Node, src: bytes) -> list[ExternCrate]:
|
|
512
|
+
"""Extract all ``extern crate`` declarations."""
|
|
513
|
+
results: list[ExternCrate] = []
|
|
514
|
+
for node in tree_root.named_children:
|
|
515
|
+
if node.type == "extern_crate_declaration":
|
|
516
|
+
name_node = node.child_by_field_name("name")
|
|
517
|
+
name = _text(name_node, src) or "?"
|
|
518
|
+
alias: Optional[str] = None
|
|
519
|
+
for child in node.named_children:
|
|
520
|
+
if child.type == "identifier" and child != name_node:
|
|
521
|
+
alias = _text(child, src)
|
|
522
|
+
results.append(ExternCrate(
|
|
523
|
+
name=name,
|
|
524
|
+
alias=alias,
|
|
525
|
+
start_line=node.start_point[0] + 1,
|
|
526
|
+
end_line=node.end_point[0] + 1,
|
|
527
|
+
))
|
|
528
|
+
return results
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# ---------------------------------------------------------------------------
|
|
532
|
+
# Call extraction
|
|
533
|
+
# ---------------------------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
def _call_target_name(fn_node: Node, src: bytes) -> Optional[tuple[str, Optional[str], bool]]:
|
|
536
|
+
"""Given the ``function`` child of a ``call_expression``, return
|
|
537
|
+
``(callee_name, receiver, is_method_call)`` or ``None``."""
|
|
538
|
+
if fn_node.type == "identifier":
|
|
539
|
+
return _text(fn_node, src), None, False
|
|
540
|
+
if fn_node.type == "field_expression":
|
|
541
|
+
field_node = fn_node.child_by_field_name("field")
|
|
542
|
+
value = fn_node.child_by_field_name("value")
|
|
543
|
+
name = _text(field_node, src)
|
|
544
|
+
receiver = _text(value, src) if value is not None else None
|
|
545
|
+
if receiver and len(receiver) > 40:
|
|
546
|
+
receiver = receiver[:37] + "..."
|
|
547
|
+
return name, receiver, True
|
|
548
|
+
if fn_node.type == "scoped_identifier":
|
|
549
|
+
path = fn_node.child_by_field_name("path")
|
|
550
|
+
name_node = fn_node.child_by_field_name("name")
|
|
551
|
+
name = _text(name_node, src)
|
|
552
|
+
receiver = _text(path, src) if path is not None else None
|
|
553
|
+
return name, receiver, False
|
|
554
|
+
return None
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def extract_calls(body_node: Node, src: bytes) -> list[CallEdge]:
|
|
558
|
+
"""Walk a function/method body and collect all direct call_expression
|
|
559
|
+
targets, without recursing into nested function/closure definitions."""
|
|
560
|
+
edges: list[CallEdge] = []
|
|
561
|
+
|
|
562
|
+
def _walk(node: Node) -> None:
|
|
563
|
+
if node.type == "call_expression":
|
|
564
|
+
fn_node = node.child_by_field_name("function")
|
|
565
|
+
resolved = _call_target_name(fn_node, src) if fn_node is not None else None
|
|
566
|
+
if resolved is not None:
|
|
567
|
+
name, receiver, is_method = resolved
|
|
568
|
+
edges.append(CallEdge(
|
|
569
|
+
callee_name=name,
|
|
570
|
+
receiver=receiver,
|
|
571
|
+
line=node.start_point[0] + 1,
|
|
572
|
+
is_method_call=is_method,
|
|
573
|
+
))
|
|
574
|
+
if node.type in TOP_LEVEL_KINDS:
|
|
575
|
+
return
|
|
576
|
+
for child in node.children:
|
|
577
|
+
_walk(child)
|
|
578
|
+
|
|
579
|
+
_walk(body_node)
|
|
580
|
+
return edges
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
# ---------------------------------------------------------------------------
|
|
584
|
+
# Public API
|
|
585
|
+
# ---------------------------------------------------------------------------
|
|
586
|
+
|
|
587
|
+
@dataclass
|
|
588
|
+
class FileExtraction:
|
|
589
|
+
"""Complete extraction result for a single Rust source file."""
|
|
590
|
+
|
|
591
|
+
items: list[ExtractedItem]
|
|
592
|
+
use_declarations: list[UseDeclaration]
|
|
593
|
+
extern_crates: list[ExternCrate]
|
|
594
|
+
total_lines: int = 0
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def extract_file(source_code: bytes) -> FileExtraction:
|
|
598
|
+
"""Parse a Rust source file and extract all entities and metadata.
|
|
599
|
+
|
|
600
|
+
Returns a ``FileExtraction`` containing items, use declarations,
|
|
601
|
+
extern crate declarations, and total line count.
|
|
602
|
+
|
|
603
|
+
Raises:
|
|
604
|
+
ParseError: If tree-sitter fails to parse the source.
|
|
605
|
+
"""
|
|
606
|
+
parser = Parser(RUST_LANGUAGE)
|
|
607
|
+
tree = parser.parse(source_code)
|
|
608
|
+
|
|
609
|
+
if tree.root_node.has_error:
|
|
610
|
+
error_nodes = _find_error_nodes(tree.root_node)
|
|
611
|
+
detail = "; ".join(
|
|
612
|
+
f"line {n.start_point[0]+1}: {n.type}"
|
|
613
|
+
for n in error_nodes[:3]
|
|
614
|
+
)
|
|
615
|
+
log.warning("Parse tree has errors: %s", detail)
|
|
616
|
+
|
|
617
|
+
items: list[ExtractedItem] = []
|
|
618
|
+
for child in tree.root_node.named_children:
|
|
619
|
+
items.extend(_extract_top_level_node(child, source_code))
|
|
620
|
+
|
|
621
|
+
uses = _extract_use_declarations(tree.root_node, source_code)
|
|
622
|
+
externs = _extract_extern_crates(tree.root_node, source_code)
|
|
623
|
+
total_lines = source_code.count(b"\n") + 1
|
|
624
|
+
|
|
625
|
+
return FileExtraction(
|
|
626
|
+
items=items,
|
|
627
|
+
use_declarations=uses,
|
|
628
|
+
extern_crates=externs,
|
|
629
|
+
total_lines=total_lines,
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _find_error_nodes(node: Node) -> list[Node]:
|
|
634
|
+
"""Collect all ERROR/MISSING nodes in the tree."""
|
|
635
|
+
errors: list[Node] = []
|
|
636
|
+
if node.type in ("ERROR", "MISSING"):
|
|
637
|
+
errors.append(node)
|
|
638
|
+
for child in node.children:
|
|
639
|
+
errors.extend(_find_error_nodes(child))
|
|
640
|
+
return errors
|