devlensio 0.6.1 → 1.0.0
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 +116 -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 +36 -48
- 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,233 @@
|
|
|
1
|
+
// inheritance.rs — IMPLEMENTS + EXTENDS edges (mirrors go/inheritance.go).
|
|
2
|
+
//
|
|
3
|
+
// IMPLEMENTS (documented Rust semantics — impl blocks ARE the mechanism):
|
|
4
|
+
// `impl Trait for Type` → IMPLEMENTS Type→Trait (local TRAIT node; external
|
|
5
|
+
// trait → gated [crate]/name::Trait member node —
|
|
6
|
+
// parity with Go's external gorm.Model embeds)
|
|
7
|
+
// plain `impl Type` → IMPLEMENTS IMPL_BLOCK→STRUCT (the tracker/
|
|
8
|
+
// phase-5.html convention: the impl block attaches
|
|
9
|
+
// to its target type)
|
|
10
|
+
// EXTENDS:
|
|
11
|
+
// trait supertraits `trait Child: Parent` → EXTENDS Child→Parent (Rust's
|
|
12
|
+
// documented inheritance mechanism)
|
|
13
|
+
// #[derive(LocalTrait)] → EXTENDS struct→trait when the derive resolves to
|
|
14
|
+
// a LOCAL trait (rare; flagged heuristic)
|
|
15
|
+
|
|
16
|
+
use crate::contract::{edge, CodeEdge};
|
|
17
|
+
use crate::extractor::{Options, ParsedRepo};
|
|
18
|
+
use crate::fingerprint::ParsedManifest;
|
|
19
|
+
use crate::lookup::LookupMaps;
|
|
20
|
+
use crate::module_map::ModuleMap;
|
|
21
|
+
use crate::parser::ParsedFile;
|
|
22
|
+
use crate::thirdparty::ThirdPartyRegistry;
|
|
23
|
+
|
|
24
|
+
pub struct InheritanceOutput {
|
|
25
|
+
pub edges: Vec<CodeEdge>,
|
|
26
|
+
pub third_party_nodes: Vec<crate::contract::CodeNode>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub fn detect_inheritance(
|
|
30
|
+
repo: &ParsedRepo,
|
|
31
|
+
mf: &ParsedManifest,
|
|
32
|
+
module_map: &ModuleMap,
|
|
33
|
+
lookup: &LookupMaps,
|
|
34
|
+
tp: &mut ThirdPartyRegistry,
|
|
35
|
+
_opts: &Options,
|
|
36
|
+
) -> InheritanceOutput {
|
|
37
|
+
let mut edges = vec![];
|
|
38
|
+
let mut nodes = vec![];
|
|
39
|
+
for pf in &repo.files {
|
|
40
|
+
if pf.is_test_file {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
for item in &pf.items {
|
|
44
|
+
if item.is_test {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
match item.kind {
|
|
48
|
+
crate::parser::KIND_IMPL_BLOCK => {
|
|
49
|
+
let impl_id = crate::lookup::node_id_for_item(&pf.rel_path, item);
|
|
50
|
+
let target = &item.impl_target;
|
|
51
|
+
if target.is_empty() {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if let Some(trait_path) = &item.impl_trait_path {
|
|
55
|
+
// impl Trait for Type → Type IMPLEMENTS Trait
|
|
56
|
+
if let Some(type_id) = resolve_type_id(target, pf, module_map, lookup) {
|
|
57
|
+
if let Some(trait_id) = resolve_trait_id(
|
|
58
|
+
trait_path, pf, module_map, lookup, mf, tp, &mut nodes,
|
|
59
|
+
) {
|
|
60
|
+
edges.push(edge(&type_id, &trait_id, "IMPLEMENTS"));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
// plain impl Type → IMPL_BLOCK IMPLEMENTS STRUCT
|
|
65
|
+
if let Some(type_id) = resolve_type_id(target, pf, module_map, lookup) {
|
|
66
|
+
edges.push(edge(&impl_id, &type_id, "IMPLEMENTS"));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
crate::parser::KIND_TRAIT => {
|
|
71
|
+
// supertraits: trait Child: Parent → EXTENDS Child→Parent
|
|
72
|
+
let child_id = crate::lookup::node_id_for_item(&pf.rel_path, item);
|
|
73
|
+
for st in &item.supertraits {
|
|
74
|
+
if let Some(parent_id) =
|
|
75
|
+
resolve_trait_id(st, pf, module_map, lookup, mf, tp, &mut nodes)
|
|
76
|
+
{
|
|
77
|
+
edges.push(edge(&child_id, &parent_id, "EXTENDS"));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
crate::parser::KIND_STRUCT | crate::parser::KIND_ENUM => {
|
|
82
|
+
// derives resolving to LOCAL traits → EXTENDS (heuristic)
|
|
83
|
+
let id = crate::lookup::node_id_for_item(&pf.rel_path, item);
|
|
84
|
+
for d in &item.derives {
|
|
85
|
+
if is_std_derive(d) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
let base_module = crate::walker::module_path_for_file(&pf.rel_path);
|
|
89
|
+
if let Some((file, rest)) = module_map.resolve(d, &base_module) {
|
|
90
|
+
let name = if rest.is_empty() {
|
|
91
|
+
d.rsplit("::").next().unwrap_or(d)
|
|
92
|
+
} else {
|
|
93
|
+
rest.split("::").next().unwrap_or(&rest)
|
|
94
|
+
};
|
|
95
|
+
let file_module = crate::walker::module_path_for_file(&file);
|
|
96
|
+
let key = if file_module.is_empty() {
|
|
97
|
+
name.to_string()
|
|
98
|
+
} else {
|
|
99
|
+
format!("{}::{}", file_module, name)
|
|
100
|
+
};
|
|
101
|
+
if let Some(tid) = lookup.item_by_module_name.get(&key).cloned() {
|
|
102
|
+
if lookup
|
|
103
|
+
.get(&tid)
|
|
104
|
+
.map(|n| n.node_type == "TRAIT")
|
|
105
|
+
.unwrap_or(false)
|
|
106
|
+
{
|
|
107
|
+
edges.push(edge(&id, &tid, "EXTENDS"));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
_ => {}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let _ = mf;
|
|
118
|
+
InheritanceOutput {
|
|
119
|
+
edges,
|
|
120
|
+
third_party_nodes: nodes,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Common derives that are never local traits (std + well-known derives).
|
|
125
|
+
fn is_std_derive(d: &str) -> bool {
|
|
126
|
+
matches!(
|
|
127
|
+
d,
|
|
128
|
+
"Debug"
|
|
129
|
+
| "Clone"
|
|
130
|
+
| "Copy"
|
|
131
|
+
| "PartialEq"
|
|
132
|
+
| "Eq"
|
|
133
|
+
| "PartialOrd"
|
|
134
|
+
| "Ord"
|
|
135
|
+
| "Hash"
|
|
136
|
+
| "Default"
|
|
137
|
+
| "Serialize"
|
|
138
|
+
| "Deserialize"
|
|
139
|
+
| "Queryable"
|
|
140
|
+
| "Insertable"
|
|
141
|
+
| "QueryableByName"
|
|
142
|
+
| "Associations"
|
|
143
|
+
| "Identifiable"
|
|
144
|
+
| "AsChangeset"
|
|
145
|
+
| "Selectable"
|
|
146
|
+
| "FromRow"
|
|
147
|
+
| "DebugWith"
|
|
148
|
+
| "thiserror::Error"
|
|
149
|
+
| "Error"
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Resolve a type path (`User`, `models::User`, `crate::models::user::User`)
|
|
154
|
+
/// to its STRUCT/ENUM node id.
|
|
155
|
+
fn resolve_type_id(
|
|
156
|
+
target: &str,
|
|
157
|
+
pf: &ParsedFile,
|
|
158
|
+
module_map: &ModuleMap,
|
|
159
|
+
lookup: &LookupMaps,
|
|
160
|
+
) -> Option<String> {
|
|
161
|
+
let base_module = crate::walker::module_path_for_file(&pf.rel_path);
|
|
162
|
+
if let Some((file, rest)) = module_map.resolve(target, &base_module) {
|
|
163
|
+
let file_module = crate::walker::module_path_for_file(&file);
|
|
164
|
+
let name = if rest.is_empty() {
|
|
165
|
+
target.rsplit("::").next().unwrap_or(target).to_string()
|
|
166
|
+
} else {
|
|
167
|
+
rest.split("::").next().unwrap_or(&rest).to_string()
|
|
168
|
+
};
|
|
169
|
+
let key = if file_module.is_empty() {
|
|
170
|
+
name.clone()
|
|
171
|
+
} else {
|
|
172
|
+
format!("{}::{}", file_module, name)
|
|
173
|
+
};
|
|
174
|
+
if let Some(id) = lookup.item_by_module_name.get(&key) {
|
|
175
|
+
return Some(id.clone());
|
|
176
|
+
}
|
|
177
|
+
// generic target like `Vec<T>` — strip generics, try the base name
|
|
178
|
+
if let Some(open) = name.find('<') {
|
|
179
|
+
let base = name[..open].to_string();
|
|
180
|
+
let key = if file_module.is_empty() {
|
|
181
|
+
base.clone()
|
|
182
|
+
} else {
|
|
183
|
+
format!("{}::{}", file_module, base)
|
|
184
|
+
};
|
|
185
|
+
return lookup.item_by_module_name.get(&key).cloned();
|
|
186
|
+
}
|
|
187
|
+
return None;
|
|
188
|
+
}
|
|
189
|
+
// same-file name
|
|
190
|
+
lookup.closest_by_path(target, &pf.rel_path)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Resolve a trait path to its node id — local TRAIT node, or (external) a
|
|
194
|
+
/// gated [crate]/name::Trait member node.
|
|
195
|
+
fn resolve_trait_id(
|
|
196
|
+
trait_path: &str,
|
|
197
|
+
pf: &ParsedFile,
|
|
198
|
+
module_map: &ModuleMap,
|
|
199
|
+
lookup: &LookupMaps,
|
|
200
|
+
mf: &ParsedManifest,
|
|
201
|
+
tp: &mut ThirdPartyRegistry,
|
|
202
|
+
nodes: &mut Vec<crate::contract::CodeNode>,
|
|
203
|
+
) -> Option<String> {
|
|
204
|
+
let base_module = crate::walker::module_path_for_file(&pf.rel_path);
|
|
205
|
+
if let Some((file, rest)) = module_map.resolve(trait_path, &base_module) {
|
|
206
|
+
let file_module = crate::walker::module_path_for_file(&file);
|
|
207
|
+
let name = if rest.is_empty() {
|
|
208
|
+
trait_path.rsplit("::").next().unwrap_or(trait_path)
|
|
209
|
+
} else {
|
|
210
|
+
rest.split("::").next().unwrap_or(&rest)
|
|
211
|
+
};
|
|
212
|
+
let key = if file_module.is_empty() {
|
|
213
|
+
name.to_string()
|
|
214
|
+
} else {
|
|
215
|
+
format!("{}::{}", file_module, name)
|
|
216
|
+
};
|
|
217
|
+
return lookup.item_by_module_name.get(&key).cloned();
|
|
218
|
+
}
|
|
219
|
+
// external trait (serde::Serialize) → gated [crate]/name::Trait member
|
|
220
|
+
let first = crate::thirdparty::first_segment(trait_path);
|
|
221
|
+
if crate::thirdparty::is_std(first) {
|
|
222
|
+
return None;
|
|
223
|
+
}
|
|
224
|
+
if !crate::thirdparty::is_external_crate(first, mf) {
|
|
225
|
+
return None;
|
|
226
|
+
}
|
|
227
|
+
if let Some(node) = tp.member_node(first, &trait_path[first.len() + 1..]) {
|
|
228
|
+
let id = node.id.clone();
|
|
229
|
+
nodes.push(node);
|
|
230
|
+
return Some(id);
|
|
231
|
+
}
|
|
232
|
+
None
|
|
233
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// lookup.rs — LookupMaps: ONE shared index built once from parsed facts,
|
|
2
|
+
// consumed by every edge detector. Edge resolution is dict lookups only —
|
|
3
|
+
// never re-walks ASTs (playbook rule). Mirrors go/lookup.go.
|
|
4
|
+
|
|
5
|
+
use crate::contract::CodeNode;
|
|
6
|
+
use crate::parser::{ParsedFile, ParsedItem};
|
|
7
|
+
use std::collections::HashMap;
|
|
8
|
+
|
|
9
|
+
pub struct LookupMaps {
|
|
10
|
+
/// node id → node (all emitted nodes register here)
|
|
11
|
+
pub node_by_id: HashMap<String, CodeNode>,
|
|
12
|
+
/// file node per rel path
|
|
13
|
+
pub file_nodes_by_path: HashMap<String, String>,
|
|
14
|
+
/// plain name → node ids (collisions disambiguated by closest_by_path)
|
|
15
|
+
pub nodes_by_name: HashMap<String, Vec<String>>,
|
|
16
|
+
/// symbol maps: rel file → alias → resolved use target
|
|
17
|
+
/// (e.g. "User" → "crate::models::user::User") — the bridge for
|
|
18
|
+
/// Calls/Routes/Tests (mirrors python's symbol_maps)
|
|
19
|
+
pub symbol_maps: HashMap<String, HashMap<String, String>>,
|
|
20
|
+
/// glob use targets per file: alias-less → base path ("crate::models")
|
|
21
|
+
pub glob_maps: HashMap<String, Vec<String>>,
|
|
22
|
+
/// module_path::Name → node id (exact resolution; Rust names are unique
|
|
23
|
+
/// per module, so no closest_by_path needed here)
|
|
24
|
+
pub item_by_module_name: HashMap<String, String>,
|
|
25
|
+
/// impl owner (target type) → METHOD ids (for receiver-free lookups)
|
|
26
|
+
pub methods_by_owner: HashMap<String, Vec<String>>,
|
|
27
|
+
/// trait name (last segment) → METHOD ids from `impl Trait for ...` blocks
|
|
28
|
+
/// — resolves `Entity::table_name(&x)` trait-dispatch calls
|
|
29
|
+
pub methods_by_trait: HashMap<String, Vec<String>>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl LookupMaps {
|
|
33
|
+
pub fn new() -> Self {
|
|
34
|
+
LookupMaps {
|
|
35
|
+
node_by_id: HashMap::new(),
|
|
36
|
+
file_nodes_by_path: HashMap::new(),
|
|
37
|
+
nodes_by_name: HashMap::new(),
|
|
38
|
+
symbol_maps: HashMap::new(),
|
|
39
|
+
glob_maps: HashMap::new(),
|
|
40
|
+
item_by_module_name: HashMap::new(),
|
|
41
|
+
methods_by_owner: HashMap::new(),
|
|
42
|
+
methods_by_trait: HashMap::new(),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub fn register(&mut self, node: CodeNode) {
|
|
47
|
+
let id = node.id.clone();
|
|
48
|
+
let name = node.name.clone();
|
|
49
|
+
self.node_by_id.insert(id.clone(), node);
|
|
50
|
+
self.nodes_by_name.entry(name).or_default().push(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub fn get(&self, id: &str) -> Option<&CodeNode> {
|
|
54
|
+
self.node_by_id.get(id)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// name lookup with file proximity: prefer nodes in the same file
|
|
58
|
+
/// (python/go closest_by_path trick).
|
|
59
|
+
pub fn closest_by_path(&self, name: &str, rel_path: &str) -> Option<String> {
|
|
60
|
+
let ids = self.nodes_by_name.get(name)?;
|
|
61
|
+
if ids.is_empty() {
|
|
62
|
+
return None;
|
|
63
|
+
}
|
|
64
|
+
if ids.len() == 1 {
|
|
65
|
+
return Some(ids[0].clone());
|
|
66
|
+
}
|
|
67
|
+
ids.iter()
|
|
68
|
+
.find(|id| id.starts_with(&format!("{}::", rel_path)))
|
|
69
|
+
.cloned()
|
|
70
|
+
.or_else(|| ids.first().cloned())
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── node id schemes (deterministic, file-scoped — mirrors go) ──
|
|
75
|
+
|
|
76
|
+
pub fn func_node_id(rel: &str, name: &str) -> String {
|
|
77
|
+
format!("{}::{}", rel, name)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pub fn method_node_id(rel: &str, owner: &str, method: &str, trait_path: Option<&str>) -> String {
|
|
81
|
+
match trait_path {
|
|
82
|
+
Some(t) => format!("{}::{}.{}::{}", rel, owner, method, last_seg(t)),
|
|
83
|
+
None => format!("{}::{}.{}", rel, owner, method),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pub fn struct_node_id(rel: &str, name: &str) -> String {
|
|
88
|
+
format!("{}::{}", rel, name)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pub fn enum_node_id(rel: &str, name: &str) -> String {
|
|
92
|
+
format!("{}::{}", rel, name)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pub fn trait_node_id(rel: &str, name: &str) -> String {
|
|
96
|
+
format!("{}::{}", rel, name)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pub fn impl_block_node_id(rel: &str, label: &str, start_line: i64) -> String {
|
|
100
|
+
format!("{}::{} [L{}]", rel, label, start_line)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pub fn third_party_id(crate_name: &str) -> String {
|
|
104
|
+
format!("[crate]/{}", crate_name)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
pub fn last_seg(p: &str) -> String {
|
|
108
|
+
p.rsplit("::").next().unwrap_or(p).to_string()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// The code-node id for a parsed item (shared by nodes.rs, calls.rs,
|
|
112
|
+
/// routes.rs — ONE source of truth for id computation).
|
|
113
|
+
pub fn node_id_for_item(rel: &str, item: &ParsedItem) -> String {
|
|
114
|
+
match item.kind {
|
|
115
|
+
crate::parser::KIND_FUNCTION => func_node_id(rel, &item.name),
|
|
116
|
+
crate::parser::KIND_METHOD => method_node_id(
|
|
117
|
+
rel,
|
|
118
|
+
&item.impl_target,
|
|
119
|
+
&item.name,
|
|
120
|
+
item.impl_trait_path.as_deref(),
|
|
121
|
+
),
|
|
122
|
+
crate::parser::KIND_STRUCT => struct_node_id(rel, &item.name),
|
|
123
|
+
crate::parser::KIND_ENUM => enum_node_id(rel, &item.name),
|
|
124
|
+
crate::parser::KIND_TRAIT => trait_node_id(rel, &item.name),
|
|
125
|
+
crate::parser::KIND_IMPL_BLOCK => {
|
|
126
|
+
let label = if item.impl_trait.is_some() {
|
|
127
|
+
format!(
|
|
128
|
+
"impl {} for {}",
|
|
129
|
+
item.impl_trait_path.as_deref().unwrap_or(""),
|
|
130
|
+
item.impl_target
|
|
131
|
+
)
|
|
132
|
+
} else {
|
|
133
|
+
format!("impl {}", item.impl_target)
|
|
134
|
+
};
|
|
135
|
+
impl_block_node_id(rel, &label, item.start_line)
|
|
136
|
+
}
|
|
137
|
+
_ => String::new(),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Build lookup maps from parsed facts. `files_by_rel` maps rel → ParsedFile.
|
|
142
|
+
pub fn build_lookup_maps(files: &[ParsedFile], nodes: &[CodeNode]) -> LookupMaps {
|
|
143
|
+
let mut l = LookupMaps::new();
|
|
144
|
+
|
|
145
|
+
// file nodes
|
|
146
|
+
for pf in files {
|
|
147
|
+
l.file_nodes_by_path
|
|
148
|
+
.insert(pf.rel_path.clone(), format!("file::{}", pf.rel_path));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// code nodes (already built by nodes.rs — register + index)
|
|
152
|
+
for n in nodes {
|
|
153
|
+
l.register(n.clone());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// symbol maps from use declarations
|
|
157
|
+
for pf in files {
|
|
158
|
+
let mut syms: HashMap<String, String> = HashMap::new();
|
|
159
|
+
let mut globs: Vec<String> = vec![];
|
|
160
|
+
for u in &pf.uses {
|
|
161
|
+
if u.glob {
|
|
162
|
+
globs.push(u.target.clone());
|
|
163
|
+
} else if !u.alias.is_empty() {
|
|
164
|
+
syms.insert(u.alias.clone(), u.target.clone());
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
l.symbol_maps.insert(pf.rel_path.clone(), syms);
|
|
168
|
+
l.glob_maps.insert(pf.rel_path.clone(), globs);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// item maps + impl indexes from parsed items
|
|
172
|
+
for pf in files {
|
|
173
|
+
let module_path = crate::walker::module_path_for_file(&pf.rel_path);
|
|
174
|
+
for item in &pf.items {
|
|
175
|
+
match item.kind {
|
|
176
|
+
crate::parser::KIND_STRUCT
|
|
177
|
+
| crate::parser::KIND_ENUM
|
|
178
|
+
| crate::parser::KIND_TRAIT => {
|
|
179
|
+
let key = if module_path.is_empty() {
|
|
180
|
+
item.name.clone()
|
|
181
|
+
} else {
|
|
182
|
+
format!("{}::{}", module_path, item.name)
|
|
183
|
+
};
|
|
184
|
+
// prefer the code-node id (impl_blocks have line labels)
|
|
185
|
+
let id = struct_node_id(&pf.rel_path, &item.name);
|
|
186
|
+
l.item_by_module_name.insert(key, id);
|
|
187
|
+
}
|
|
188
|
+
crate::parser::KIND_FUNCTION => {
|
|
189
|
+
let key = if module_path.is_empty() {
|
|
190
|
+
item.name.clone()
|
|
191
|
+
} else {
|
|
192
|
+
format!("{}::{}", module_path, item.name)
|
|
193
|
+
};
|
|
194
|
+
l.item_by_module_name
|
|
195
|
+
.insert(key, func_node_id(&pf.rel_path, &item.name));
|
|
196
|
+
}
|
|
197
|
+
crate::parser::KIND_METHOD => {
|
|
198
|
+
// owner = impl target (set by the impl block's item); the
|
|
199
|
+
// method ParsedItem carries it via impl_target? — methods
|
|
200
|
+
// are collected inside impl blocks; we resolve the owner
|
|
201
|
+
// from the enclosing impl in nodes.rs; here we just index
|
|
202
|
+
// what nodes.rs emitted via item_by_module_name too
|
|
203
|
+
let owner = &item.impl_target;
|
|
204
|
+
if !owner.is_empty() {
|
|
205
|
+
let mid = crate::lookup::method_node_id(
|
|
206
|
+
&pf.rel_path,
|
|
207
|
+
owner,
|
|
208
|
+
&item.name,
|
|
209
|
+
item.impl_trait_path.as_deref(),
|
|
210
|
+
);
|
|
211
|
+
l.methods_by_owner
|
|
212
|
+
.entry(owner.clone())
|
|
213
|
+
.or_default()
|
|
214
|
+
.push(mid.clone());
|
|
215
|
+
if let Some(t) = &item.impl_trait_path {
|
|
216
|
+
let tname = crate::lookup::last_seg(t);
|
|
217
|
+
l.methods_by_trait.entry(tname).or_default().push(mid);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
_ => {}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
l
|
|
226
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// main.rs — entry point: stdin JSON {repoPath, options} → stdout JSON
|
|
2
|
+
// ExtractorResult. JSON only on stdout; logs go to stderr. Mirrors
|
|
3
|
+
// extractors/go/main.go.
|
|
4
|
+
|
|
5
|
+
mod calls;
|
|
6
|
+
mod contract;
|
|
7
|
+
mod enrich;
|
|
8
|
+
mod extractor;
|
|
9
|
+
mod fingerprint;
|
|
10
|
+
mod imports;
|
|
11
|
+
mod inheritance;
|
|
12
|
+
mod lookup;
|
|
13
|
+
mod module_map;
|
|
14
|
+
mod nodes;
|
|
15
|
+
mod orm_edges;
|
|
16
|
+
mod parser;
|
|
17
|
+
mod routes;
|
|
18
|
+
mod tests;
|
|
19
|
+
mod thirdparty;
|
|
20
|
+
mod walker;
|
|
21
|
+
|
|
22
|
+
use std::io::Read;
|
|
23
|
+
|
|
24
|
+
fn main() {
|
|
25
|
+
let mut raw = String::new();
|
|
26
|
+
if let Err(e) = std::io::stdin().read_to_string(&mut raw) {
|
|
27
|
+
eprintln!("error reading stdin: {}", e);
|
|
28
|
+
std::process::exit(1);
|
|
29
|
+
}
|
|
30
|
+
let input: contract::ExtractorInput = match serde_json::from_str(&raw) {
|
|
31
|
+
Ok(v) => v,
|
|
32
|
+
Err(e) => {
|
|
33
|
+
eprintln!("bad input json: {}", e);
|
|
34
|
+
std::process::exit(1);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
if input.repo_path.is_empty() {
|
|
38
|
+
eprintln!("repoPath is required");
|
|
39
|
+
std::process::exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Options: the engine forwards includeThirdPartyLibs (both spellings are
|
|
43
|
+
// accepted: engine truth + contract.html docs alias).
|
|
44
|
+
let opts = extractor::Options::from_json(&input.options);
|
|
45
|
+
|
|
46
|
+
let result = extractor::run(&input.repo_path, &opts);
|
|
47
|
+
|
|
48
|
+
if let Err(e) = serde_json::to_writer(std::io::stdout(), &result) {
|
|
49
|
+
eprintln!("error marshaling result: {}", e);
|
|
50
|
+
std::process::exit(1);
|
|
51
|
+
}
|
|
52
|
+
println!();
|
|
53
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// module_map.rs — module path → file resolution (the Rust analog of Go's
|
|
2
|
+
// package grouping).
|
|
3
|
+
//
|
|
4
|
+
// Module paths are derived from the FILESYSTEM (walker::module_path_for_file):
|
|
5
|
+
// src/models/user.rs → module `models::user`
|
|
6
|
+
// `mod foo;` declarations are also recorded (mod_decls) but the map itself is
|
|
7
|
+
// filesystem-driven — deterministic by construction.
|
|
8
|
+
//
|
|
9
|
+
// Resolution rules (documented Rust module semantics):
|
|
10
|
+
// crate::a::b → absolute within the crate
|
|
11
|
+
// super::a → parent of the current file's module
|
|
12
|
+
// self::a → sibling of the current file's module
|
|
13
|
+
// bare `a::b` → try relative to the current module, then crate root
|
|
14
|
+
|
|
15
|
+
use crate::parser::ParsedFile;
|
|
16
|
+
use std::collections::HashMap;
|
|
17
|
+
|
|
18
|
+
pub struct ModuleMap {
|
|
19
|
+
/// module path → rel file path (all .rs files, empty path = crate root)
|
|
20
|
+
map: HashMap<String, String>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl ModuleMap {
|
|
24
|
+
pub fn build(files: &[ParsedFile]) -> Self {
|
|
25
|
+
let mut map = HashMap::new();
|
|
26
|
+
for pf in files {
|
|
27
|
+
let mpath = crate::walker::module_path_for_file(&pf.rel_path);
|
|
28
|
+
map.insert(mpath, pf.rel_path.clone());
|
|
29
|
+
}
|
|
30
|
+
ModuleMap { map }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Is `name` a module reachable from `base_module` (or the crate root)?
|
|
34
|
+
/// Resolve a use/call path from a file whose module is `base_module`.
|
|
35
|
+
/// Returns (file rel path, remaining item path) for the LONGEST module
|
|
36
|
+
/// prefix of the path. External crates / std → None.
|
|
37
|
+
///
|
|
38
|
+
/// `crate::models::user::User` (base "models::db") →
|
|
39
|
+
/// candidates: "models::user" (module) + "User" (item)
|
|
40
|
+
pub fn resolve(&self, path: &str, base_module: &str) -> Option<(String, String)> {
|
|
41
|
+
let (crate_abs, stripped) = normalize(path, base_module);
|
|
42
|
+
if crate_abs {
|
|
43
|
+
// absolute within this crate: try longest module prefix
|
|
44
|
+
return self.longest_prefix(&stripped);
|
|
45
|
+
}
|
|
46
|
+
// bare path: relative to base module first, then crate root
|
|
47
|
+
if !base_module.is_empty() {
|
|
48
|
+
let rel = format!("{}::{}", base_module, stripped);
|
|
49
|
+
if let Some(hit) = self.longest_prefix(&rel) {
|
|
50
|
+
return Some(hit);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
self.longest_prefix(&stripped)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Resolve a `use` path (edition-2018 semantics): bare first segments are
|
|
57
|
+
/// crate-root-absolute — `use axum::routing::get` NEVER means "relative
|
|
58
|
+
/// to this module"; relative requires explicit `self::`/`super::` (which
|
|
59
|
+
/// normalize() handles). Call paths (calls.rs) keep resolve()'s
|
|
60
|
+
/// relative-first ladder; use paths MUST NOT (a bare path whose first
|
|
61
|
+
/// segment collides with the current module would self-resolve).
|
|
62
|
+
pub fn resolve_use(&self, path: &str, base_module: &str) -> Option<(String, String)> {
|
|
63
|
+
let (crate_abs, stripped) = normalize(path, base_module);
|
|
64
|
+
if crate_abs {
|
|
65
|
+
return self.longest_prefix(&stripped);
|
|
66
|
+
}
|
|
67
|
+
self.longest_prefix(&stripped)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Try the full path as a module, then backtrack segment by segment.
|
|
71
|
+
/// `models::user::User` → module `models::user`, item `User`.
|
|
72
|
+
fn longest_prefix(&self, path: &str) -> Option<(String, String)> {
|
|
73
|
+
let segs: Vec<&str> = path.split("::").collect();
|
|
74
|
+
for cut in (1..=segs.len()).rev() {
|
|
75
|
+
let module = segs[..cut].join("::");
|
|
76
|
+
if let Some(file) = self.map.get(&module) {
|
|
77
|
+
let rest = segs[cut..].join("::");
|
|
78
|
+
return Some((file.clone(), rest));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
None
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Normalize a path against the base module. Returns (is_crate_absolute, path).
|
|
86
|
+
fn normalize(path: &str, base_module: &str) -> (bool, String) {
|
|
87
|
+
// bare root/self/super tokens (glob targets like `use crate::*`)
|
|
88
|
+
if path == "crate" {
|
|
89
|
+
return (true, String::new());
|
|
90
|
+
}
|
|
91
|
+
if path == "self" {
|
|
92
|
+
return (true, base_module.to_string());
|
|
93
|
+
}
|
|
94
|
+
if path == "super" {
|
|
95
|
+
let mut base_parts: Vec<&str> = base_module.split("::").collect();
|
|
96
|
+
base_parts.pop();
|
|
97
|
+
return (true, base_parts.join("::"));
|
|
98
|
+
}
|
|
99
|
+
if let Some(rest) = path.strip_prefix("crate::") {
|
|
100
|
+
return (true, rest.to_string());
|
|
101
|
+
}
|
|
102
|
+
if let Some(rest) = path.strip_prefix("self::") {
|
|
103
|
+
let p = if base_module.is_empty() {
|
|
104
|
+
rest.to_string()
|
|
105
|
+
} else {
|
|
106
|
+
format!("{}::{}", base_module, rest)
|
|
107
|
+
};
|
|
108
|
+
return (true, p);
|
|
109
|
+
}
|
|
110
|
+
if let Some(rest) = path.strip_prefix("super::") {
|
|
111
|
+
// count supers
|
|
112
|
+
let mut count = 1;
|
|
113
|
+
let mut rest = rest.to_string();
|
|
114
|
+
while let Some(r2) = rest.strip_prefix("super::") {
|
|
115
|
+
count += 1;
|
|
116
|
+
rest = r2.to_string();
|
|
117
|
+
}
|
|
118
|
+
let mut base_parts: Vec<&str> = base_module.split("::").collect();
|
|
119
|
+
for _ in 0..count {
|
|
120
|
+
base_parts.pop();
|
|
121
|
+
}
|
|
122
|
+
let p = if base_parts.is_empty() {
|
|
123
|
+
rest
|
|
124
|
+
} else {
|
|
125
|
+
format!("{}::{}", base_parts.join("::"), rest)
|
|
126
|
+
};
|
|
127
|
+
return (true, p);
|
|
128
|
+
}
|
|
129
|
+
// bare — resolve() decides relative-vs-root
|
|
130
|
+
(false, path.to_string())
|
|
131
|
+
}
|