devlensio 0.6.2 → 1.0.1
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.
- package/README.md +89 -31
- package/dist/extractors/detectLanguage.d.ts +2 -0
- package/dist/extractors/detectLanguage.js +33 -0
- package/dist/extractors/index.d.ts +6 -0
- package/dist/extractors/index.js +155 -0
- package/dist/extractors/runner.d.ts +7 -0
- package/dist/extractors/runner.js +194 -0
- package/dist/extractors/types.d.ts +37 -0
- package/dist/extractors/types.js +8 -0
- package/dist/graph/buildLookup.d.ts +1 -0
- package/dist/graph/buildLookup.js +2 -1
- package/dist/graph/edges/callEdges.js +19 -5
- package/dist/graph/edges/callEdges.test.d.ts +1 -0
- package/dist/graph/edges/callEdges.test.js +200 -0
- package/dist/graph/edges/importEdges.js +12 -0
- package/dist/graph/edges/inheritanceEdges.d.ts +3 -0
- package/dist/graph/edges/inheritanceEdges.js +73 -0
- package/dist/graph/edges/inheritanceEdges.test.d.ts +1 -0
- package/dist/graph/edges/inheritanceEdges.test.js +140 -0
- package/dist/graph/index.js +4 -0
- package/dist/parser/classes.test.d.ts +1 -0
- package/dist/parser/classes.test.js +360 -0
- package/dist/parser/extractors/classes.d.ts +5 -0
- package/dist/parser/extractors/classes.js +241 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +7 -1
- package/dist/pipeline/index.d.ts +2 -1
- package/dist/pipeline/index.js +30 -42
- package/dist/scoring/index.js +8 -0
- package/dist/scoring/index.test.js +43 -0
- package/dist/scoring/nodeScorer.js +2 -0
- package/dist/scoring/pruneDisconnected.d.ts +8 -0
- package/dist/scoring/pruneDisconnected.js +66 -0
- package/dist/scoring/pruneDisconnected.test.d.ts +1 -0
- package/dist/scoring/pruneDisconnected.test.js +123 -0
- package/dist/summarizer/prompts.d.ts +1 -1
- package/dist/types.d.ts +5 -5
- package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
- package/extractors/go/build.mjs +43 -0
- package/extractors/go/calls.go +289 -0
- package/extractors/go/contract.go +140 -0
- package/extractors/go/extractor.go +161 -0
- package/extractors/go/fingerprint.go +167 -0
- package/extractors/go/go.mod +3 -0
- package/extractors/go/imports.go +90 -0
- package/extractors/go/inheritance.go +138 -0
- package/extractors/go/lookup.go +106 -0
- package/extractors/go/main.go +57 -0
- package/extractors/go/nodes.go +177 -0
- package/extractors/go/orm_edges.go +277 -0
- package/extractors/go/parser.go +469 -0
- package/extractors/go/routes.go +521 -0
- package/extractors/go/tests.go +45 -0
- package/extractors/go/thirdparty.go +138 -0
- package/extractors/go/typeload.go +178 -0
- package/extractors/go/walker.go +67 -0
- package/extractors/java/build.mjs +90 -0
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/extractors/java/src/devlens/extractor/Contract.java +171 -0
- package/extractors/java/src/devlens/extractor/Extractor.java +262 -0
- package/extractors/java/src/devlens/extractor/ExtractorResult.java +12 -0
- package/extractors/java/src/devlens/extractor/Fingerprint.java +239 -0
- package/extractors/java/src/devlens/extractor/LookupMaps.java +123 -0
- package/extractors/java/src/devlens/extractor/Main.java +66 -0
- package/extractors/java/src/devlens/extractor/Parser.java +522 -0
- package/extractors/java/src/devlens/extractor/SourceWalker.java +82 -0
- package/extractors/java/src/devlens/extractor/ThirdParty.java +141 -0
- package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +43 -0
- package/extractors/java/src/devlens/extractor/edges/Calls.java +266 -0
- package/extractors/java/src/devlens/extractor/edges/Enrich.java +54 -0
- package/extractors/java/src/devlens/extractor/edges/Imports.java +154 -0
- package/extractors/java/src/devlens/extractor/edges/Inheritance.java +79 -0
- package/extractors/java/src/devlens/extractor/edges/OrmEdges.java +209 -0
- package/extractors/java/src/devlens/extractor/edges/Routes.java +143 -0
- package/extractors/java/src/devlens/extractor/edges/Tests.java +53 -0
- package/extractors/python/devlens_extractors_python/__init__.py +3 -0
- package/extractors/python/devlens_extractors_python/__main__.py +33 -0
- package/extractors/python/devlens_extractors_python/contract.py +94 -0
- package/extractors/python/devlens_extractors_python/edges/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/calls.py +112 -0
- package/extractors/python/devlens_extractors_python/edges/enrich.py +44 -0
- package/extractors/python/devlens_extractors_python/edges/imports.py +155 -0
- package/extractors/python/devlens_extractors_python/edges/inheritance.py +97 -0
- package/extractors/python/devlens_extractors_python/edges/orm_edges.py +225 -0
- package/extractors/python/devlens_extractors_python/edges/routes/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/routes/common.py +79 -0
- package/extractors/python/devlens_extractors_python/edges/routes/decorators.py +212 -0
- package/extractors/python/devlens_extractors_python/edges/routes/django_urls.py +213 -0
- package/extractors/python/devlens_extractors_python/edges/routes/drf.py +128 -0
- package/extractors/python/devlens_extractors_python/edges/tests.py +53 -0
- package/extractors/python/devlens_extractors_python/extractor.py +107 -0
- package/extractors/python/devlens_extractors_python/fingerprint.py +201 -0
- package/extractors/python/devlens_extractors_python/lookup.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/__init__.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/classes.py +109 -0
- package/extractors/python/devlens_extractors_python/parser/functions.py +163 -0
- package/extractors/python/devlens_extractors_python/parser/walker.py +30 -0
- package/extractors/python/devlens_extractors_python/third_party.py +103 -0
- package/extractors/python/pyproject.toml +16 -0
- package/extractors/python/setup.mjs +54 -0
- package/extractors/rust/Cargo.toml +29 -0
- package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
- package/extractors/rust/build.mjs +85 -0
- package/extractors/rust/src/calls.rs +295 -0
- package/extractors/rust/src/contract.rs +236 -0
- package/extractors/rust/src/enrich.rs +107 -0
- package/extractors/rust/src/extractor.rs +199 -0
- package/extractors/rust/src/fingerprint.rs +238 -0
- package/extractors/rust/src/imports.rs +101 -0
- package/extractors/rust/src/inheritance.rs +233 -0
- package/extractors/rust/src/lookup.rs +226 -0
- package/extractors/rust/src/main.rs +53 -0
- package/extractors/rust/src/module_map.rs +131 -0
- package/extractors/rust/src/nodes.rs +174 -0
- package/extractors/rust/src/orm_edges.rs +125 -0
- package/extractors/rust/src/parser.rs +859 -0
- package/extractors/rust/src/routes.rs +1265 -0
- package/extractors/rust/src/tests.rs +109 -0
- package/extractors/rust/src/thirdparty.rs +114 -0
- package/extractors/rust/src/walker.rs +82 -0
- package/package.json +22 -4
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// calls.rs — the CALLS ladder (syntactic — syn does no type resolution).
|
|
2
|
+
//
|
|
3
|
+
// Ladder (each rung documented; later rungs flagged heuristic where fuzzy):
|
|
4
|
+
// 1. Path call `a::b::c()` / `Type::m()`:
|
|
5
|
+
// a. first segment via the file's use-symbol map (alias → target)
|
|
6
|
+
// b. module_map.resolve → (file, rest)
|
|
7
|
+
// c. rest = single name → FUNCTION in that file (closest_by_path)
|
|
8
|
+
// d. rest = Type::method → STRUCT/ENUM/TRAIT in that file, then
|
|
9
|
+
// methods_by_owner (same file preferred) → METHOD
|
|
10
|
+
// 2. Plain `bar()` → same-file FUNCTION first, then crate-wide name
|
|
11
|
+
// 3. `Type::new()` with the type in scope but NOT imported (same module /
|
|
12
|
+
// no use statement) → type via closest_by_path, then its methods
|
|
13
|
+
// 4. `self.method()` → methods of the enclosing impl's owner type
|
|
14
|
+
// 5. `x.method()` (other receivers) → no type info → metadata only V1
|
|
15
|
+
// 6. External first segment (a Cargo.toml dep, not std/local) → lazy
|
|
16
|
+
// [crate]/name::member node (gated)
|
|
17
|
+
//
|
|
18
|
+
// metadata.calls already carries the raw strings (parser); resolved targets
|
|
19
|
+
// are written back as metadata.resolvedCalls.
|
|
20
|
+
|
|
21
|
+
use crate::contract::{edge, CodeEdge};
|
|
22
|
+
use crate::extractor::{Options, ParsedRepo};
|
|
23
|
+
use crate::fingerprint::ParsedManifest;
|
|
24
|
+
use crate::lookup::LookupMaps;
|
|
25
|
+
use crate::module_map::ModuleMap;
|
|
26
|
+
use crate::parser::ParsedItem;
|
|
27
|
+
use crate::thirdparty::ThirdPartyRegistry;
|
|
28
|
+
|
|
29
|
+
pub struct CallsOutput {
|
|
30
|
+
pub edges: Vec<CodeEdge>,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub fn detect_calls(
|
|
34
|
+
repo: &ParsedRepo,
|
|
35
|
+
module_map: &ModuleMap,
|
|
36
|
+
lookup: &mut LookupMaps,
|
|
37
|
+
mf: &ParsedManifest,
|
|
38
|
+
tp: &mut ThirdPartyRegistry,
|
|
39
|
+
_opts: &Options,
|
|
40
|
+
) -> CallsOutput {
|
|
41
|
+
let mut edges = vec![];
|
|
42
|
+
|
|
43
|
+
for pf in &repo.files {
|
|
44
|
+
if pf.is_test_file {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
for item in &pf.items {
|
|
48
|
+
if item.is_test || item.calls.is_empty() {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
let from = crate::lookup::node_id_for_item(&pf.rel_path, item);
|
|
52
|
+
if from.is_empty() {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
let base_module = crate::walker::module_path_for_file(&pf.rel_path);
|
|
56
|
+
let mut resolved: Vec<String> = vec![];
|
|
57
|
+
for call in &item.calls {
|
|
58
|
+
if let Some(target) = resolve_call(
|
|
59
|
+
call,
|
|
60
|
+
item,
|
|
61
|
+
&pf.rel_path,
|
|
62
|
+
&base_module,
|
|
63
|
+
module_map,
|
|
64
|
+
lookup,
|
|
65
|
+
mf,
|
|
66
|
+
tp,
|
|
67
|
+
) {
|
|
68
|
+
edges.push(edge(&from, &target, "CALLS"));
|
|
69
|
+
resolved.push(target);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if !resolved.is_empty() {
|
|
73
|
+
// write metadata.resolvedCalls back (nodes are owned by the
|
|
74
|
+
// registry; clone → mutate → reinsert)
|
|
75
|
+
if let Some(node) = lookup.node_by_id.get(&from).cloned() {
|
|
76
|
+
let mut n = node;
|
|
77
|
+
let r: Vec<serde_json::Value> = resolved
|
|
78
|
+
.iter()
|
|
79
|
+
.map(|s| serde_json::Value::from(s.clone()))
|
|
80
|
+
.collect();
|
|
81
|
+
n.metadata
|
|
82
|
+
.insert("resolvedCalls".to_string(), serde_json::Value::Array(r));
|
|
83
|
+
lookup.node_by_id.insert(from, n);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
CallsOutput { edges }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Resolve one call string → target node id (or None).
|
|
93
|
+
#[allow(clippy::too_many_arguments)]
|
|
94
|
+
fn resolve_call(
|
|
95
|
+
call: &str,
|
|
96
|
+
item: &ParsedItem,
|
|
97
|
+
rel_path: &str,
|
|
98
|
+
base_module: &str,
|
|
99
|
+
module_map: &ModuleMap,
|
|
100
|
+
lookup: &LookupMaps,
|
|
101
|
+
mf: &ParsedManifest,
|
|
102
|
+
tp: &mut ThirdPartyRegistry,
|
|
103
|
+
) -> Option<String> {
|
|
104
|
+
// `x.method()` — receiver form
|
|
105
|
+
if let Some((recv, method)) = call.split_once('.') {
|
|
106
|
+
if recv == "self" {
|
|
107
|
+
// methods of the enclosing impl's owner type
|
|
108
|
+
let owner = &item.impl_target;
|
|
109
|
+
if owner.is_empty() {
|
|
110
|
+
return None;
|
|
111
|
+
}
|
|
112
|
+
return method_of_owner(owner, method, rel_path, lookup);
|
|
113
|
+
}
|
|
114
|
+
// other receivers: no type resolution in V1 — metadata only
|
|
115
|
+
return None;
|
|
116
|
+
}
|
|
117
|
+
resolve_path_call(call, rel_path, base_module, module_map, lookup, mf, tp)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Resolve a path-form call (`a::b::c`, `Type::m`, plain `bar`) to a node id.
|
|
121
|
+
/// Shared by the CALLS ladder and utoipa-axum route handlers (routes.rs).
|
|
122
|
+
///
|
|
123
|
+
/// Ladder (documented Rust semantics): first segment via the file's use-symbol
|
|
124
|
+
/// map → module map resolve (relative-first for call paths) → glob-import
|
|
125
|
+
/// unroll (`use crate::controllers::*` brings `krate` into scope) → external
|
|
126
|
+
/// crate → lazy [crate]/name::member (gated).
|
|
127
|
+
pub fn resolve_path_call(
|
|
128
|
+
call: &str,
|
|
129
|
+
rel_path: &str,
|
|
130
|
+
base_module: &str,
|
|
131
|
+
module_map: &ModuleMap,
|
|
132
|
+
lookup: &LookupMaps,
|
|
133
|
+
mf: &ParsedManifest,
|
|
134
|
+
tp: &mut ThirdPartyRegistry,
|
|
135
|
+
) -> Option<String> {
|
|
136
|
+
let segs: Vec<&str> = call.split("::").collect();
|
|
137
|
+
if segs.is_empty() {
|
|
138
|
+
return None;
|
|
139
|
+
}
|
|
140
|
+
if segs.len() == 1 {
|
|
141
|
+
// plain name → same-file FUNCTION, then crate-wide
|
|
142
|
+
if let Some(id) = lookup.closest_by_path(segs[0], rel_path) {
|
|
143
|
+
return Some(id);
|
|
144
|
+
}
|
|
145
|
+
return None;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// multi-segment path — first segment via symbol map, then module resolve
|
|
149
|
+
let sym_target = lookup
|
|
150
|
+
.symbol_maps
|
|
151
|
+
.get(rel_path)
|
|
152
|
+
.and_then(|syms| syms.get(segs[0]))
|
|
153
|
+
.cloned();
|
|
154
|
+
let full_path = match &sym_target {
|
|
155
|
+
Some(t) => format!("{}::{}", t, segs[1..].join("::")),
|
|
156
|
+
None => call.to_string(),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Relative-first module resolution (the call-path ladder). NOTE: when
|
|
160
|
+
// the caller's own file IS a module (e.g. src/router.rs → module
|
|
161
|
+
// "router"), bare paths like `krate::search::list_crates` resolve
|
|
162
|
+
// relative-first to `router::krate::...` → matches the file's OWN module
|
|
163
|
+
// → returns (src/router.rs, "krate::search::list_crates") →
|
|
164
|
+
// resolve_in_file finds no item_by_module_name["router::krate"] → None.
|
|
165
|
+
// That None is NOT a hard failure — fall through to glob unroll +
|
|
166
|
+
// crate-root absolute, where utoipa `routes!(krate::search::list_crates)`
|
|
167
|
+
// handlers actually live (brought into scope via `use crate::controllers::*;`).
|
|
168
|
+
if let Some((file, rest)) = module_map.resolve(&full_path, base_module) {
|
|
169
|
+
if let Some(id) = resolve_in_file(&file, &rest, rel_path, lookup) {
|
|
170
|
+
return Some(id);
|
|
171
|
+
}
|
|
172
|
+
// don't return None — fall through to glob unroll + crate-root
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// glob unroll: `use crate::controllers::*;` brings `krate` into scope, so
|
|
176
|
+
// `krate::search::list_crates` resolves as `crate::controllers::krate::
|
|
177
|
+
// search::list_crates` (documented Rust glob-import semantics)
|
|
178
|
+
if sym_target.is_none() {
|
|
179
|
+
if let Some(globs) = lookup.glob_maps.get(rel_path) {
|
|
180
|
+
for g in globs {
|
|
181
|
+
let cand = format!("{}::{}", g, full_path);
|
|
182
|
+
if let Some((file, rest)) = module_map.resolve(&cand, base_module) {
|
|
183
|
+
if let Some(id) = resolve_in_file(&file, &rest, rel_path, lookup) {
|
|
184
|
+
return Some(id);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// crate-root absolute fallback. A bare multi-segment path whose first
|
|
192
|
+
// segment is a local module resolves from the CRATE ROOT in edition 2018
|
|
193
|
+
// (same semantics as use-paths — `krate::search::list_crates` from
|
|
194
|
+
// src/router.rs means `crate::krate::search::list_crates`, NOT
|
|
195
|
+
// `router::krate::...`). resolve_use() is crate-root-absolute and won't
|
|
196
|
+
// misresolve against the caller's own module.
|
|
197
|
+
if let Some((file, rest)) = module_map.resolve_use(&full_path, base_module) {
|
|
198
|
+
if let Some(id) = resolve_in_file(&file, &rest, rel_path, lookup) {
|
|
199
|
+
return Some(id);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// external crate (a dep, not std, not local) → lazy member node
|
|
204
|
+
let first = crate::thirdparty::first_segment(&full_path);
|
|
205
|
+
if crate::thirdparty::is_std(first) {
|
|
206
|
+
return None;
|
|
207
|
+
}
|
|
208
|
+
if crate::thirdparty::is_external_crate(first, mf) {
|
|
209
|
+
let member = full_path.split("::").skip(1).collect::<Vec<_>>().join("::");
|
|
210
|
+
return tp.member_node(first, &member).map(|n| n.id.clone());
|
|
211
|
+
}
|
|
212
|
+
None
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// Resolve `rest` (item path within `file`) → node id.
|
|
216
|
+
fn resolve_in_file(file: &str, rest: &str, from_rel: &str, lookup: &LookupMaps) -> Option<String> {
|
|
217
|
+
if rest.is_empty() {
|
|
218
|
+
return None;
|
|
219
|
+
}
|
|
220
|
+
let file_module = crate::walker::module_path_for_file(file);
|
|
221
|
+
let parts: Vec<&str> = rest.split("::").collect();
|
|
222
|
+
if parts.len() == 1 {
|
|
223
|
+
// single name — FUNCTION (or struct constructor call `Type(...)`)
|
|
224
|
+
let key = if file_module.is_empty() {
|
|
225
|
+
parts[0].to_string()
|
|
226
|
+
} else {
|
|
227
|
+
format!("{}::{}", file_module, parts[0])
|
|
228
|
+
};
|
|
229
|
+
if let Some(id) = lookup.item_by_module_name.get(&key) {
|
|
230
|
+
return Some(id.clone());
|
|
231
|
+
}
|
|
232
|
+
return lookup.closest_by_path(parts[0], file);
|
|
233
|
+
}
|
|
234
|
+
// Type::method
|
|
235
|
+
let ty_key = if file_module.is_empty() {
|
|
236
|
+
parts[0].to_string()
|
|
237
|
+
} else {
|
|
238
|
+
format!("{}::{}", file_module, parts[0])
|
|
239
|
+
};
|
|
240
|
+
let type_id = lookup.item_by_module_name.get(&ty_key).cloned();
|
|
241
|
+
let method = parts[1];
|
|
242
|
+
if let Some(tid) = type_id {
|
|
243
|
+
if let Some(mid) = method_of_owner_for_type(parts[0], method, file, lookup) {
|
|
244
|
+
let _ = tid;
|
|
245
|
+
return Some(mid);
|
|
246
|
+
}
|
|
247
|
+
// trait-dispatch call: `Trait::method(&x)` — the trait name resolved;
|
|
248
|
+
// find the method among `impl Trait for ...` blocks
|
|
249
|
+
if let Some(mid) = method_of_trait(parts[0], method, lookup) {
|
|
250
|
+
return Some(mid);
|
|
251
|
+
}
|
|
252
|
+
return None;
|
|
253
|
+
}
|
|
254
|
+
// type not found as item — maybe a trait in that module
|
|
255
|
+
let _ = from_rel;
|
|
256
|
+
lookup
|
|
257
|
+
.item_by_module_name
|
|
258
|
+
.get(&format!("{}::{}", file_module, parts[0]))
|
|
259
|
+
.cloned()
|
|
260
|
+
.and_then(|_| method_of_owner(parts[0], method, file, lookup))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// Method of a trait (resolved via `impl Trait for Type` blocks).
|
|
264
|
+
fn method_of_trait(trait_name: &str, method: &str, lookup: &LookupMaps) -> Option<String> {
|
|
265
|
+
let ids = lookup.methods_by_trait.get(trait_name)?;
|
|
266
|
+
// id shapes: `rel::Owner.method` (inherent) / `rel::Owner.method::Trait`
|
|
267
|
+
ids.iter()
|
|
268
|
+
.find(|id| id.ends_with(&format!(".{}", method)) || id.contains(&format!(".{}::", method)))
|
|
269
|
+
.cloned()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// Method of an owner type: same-file preferred, then any file.
|
|
273
|
+
fn method_of_owner(owner: &str, method: &str, file: &str, lookup: &LookupMaps) -> Option<String> {
|
|
274
|
+
let ids = lookup.methods_by_owner.get(owner)?;
|
|
275
|
+
// id shapes: `rel::Owner.method` / `rel::Owner.method::Trait`
|
|
276
|
+
let matches = |id: &String| {
|
|
277
|
+
id.ends_with(&format!(".{}", method)) || id.contains(&format!(".{}::", method))
|
|
278
|
+
};
|
|
279
|
+
let same_file = ids
|
|
280
|
+
.iter()
|
|
281
|
+
.find(|id| id.starts_with(&format!("{}::", file)) && matches(id));
|
|
282
|
+
match same_file {
|
|
283
|
+
Some(id) => Some(id.clone()),
|
|
284
|
+
None => ids.iter().find(|id| matches(id)).cloned(),
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
fn method_of_owner_for_type(
|
|
289
|
+
owner: &str,
|
|
290
|
+
method: &str,
|
|
291
|
+
file: &str,
|
|
292
|
+
lookup: &LookupMaps,
|
|
293
|
+
) -> Option<String> {
|
|
294
|
+
method_of_owner(owner, method, file, lookup)
|
|
295
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// contract.rs — the JSON shape DevLens Engine expects.
|
|
2
|
+
//
|
|
3
|
+
// Every key here is camelCase to match the TypeScript interfaces in the
|
|
4
|
+
// devlens-engine (src/types.ts, src/extractors/types.ts). This file is the
|
|
5
|
+
// single source of truth for the output shape; other modules never spell
|
|
6
|
+
// keys by hand. Mirrors extractors/go/contract.go.
|
|
7
|
+
//
|
|
8
|
+
// serde_json::Map for `metadata` (free-form per-node extras) and strongly
|
|
9
|
+
// typed structs for everything else. rawDependencies is a BTreeMap so keys
|
|
10
|
+
// serialize sorted (deterministic output — contract rule).
|
|
11
|
+
|
|
12
|
+
use serde::{Deserialize, Serialize};
|
|
13
|
+
use serde_json::{Map, Value};
|
|
14
|
+
use sha2::{Digest, Sha256};
|
|
15
|
+
use std::collections::BTreeMap;
|
|
16
|
+
|
|
17
|
+
// ── Node types (subset of the engine's NodeType union used by Rust) ──
|
|
18
|
+
pub const NODE_FILE: &str = "FILE";
|
|
19
|
+
pub const NODE_TEST: &str = "TEST";
|
|
20
|
+
pub const NODE_ROUTE: &str = "ROUTE";
|
|
21
|
+
pub const NODE_THIRD_PARTY: &str = "THIRD_PARTY";
|
|
22
|
+
|
|
23
|
+
// ── Edge types (subset of the engine's EdgeType union used by Rust) ──
|
|
24
|
+
pub const EDGE_READS_FROM: &str = "READS_FROM";
|
|
25
|
+
pub const EDGE_WRITES_TO: &str = "WRITES_TO";
|
|
26
|
+
|
|
27
|
+
// codeHash = sha256(rawCode) hex-encoded, first 16 chars (contract rule).
|
|
28
|
+
pub fn code_hash(raw: &str) -> String {
|
|
29
|
+
let mut h = Sha256::new();
|
|
30
|
+
h.update(raw.as_bytes());
|
|
31
|
+
let sum = h.finalize();
|
|
32
|
+
hex16(&sum)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn hex16(bytes: &[u8]) -> String {
|
|
36
|
+
let mut s = String::with_capacity(bytes.len() * 2);
|
|
37
|
+
for b in bytes {
|
|
38
|
+
s.push_str(&format!("{:02x}", b));
|
|
39
|
+
}
|
|
40
|
+
s.chars().take(16).collect()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Fingerprint ──
|
|
44
|
+
|
|
45
|
+
#[derive(Serialize, Clone)]
|
|
46
|
+
#[serde(rename_all = "camelCase")]
|
|
47
|
+
pub struct Fingerprint {
|
|
48
|
+
pub language: String,
|
|
49
|
+
pub project_type: String,
|
|
50
|
+
pub framework: String,
|
|
51
|
+
pub router: String,
|
|
52
|
+
pub state_management: Vec<String>,
|
|
53
|
+
pub data_fetching: Vec<String>,
|
|
54
|
+
pub databases: Vec<String>,
|
|
55
|
+
pub raw_dependencies: BTreeMap<String, String>,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
impl Fingerprint {
|
|
59
|
+
pub fn new() -> Self {
|
|
60
|
+
Fingerprint {
|
|
61
|
+
language: "rust".to_string(),
|
|
62
|
+
project_type: "unknown".to_string(),
|
|
63
|
+
framework: "unknown".to_string(),
|
|
64
|
+
router: "none".to_string(),
|
|
65
|
+
state_management: vec![],
|
|
66
|
+
data_fetching: vec![],
|
|
67
|
+
databases: vec![],
|
|
68
|
+
raw_dependencies: BTreeMap::new(),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Stats / Errors / Result ──
|
|
74
|
+
|
|
75
|
+
#[derive(Serialize, Clone)]
|
|
76
|
+
#[serde(rename_all = "camelCase")]
|
|
77
|
+
pub struct Stats {
|
|
78
|
+
pub total_files: i64,
|
|
79
|
+
pub total_nodes: i64,
|
|
80
|
+
pub skipped_files: i64,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#[derive(Serialize, Clone)]
|
|
84
|
+
#[serde(rename_all = "camelCase")]
|
|
85
|
+
pub struct ExtractorError {
|
|
86
|
+
#[serde(skip_serializing_if = "String::is_empty")]
|
|
87
|
+
pub file: String,
|
|
88
|
+
pub error: String,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[derive(Serialize, Clone)]
|
|
92
|
+
#[serde(rename_all = "camelCase")]
|
|
93
|
+
pub struct ExtractorResult {
|
|
94
|
+
pub fingerprint: Fingerprint,
|
|
95
|
+
pub nodes: Vec<CodeNode>,
|
|
96
|
+
pub edges: Vec<CodeEdge>,
|
|
97
|
+
pub routes: Vec<RouteNode>,
|
|
98
|
+
pub stats: Stats,
|
|
99
|
+
pub errors: Vec<ExtractorError>,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Input (stdin) ──
|
|
103
|
+
|
|
104
|
+
#[derive(Deserialize)]
|
|
105
|
+
#[serde(rename_all = "camelCase")]
|
|
106
|
+
pub struct ExtractorInput {
|
|
107
|
+
pub repo_path: String,
|
|
108
|
+
#[serde(default)]
|
|
109
|
+
pub options: Value,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Nodes / edges ──
|
|
113
|
+
|
|
114
|
+
#[derive(Serialize, Clone)]
|
|
115
|
+
#[serde(rename_all = "camelCase")]
|
|
116
|
+
pub struct CodeNode {
|
|
117
|
+
pub id: String,
|
|
118
|
+
pub name: String,
|
|
119
|
+
#[serde(rename = "type")]
|
|
120
|
+
pub node_type: String,
|
|
121
|
+
pub file_path: String,
|
|
122
|
+
pub start_line: i64,
|
|
123
|
+
pub end_line: i64,
|
|
124
|
+
pub parent_file: String,
|
|
125
|
+
#[serde(skip_serializing_if = "String::is_empty")]
|
|
126
|
+
pub code_hash: String,
|
|
127
|
+
#[serde(skip_serializing_if = "String::is_empty")]
|
|
128
|
+
pub raw_code: String,
|
|
129
|
+
pub metadata: Map<String, Value>,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[derive(Serialize, Clone)]
|
|
133
|
+
#[serde(rename_all = "camelCase")]
|
|
134
|
+
pub struct CodeEdge {
|
|
135
|
+
pub from: String,
|
|
136
|
+
pub to: String,
|
|
137
|
+
#[serde(rename = "type")]
|
|
138
|
+
pub edge_type: String,
|
|
139
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
140
|
+
pub metadata: Option<Map<String, Value>>,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Routes (the `routes` array — BackendRouteNode shape, mirrors Go) ──
|
|
144
|
+
|
|
145
|
+
#[derive(Serialize, Clone)]
|
|
146
|
+
#[serde(rename_all = "camelCase")]
|
|
147
|
+
pub struct RouteNode {
|
|
148
|
+
#[serde(rename = "type")]
|
|
149
|
+
pub route_type: String,
|
|
150
|
+
pub url_path: String,
|
|
151
|
+
pub file_path: String,
|
|
152
|
+
pub http_method: String,
|
|
153
|
+
pub framework: String,
|
|
154
|
+
pub is_dynamic: bool,
|
|
155
|
+
pub params: Vec<String>,
|
|
156
|
+
pub handler_name: String,
|
|
157
|
+
pub node_id: String,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Builders ──
|
|
161
|
+
|
|
162
|
+
// Base metadata every code node carries (mirrors the inline JS extractor).
|
|
163
|
+
pub fn base_metadata() -> Map<String, Value> {
|
|
164
|
+
let mut m = Map::new();
|
|
165
|
+
m.insert("nodeCount".to_string(), Value::from(0));
|
|
166
|
+
m.insert("childNodeIds".to_string(), Value::Array(vec![]));
|
|
167
|
+
m.insert("language".to_string(), Value::from("rust"));
|
|
168
|
+
m
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// fileNode — FILE (or TEST) node. Id format: file::rel/path.rs (matches
|
|
172
|
+
// src/parser/index.ts). File nodes parent themselves.
|
|
173
|
+
pub fn file_node(rel_path: &str, end_line: i64, node_type: &str) -> CodeNode {
|
|
174
|
+
CodeNode {
|
|
175
|
+
id: format!("file::{}", rel_path),
|
|
176
|
+
name: rel_path.rsplit('/').next().unwrap_or(rel_path).to_string(),
|
|
177
|
+
node_type: node_type.to_string(),
|
|
178
|
+
file_path: rel_path.to_string(),
|
|
179
|
+
start_line: 1,
|
|
180
|
+
end_line,
|
|
181
|
+
parent_file: format!("file::{}", rel_path),
|
|
182
|
+
code_hash: String::new(),
|
|
183
|
+
raw_code: String::new(),
|
|
184
|
+
metadata: base_metadata(),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// code_node — FUNCTION/METHOD/STRUCT/ENUM/TRAIT/IMPL_BLOCK/THIRD_PARTY/ROUTE.
|
|
189
|
+
// rawCode on every code node (summarizer input — engine discards after).
|
|
190
|
+
#[allow(clippy::too_many_arguments)]
|
|
191
|
+
pub fn code_node(
|
|
192
|
+
id: &str,
|
|
193
|
+
rel_path: &str,
|
|
194
|
+
name: &str,
|
|
195
|
+
node_type: &str,
|
|
196
|
+
start_line: i64,
|
|
197
|
+
end_line: i64,
|
|
198
|
+
raw_code: &str,
|
|
199
|
+
metadata: Map<String, Value>,
|
|
200
|
+
) -> CodeNode {
|
|
201
|
+
CodeNode {
|
|
202
|
+
id: id.to_string(),
|
|
203
|
+
name: name.to_string(),
|
|
204
|
+
node_type: node_type.to_string(),
|
|
205
|
+
file_path: rel_path.to_string(),
|
|
206
|
+
start_line,
|
|
207
|
+
end_line,
|
|
208
|
+
parent_file: format!("file::{}", rel_path),
|
|
209
|
+
code_hash: code_hash(raw_code),
|
|
210
|
+
raw_code: raw_code.to_string(),
|
|
211
|
+
metadata,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
pub fn edge(from: &str, to: &str, edge_type: &str) -> CodeEdge {
|
|
216
|
+
CodeEdge {
|
|
217
|
+
from: from.to_string(),
|
|
218
|
+
to: to.to_string(),
|
|
219
|
+
edge_type: edge_type.to_string(),
|
|
220
|
+
metadata: None,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
pub fn edge_with_meta(
|
|
225
|
+
from: &str,
|
|
226
|
+
to: &str,
|
|
227
|
+
edge_type: &str,
|
|
228
|
+
metadata: Map<String, Value>,
|
|
229
|
+
) -> CodeEdge {
|
|
230
|
+
CodeEdge {
|
|
231
|
+
from: from.to_string(),
|
|
232
|
+
to: to.to_string(),
|
|
233
|
+
edge_type: edge_type.to_string(),
|
|
234
|
+
metadata: Some(metadata),
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// enrich.rs — semantic metadata markers applied as a final pass over the
|
|
2
|
+
// emitted nodes (mirrors go's enrich.go + python's edges/enrich.py).
|
|
3
|
+
//
|
|
4
|
+
// Memo-isolation rule (python incident 10): each marker scan derives from
|
|
5
|
+
// parsed facts independently — no shared memo map that could carry a cached
|
|
6
|
+
// miss between scans.
|
|
7
|
+
//
|
|
8
|
+
// Markers:
|
|
9
|
+
// isModel / modelType / tableName / tableColumns — Diesel structs
|
|
10
|
+
// isHandler / handlerKind — targets of HANDLES edges (routes.rs emitted)
|
|
11
|
+
// isPublic / receiver / isAsync — already set in nodes.rs
|
|
12
|
+
|
|
13
|
+
use crate::contract::CodeNode;
|
|
14
|
+
use crate::extractor::ParsedRepo;
|
|
15
|
+
|
|
16
|
+
pub fn enrich_nodes(
|
|
17
|
+
repo: &ParsedRepo,
|
|
18
|
+
nodes: &mut [CodeNode],
|
|
19
|
+
handles_edges: &[crate::contract::CodeEdge],
|
|
20
|
+
) {
|
|
21
|
+
// ── isHandler: any node that is a HANDLES target ──
|
|
22
|
+
let mut handler_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
23
|
+
for e in handles_edges {
|
|
24
|
+
if e.edge_type == "HANDLES" {
|
|
25
|
+
handler_ids.insert(e.to.clone());
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// handlerKind from the HANDLES edge metadata
|
|
29
|
+
let mut handler_kinds: std::collections::HashMap<String, String> =
|
|
30
|
+
std::collections::HashMap::new();
|
|
31
|
+
for e in handles_edges {
|
|
32
|
+
if e.edge_type == "HANDLES" {
|
|
33
|
+
if let Some(meta) = &e.metadata {
|
|
34
|
+
if let Some(k) = meta.get("handlerKind").and_then(|v| v.as_str()) {
|
|
35
|
+
handler_kinds.insert(e.to.clone(), k.to_string());
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Diesel model facts (re-derived from parsed items — orm_edges.rs
|
|
42
|
+
// computes the same map for edge targets; single source = facts) ──
|
|
43
|
+
let table_to_model = crate::orm_edges::table_to_model_map(repo);
|
|
44
|
+
// table name → columns (for tableColumns metadata)
|
|
45
|
+
let mut table_columns: std::collections::HashMap<String, Vec<String>> =
|
|
46
|
+
std::collections::HashMap::new();
|
|
47
|
+
for pf in &repo.files {
|
|
48
|
+
for t in &pf.tables {
|
|
49
|
+
table_columns
|
|
50
|
+
.entry(t.name.clone())
|
|
51
|
+
.or_default()
|
|
52
|
+
.extend(t.columns.iter().map(|(n, _)| n.clone()));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// model id → table name (looked up via the item facts)
|
|
57
|
+
let mut model_table: std::collections::HashMap<String, String> =
|
|
58
|
+
std::collections::HashMap::new();
|
|
59
|
+
for pf in &repo.files {
|
|
60
|
+
if pf.is_test_file {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
for item in &pf.items {
|
|
64
|
+
if item.kind == crate::parser::KIND_STRUCT && !item.is_test {
|
|
65
|
+
let id = crate::lookup::struct_node_id(&pf.rel_path, &item.name);
|
|
66
|
+
if table_to_model.values().any(|v| v == &id) {
|
|
67
|
+
let tn = crate::orm_edges::diesel_table_name(item);
|
|
68
|
+
if !tn.is_empty() {
|
|
69
|
+
model_table.insert(id, tn);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for n in nodes.iter_mut() {
|
|
77
|
+
if n.node_type != "FILE" && n.node_type != "TEST" {
|
|
78
|
+
if handler_ids.contains(&n.id) {
|
|
79
|
+
n.metadata.insert("isHandler".to_string(), true.into());
|
|
80
|
+
if let Some(k) = handler_kinds.get(&n.id) {
|
|
81
|
+
n.metadata.insert(
|
|
82
|
+
"handlerKind".to_string(),
|
|
83
|
+
serde_json::Value::from(k.clone()),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if table_to_model.values().any(|v| v == &n.id) {
|
|
88
|
+
n.metadata.insert("isModel".to_string(), true.into());
|
|
89
|
+
n.metadata.insert("modelType".to_string(), "diesel".into());
|
|
90
|
+
if let Some(tn) = model_table.get(&n.id) {
|
|
91
|
+
n.metadata
|
|
92
|
+
.insert("tableName".to_string(), serde_json::Value::from(tn.clone()));
|
|
93
|
+
if let Some(cols) = table_columns.get(tn) {
|
|
94
|
+
n.metadata.insert(
|
|
95
|
+
"tableColumns".to_string(),
|
|
96
|
+
serde_json::Value::Array(
|
|
97
|
+
cols.iter()
|
|
98
|
+
.map(|c| serde_json::Value::from(c.clone()))
|
|
99
|
+
.collect(),
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|