devlensio 1.0.3 → 1.1.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/dist/config/backcompat.test.d.ts +1 -0
- package/dist/config/backcompat.test.js +188 -0
- package/dist/config/index.d.ts +5 -3
- package/dist/config/index.js +37 -54
- package/dist/config/providers/file.d.ts +3 -1
- package/dist/config/providers/file.js +47 -53
- package/dist/config/providers/providers.default.js +0 -1
- package/dist/config/providers/request.js +1 -1
- package/dist/config/types.d.ts +0 -1
- package/dist/config/types.js +2 -18
- package/dist/graph/edges/callEdges.js +97 -5
- package/dist/graph/edges/callEdges.test.js +154 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/jobs/queue/memory.js +12 -1
- package/dist/parser/extractors/classes.js +23 -4
- package/dist/parser/extractors/functions.d.ts +10 -3
- package/dist/parser/extractors/functions.js +135 -65
- package/dist/parser/extractors/hooks.js +5 -29
- package/dist/parser/extractors/objectMethods.js +3 -4
- package/dist/parser/index.js +11 -6
- package/dist/parser/overloads.d.ts +2 -0
- package/dist/parser/overloads.js +96 -0
- package/dist/parser/overloads.test.d.ts +1 -0
- package/dist/parser/overloads.test.js +175 -0
- package/dist/parser/typeUtils.d.ts +2 -0
- package/dist/parser/typeUtils.js +2 -0
- package/dist/summarizer/providers/index.js +4 -4
- package/dist/summarizer/providers/openai.js +1 -1
- package/dist/types.d.ts +6 -0
- 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/calls.go +134 -2
- package/extractors/go/contract.go +1 -0
- package/extractors/go/exports.go +107 -0
- package/extractors/go/exports_test.go +64 -0
- package/extractors/go/extractor.go +1 -0
- package/extractors/go/lookup.go +63 -7
- package/extractors/go/nodes.go +28 -0
- package/extractors/go/parser.go +51 -8
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/extractors/java/src/devlens/extractor/ExportsMapBuilder.java +148 -0
- package/extractors/java/src/devlens/extractor/Extractor.java +34 -0
- package/extractors/java/src/devlens/extractor/LookupMaps.java +12 -0
- package/extractors/java/src/devlens/extractor/Overloads.java +290 -0
- package/extractors/java/src/devlens/extractor/Parser.java +39 -0
- package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +9 -0
- package/extractors/java/src/devlens/extractor/edges/Calls.java +42 -36
- package/extractors/java/src/devlens/extractor/edges/Routes.java +10 -3
- package/extractors/python/devlens_extractors_python/edges/calls.py +127 -26
- package/extractors/python/devlens_extractors_python/exports_map.py +263 -0
- package/extractors/python/devlens_extractors_python/extractor.py +7 -1
- package/extractors/python/devlens_extractors_python/lookup.py +6 -0
- package/extractors/python/devlens_extractors_python/parser/__init__.py +6 -0
- package/extractors/python/devlens_extractors_python/parser/functions.py +103 -6
- package/extractors/python/devlens_extractors_python/parser/overloads.py +105 -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/src/calls.rs +27 -16
- package/extractors/rust/src/contract.rs +2 -0
- package/extractors/rust/src/exports.rs +387 -0
- package/extractors/rust/src/exports_test.rs +105 -0
- package/extractors/rust/src/extractor.rs +2 -0
- package/extractors/rust/src/lookup.rs +60 -0
- package/extractors/rust/src/main.rs +4 -0
- package/extractors/rust/src/nodes.rs +19 -0
- package/extractors/rust/src/parser.rs +66 -1
- package/extractors/rust/src/routes.rs +2 -1
- package/package.json +10 -3
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// exports.rs — the flattened public surface of a Rust crate, shipped in the
|
|
2
|
+
// ExtractorResult for cross-repo linking (docs/EXPORTS_MAP.md).
|
|
3
|
+
//
|
|
4
|
+
// Rust-specific concerns:
|
|
5
|
+
// - The "door" is the crate root (src/lib.rs or src/main.rs). The map key
|
|
6
|
+
// is the path an importer writes after the crate name:
|
|
7
|
+
// pub fn run() → "run"
|
|
8
|
+
// pub mod foo + pub items → "foo" (file node) and "foo::Item"
|
|
9
|
+
// pub use a::B as C; → "C" (binding-resolved, name-swap safe)
|
|
10
|
+
// pub use foo::*; → foo's pub items at the same level
|
|
11
|
+
// pub methods of pub types → "Type.method"
|
|
12
|
+
// - `pub use` chains are flattened recursively through mod.rs files with a
|
|
13
|
+
// visited guard — consumers of the map never walk re-export chains.
|
|
14
|
+
// - Private mods (`mod foo;`) contribute nothing; only `pub mod` opens a
|
|
15
|
+
// subpath. Inline `pub mod foo { ... }` bodies are out of scope for v1
|
|
16
|
+
// (their items are parsed inside the same file's mod tree, not top-level).
|
|
17
|
+
// - Values are ALWAYS arrays of proper nodeIds matching nodes.rs exactly
|
|
18
|
+
// (func/struct/enum/trait: rel::name; inherent methods: rel::Type.method;
|
|
19
|
+
// trait-impl methods are NOT surface — the trait defines them).
|
|
20
|
+
// - Rust rejects duplicate re-exports at compile time, so ambiguousNames is
|
|
21
|
+
// a safety net that should stay empty.
|
|
22
|
+
// - Pure-test files and #[test] items are excluded.
|
|
23
|
+
// - Workspace members other than the root crate are out of scope for v1.
|
|
24
|
+
|
|
25
|
+
use crate::module_map::ModuleMap;
|
|
26
|
+
use crate::parser::{ParsedFile, ParsedItem, KIND_ENUM, KIND_FUNCTION, KIND_METHOD, KIND_STRUCT, KIND_TRAIT};
|
|
27
|
+
use serde::Serialize;
|
|
28
|
+
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
|
29
|
+
|
|
30
|
+
#[derive(Serialize, Clone)]
|
|
31
|
+
#[serde(rename_all = "camelCase")]
|
|
32
|
+
pub struct ExportsMap {
|
|
33
|
+
pub exports: BTreeMap<String, BTreeMap<String, Vec<String>>>,
|
|
34
|
+
pub ambiguous_names: BTreeMap<String, Vec<String>>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub fn build_exports_map(files: &[ParsedFile], module_map: &ModuleMap) -> Option<ExportsMap> {
|
|
38
|
+
let root = files
|
|
39
|
+
.iter()
|
|
40
|
+
.find(|f| f.rel_path == "src/lib.rs")
|
|
41
|
+
.or_else(|| files.iter().find(|f| f.rel_path == "src/main.rs"))?;
|
|
42
|
+
|
|
43
|
+
let index = ItemIndex::build(files);
|
|
44
|
+
let mut ctx = Ctx {
|
|
45
|
+
files,
|
|
46
|
+
module_map,
|
|
47
|
+
index,
|
|
48
|
+
by_rel: files.iter().map(|f| (f.rel_path.clone(), f)).collect(),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let mut exports: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
|
52
|
+
let mut ambiguous: Vec<String> = Vec::new();
|
|
53
|
+
let mut visited = BTreeSet::new();
|
|
54
|
+
collect_surface(&ctx, root, "", &mut exports, &mut ambiguous, &mut visited);
|
|
55
|
+
|
|
56
|
+
if exports.is_empty() {
|
|
57
|
+
return None;
|
|
58
|
+
}
|
|
59
|
+
Some(ExportsMap {
|
|
60
|
+
exports: BTreeMap::from([(".".to_string(), exports)]),
|
|
61
|
+
ambiguous_names: if ambiguous.is_empty() {
|
|
62
|
+
BTreeMap::new()
|
|
63
|
+
} else {
|
|
64
|
+
BTreeMap::from([(".".to_string(), ambiguous)])
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
struct Ctx<'a> {
|
|
70
|
+
files: &'a [ParsedFile],
|
|
71
|
+
module_map: &'a ModuleMap,
|
|
72
|
+
index: ItemIndex,
|
|
73
|
+
by_rel: HashMap<String, &'a ParsedFile>,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// (rel_path, item_name) → nodeIds, reconstructed with the SAME id helpers
|
|
77
|
+
/// nodes.rs uses, so map values are guaranteed joinable.
|
|
78
|
+
struct ItemIndex {
|
|
79
|
+
by_file: HashMap<String, HashMap<String, Vec<String>>>,
|
|
80
|
+
pub_types: BTreeSet<String>,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
impl ItemIndex {
|
|
84
|
+
fn build(files: &[ParsedFile]) -> Self {
|
|
85
|
+
let mut by_file: HashMap<String, HashMap<String, Vec<String>>> = HashMap::new();
|
|
86
|
+
let mut pub_types = BTreeSet::new();
|
|
87
|
+
for pf in files {
|
|
88
|
+
if pf.is_pure_test_file {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
let mut names: HashMap<String, Vec<String>> = HashMap::new();
|
|
92
|
+
for item in &pf.items {
|
|
93
|
+
if item.is_test {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if let Some(id) = surface_node_id(pf, item) {
|
|
97
|
+
match item.kind {
|
|
98
|
+
KIND_STRUCT | KIND_ENUM | KIND_TRAIT => {
|
|
99
|
+
if item.is_pub {
|
|
100
|
+
pub_types.insert(item.name.clone());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
_ => {}
|
|
104
|
+
}
|
|
105
|
+
names.entry(surface_key(item)).or_default().push(id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
by_file.insert(pf.rel_path.clone(), names);
|
|
109
|
+
}
|
|
110
|
+
ItemIndex { by_file, pub_types }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn get(&self, rel: &str, key: &str) -> Option<Vec<String>> {
|
|
114
|
+
self.by_file.get(rel)?.get(key).cloned()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn surface_key(item: &ParsedItem) -> String {
|
|
119
|
+
if item.kind == KIND_METHOD && !item.impl_target.is_empty() {
|
|
120
|
+
format!("{}.{}", item.impl_target, item.name)
|
|
121
|
+
} else {
|
|
122
|
+
item.name.clone()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// nodeIds only for kinds that are import surface (impl blocks etc. excluded).
|
|
127
|
+
fn surface_node_id(pf: &ParsedFile, item: &ParsedItem) -> Option<String> {
|
|
128
|
+
match item.kind {
|
|
129
|
+
KIND_FUNCTION => Some(crate::lookup::func_node_id(&pf.rel_path, &item.name)),
|
|
130
|
+
KIND_METHOD if item.impl_trait_path.is_none() && !item.impl_target.is_empty() => {
|
|
131
|
+
Some(crate::lookup::method_node_id(
|
|
132
|
+
&pf.rel_path,
|
|
133
|
+
&item.impl_target,
|
|
134
|
+
&item.name,
|
|
135
|
+
None,
|
|
136
|
+
))
|
|
137
|
+
}
|
|
138
|
+
KIND_STRUCT => Some(crate::lookup::struct_node_id(&pf.rel_path, &item.name)),
|
|
139
|
+
KIND_ENUM => Some(crate::lookup::enum_node_id(&pf.rel_path, &item.name)),
|
|
140
|
+
KIND_TRAIT => Some(crate::lookup::trait_node_id(&pf.rel_path, &item.name)),
|
|
141
|
+
_ => None,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fn collect_surface(
|
|
146
|
+
ctx: &Ctx,
|
|
147
|
+
file: &ParsedFile,
|
|
148
|
+
prefix: &str,
|
|
149
|
+
exports: &mut BTreeMap<String, Vec<String>>,
|
|
150
|
+
ambiguous: &mut Vec<String>,
|
|
151
|
+
visited: &mut BTreeSet<String>,
|
|
152
|
+
) {
|
|
153
|
+
let visit_key = format!("{}@{}", file.rel_path, prefix);
|
|
154
|
+
if !visited.insert(visit_key) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let base_module = crate::walker::module_path_for_file(&file.rel_path);
|
|
159
|
+
let top_level = top_level_item_keys(file);
|
|
160
|
+
|
|
161
|
+
for item in &file.items {
|
|
162
|
+
if !item.is_pub || item.is_test {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
match item.kind {
|
|
166
|
+
KIND_FUNCTION | KIND_STRUCT | KIND_ENUM | KIND_TRAIT => {
|
|
167
|
+
// parser flattens inline-mod contents into file.items; only
|
|
168
|
+
// DIRECT children of the file are crate surface (a `pub`
|
|
169
|
+
// struct inside a PRIVATE `mod` is not importable).
|
|
170
|
+
if !top_level.contains(&(item.kind.to_string(), item.name.clone())) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if let Some(ids) = ctx.index.get(&file.rel_path, &surface_key(item)) {
|
|
174
|
+
record(exports, ambiguous, &format!("{}{}", prefix, item.name), ids);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
KIND_METHOD => {
|
|
178
|
+
if item.impl_trait_path.is_none()
|
|
179
|
+
&& !item.impl_target.is_empty()
|
|
180
|
+
&& ctx.index.pub_types.contains(&item.impl_target)
|
|
181
|
+
{
|
|
182
|
+
if let Some(ids) = ctx.index.get(&file.rel_path, &surface_key(item)) {
|
|
183
|
+
record(
|
|
184
|
+
exports,
|
|
185
|
+
ambiguous,
|
|
186
|
+
&format!("{}{}", prefix, surface_key(item)),
|
|
187
|
+
ids,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
_ => {}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for u in &file.uses {
|
|
197
|
+
if u.glob {
|
|
198
|
+
if let Some((target_file, _rest)) = ctx.module_map.resolve_use(&u.target, &base_module) {
|
|
199
|
+
if let Some(pf) = ctx.by_rel.get(&target_file) {
|
|
200
|
+
if pf.rel_path != file.rel_path {
|
|
201
|
+
collect_surface(ctx, pf, prefix, exports, ambiguous, visited);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if let Some(ids) = resolve_use_target(ctx, file, &base_module, &u.target, &mut BTreeSet::new(), visited) {
|
|
208
|
+
record(exports, ambiguous, &format!("{}{}", prefix, u.alias), ids);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for m in pub_mods(file) {
|
|
213
|
+
let module = if base_module.is_empty() {
|
|
214
|
+
m.clone()
|
|
215
|
+
} else {
|
|
216
|
+
format!("{}::{}", base_module, m)
|
|
217
|
+
};
|
|
218
|
+
if let Some(pf) = ctx
|
|
219
|
+
.module_map
|
|
220
|
+
.resolve_use(&module, "")
|
|
221
|
+
.and_then(|(f, _)| ctx.by_rel.get(&f).copied())
|
|
222
|
+
{
|
|
223
|
+
record(exports, ambiguous, &format!("{}{}", prefix, m), vec![format!("file::{}", pf.rel_path)]);
|
|
224
|
+
collect_surface(ctx, pf, &format!("{}{}::", prefix, m), exports, ambiguous, visited);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// Resolve one `use` target path to definition nodeIds, following bindings
|
|
230
|
+
/// through mod.rs files. Binding-based — never string-matched against names
|
|
231
|
+
/// the target file happens to reuse.
|
|
232
|
+
fn resolve_use_target(
|
|
233
|
+
ctx: &Ctx,
|
|
234
|
+
from_file: &ParsedFile,
|
|
235
|
+
base_module: &str,
|
|
236
|
+
target: &str,
|
|
237
|
+
seen: &mut BTreeSet<(String, String)>,
|
|
238
|
+
visited: &mut BTreeSet<String>,
|
|
239
|
+
) -> Option<Vec<String>> {
|
|
240
|
+
let key = (from_file.rel_path.clone(), target.to_string());
|
|
241
|
+
if !seen.insert(key) {
|
|
242
|
+
return None;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let (file, rest) = ctx.module_map.resolve_use(target, base_module)?;
|
|
246
|
+
let pf = ctx.by_rel.get(&file)?;
|
|
247
|
+
|
|
248
|
+
if rest.is_empty() {
|
|
249
|
+
let mut ids = vec![format!("file::{}", pf.rel_path)];
|
|
250
|
+
if pf.rel_path != from_file.rel_path {
|
|
251
|
+
collect_surface_into(ctx, pf, visited, &mut ids);
|
|
252
|
+
}
|
|
253
|
+
return Some(ids);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let segs: Vec<&str> = rest.split("::").collect();
|
|
257
|
+
let name = segs[segs.len() - 1];
|
|
258
|
+
|
|
259
|
+
if let Some(ids) = ctx.index.get(&pf.rel_path, name) {
|
|
260
|
+
return Some(ids);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let module_of_file = crate::walker::module_path_for_file(&pf.rel_path);
|
|
264
|
+
|
|
265
|
+
// re-export chains: the target file may bind the name via its own `use`
|
|
266
|
+
// (incl. `pub use` re-exports) — follow the binding, never string-match.
|
|
267
|
+
if let Some(first) = segs.first() {
|
|
268
|
+
let rest_after: String = segs[1..].join("::");
|
|
269
|
+
for u in &pf.uses {
|
|
270
|
+
if u.glob || u.alias != *first {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if rest_after.is_empty() {
|
|
274
|
+
return resolve_use_target(ctx, pf, &module_of_file, &u.target, seen, visited);
|
|
275
|
+
}
|
|
276
|
+
if let Some((bf, brest)) = ctx.module_map.resolve_use(&u.target, &module_of_file) {
|
|
277
|
+
let bpf = ctx.by_rel.get(&bf)?;
|
|
278
|
+
let combined = if brest.is_empty() {
|
|
279
|
+
rest_after.clone()
|
|
280
|
+
} else {
|
|
281
|
+
format!("{}::{}", brest, rest_after)
|
|
282
|
+
};
|
|
283
|
+
return resolve_use_target(
|
|
284
|
+
ctx,
|
|
285
|
+
bpf,
|
|
286
|
+
&crate::walker::module_path_for_file(&bf),
|
|
287
|
+
&combined,
|
|
288
|
+
seen,
|
|
289
|
+
visited,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
let mut module = module_of_file.clone();
|
|
295
|
+
for seg in &segs {
|
|
296
|
+
module = if module.is_empty() {
|
|
297
|
+
seg.to_string()
|
|
298
|
+
} else {
|
|
299
|
+
format!("{}::{}", module, seg)
|
|
300
|
+
};
|
|
301
|
+
if let Some((sub_file, sub_rest)) = ctx.module_map.resolve_use(&module, "") {
|
|
302
|
+
if sub_rest.is_empty() {
|
|
303
|
+
let sub_pf = ctx.by_rel.get(&sub_file)?;
|
|
304
|
+
if segs.len() == 1 || *seg == name {
|
|
305
|
+
return Some(vec![format!("file::{}", sub_pf.rel_path)]);
|
|
306
|
+
}
|
|
307
|
+
let remaining = segs[segs.iter().position(|s| *s == *seg).unwrap() + 1..].join("::");
|
|
308
|
+
if remaining.is_empty() {
|
|
309
|
+
return Some(vec![format!("file::{}", sub_pf.rel_path)]);
|
|
310
|
+
}
|
|
311
|
+
return resolve_use_target(ctx, sub_pf, &module_of_file, &remaining, seen, visited);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
None
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fn collect_surface_into(ctx: &Ctx, pf: &ParsedFile, visited: &mut BTreeSet<String>, ids: &mut Vec<String>) {
|
|
320
|
+
let base_module = crate::walker::module_path_for_file(&pf.rel_path);
|
|
321
|
+
for item in &pf.items {
|
|
322
|
+
if item.is_pub && !item.is_test {
|
|
323
|
+
if let Some(id) = surface_node_id(pf, item) {
|
|
324
|
+
ids.push(id);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
for u in &pf.uses {
|
|
329
|
+
if u.glob {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if let Some(resolved) = resolve_use_target(ctx, pf, &base_module, &u.target, &mut BTreeSet::new(), visited) {
|
|
333
|
+
ids.extend(resolved);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fn top_level_item_keys(file: &ParsedFile) -> std::collections::HashSet<(String, String)> {
|
|
339
|
+
let mut out = std::collections::HashSet::new();
|
|
340
|
+
for item in &file.ast.items {
|
|
341
|
+
let key = match item {
|
|
342
|
+
syn::Item::Fn(f) => ("FUNCTION".to_string(), f.sig.ident.to_string()),
|
|
343
|
+
syn::Item::Struct(s) => ("STRUCT".to_string(), s.ident.to_string()),
|
|
344
|
+
syn::Item::Enum(e) => ("ENUM".to_string(), e.ident.to_string()),
|
|
345
|
+
syn::Item::Trait(t) => ("TRAIT".to_string(), t.ident.to_string()),
|
|
346
|
+
_ => continue,
|
|
347
|
+
};
|
|
348
|
+
out.insert(key);
|
|
349
|
+
}
|
|
350
|
+
out
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
fn pub_mods(file: &ParsedFile) -> Vec<String> {
|
|
354
|
+
let mut out = Vec::new();
|
|
355
|
+
for item in &file.ast.items {
|
|
356
|
+
if let syn::Item::Mod(m) = item {
|
|
357
|
+
if matches!(m.vis, syn::Visibility::Public(_)) && m.content.is_none() {
|
|
358
|
+
out.push(m.ident.to_string());
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
out
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
fn record(
|
|
366
|
+
exports: &mut BTreeMap<String, Vec<String>>,
|
|
367
|
+
ambiguous: &mut Vec<String>,
|
|
368
|
+
key: &str,
|
|
369
|
+
ids: Vec<String>,
|
|
370
|
+
) {
|
|
371
|
+
let unique: Vec<String> = {
|
|
372
|
+
let mut s: Vec<String> = ids;
|
|
373
|
+
s.sort();
|
|
374
|
+
s.dedup();
|
|
375
|
+
s
|
|
376
|
+
};
|
|
377
|
+
if unique.is_empty() {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if let Some(existing) = exports.get(key) {
|
|
381
|
+
if existing != &unique {
|
|
382
|
+
ambiguous.push(key.to_string());
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
exports.insert(key.to_string(), unique);
|
|
387
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
use crate::contract::CodeNode;
|
|
2
|
+
use crate::exports::build_exports_map;
|
|
3
|
+
use crate::module_map::ModuleMap;
|
|
4
|
+
use crate::parser;
|
|
5
|
+
|
|
6
|
+
fn parse_fixture() -> (Vec<parser::ParsedFile>, ModuleMap) {
|
|
7
|
+
let repo = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/exportsfix");
|
|
8
|
+
let mut files = vec![];
|
|
9
|
+
for rel in crate::walker::collect_rs_files(repo) {
|
|
10
|
+
let abs = format!("{}/{}", repo, rel);
|
|
11
|
+
if let Ok(source) = std::fs::read_to_string(&abs) {
|
|
12
|
+
if let Ok(pf) = parser::parse_file(&rel, &source) {
|
|
13
|
+
files.push(pf);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
let map = ModuleMap::build(&files);
|
|
18
|
+
(files, map)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[test]
|
|
22
|
+
fn root_surface_maps_pub_items_to_proper_node_ids() {
|
|
23
|
+
let (files, mm) = parse_fixture();
|
|
24
|
+
let m = build_exports_map(&files, &mm).expect("map expected");
|
|
25
|
+
let root = m.exports.get(".").expect("root subpath");
|
|
26
|
+
|
|
27
|
+
assert_eq!(root.get("run"), Some(&vec!["src/lib.rs::run".to_string()]));
|
|
28
|
+
assert_eq!(root.get("Engine"), Some(&vec!["src/lib.rs::Engine".to_string()]));
|
|
29
|
+
assert_eq!(root.get("Mode"), Some(&vec!["src/lib.rs::Mode".to_string()]));
|
|
30
|
+
assert_eq!(root.get("Storage"), Some(&vec!["src/lib.rs::Storage".to_string()]));
|
|
31
|
+
assert_eq!(root.get("Engine.start"), Some(&vec!["src/lib.rs::Engine.start".to_string()]));
|
|
32
|
+
assert_eq!(root.get("private_helper"), None);
|
|
33
|
+
assert_eq!(root.get("Hidden"), None);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[test]
|
|
37
|
+
fn pub_mod_opens_a_prefixed_surface() {
|
|
38
|
+
let (files, mm) = parse_fixture();
|
|
39
|
+
let m = build_exports_map(&files, &mm).expect("map expected");
|
|
40
|
+
let root = m.exports.get(".").expect("root subpath");
|
|
41
|
+
|
|
42
|
+
assert_eq!(root.get("models"), Some(&vec!["file::src/models/mod.rs".to_string()]));
|
|
43
|
+
assert_eq!(root.get("models::user"), Some(&vec!["file::src/models/user.rs".to_string()]));
|
|
44
|
+
assert_eq!(root.get("models::user::PublicUser"), Some(&vec!["src/models/user.rs::PublicUser".to_string()]));
|
|
45
|
+
assert_eq!(root.get("models::user::Role"), Some(&vec!["src/models/user.rs::Role".to_string()]));
|
|
46
|
+
assert_eq!(root.get("models::user::PublicUser.new"), Some(&vec!["src/models/user.rs::PublicUser.new".to_string()]));
|
|
47
|
+
assert_eq!(root.get("models::user::PrivateThing"), None);
|
|
48
|
+
assert_eq!(root.get("models::internal::InternalDetail"), None);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn pub_use_aliases_resolve_through_bindings_name_swap_safe() {
|
|
53
|
+
let (files, mm) = parse_fixture();
|
|
54
|
+
let m = build_exports_map(&files, &mm).expect("map expected");
|
|
55
|
+
let root = m.exports.get(".").expect("root subpath");
|
|
56
|
+
|
|
57
|
+
assert_eq!(root.get("VisibleUser"), Some(&vec!["src/models/user.rs::PublicUser".to_string()]));
|
|
58
|
+
// lib.rs re-exports aliasing's names SWAPPED: lib's SwappedA = aliasing's
|
|
59
|
+
// SwappedB = Role; lib's SwappedB = aliasing's SwappedA = PublicUser.
|
|
60
|
+
assert_eq!(root.get("SwappedA"), Some(&vec!["src/models/user.rs::Role".to_string()]));
|
|
61
|
+
assert_eq!(root.get("SwappedB"), Some(&vec!["src/models/user.rs::PublicUser".to_string()]));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[test]
|
|
65
|
+
fn glob_reexport_brings_target_pub_items_to_same_level() {
|
|
66
|
+
let (files, mm) = parse_fixture();
|
|
67
|
+
let m = build_exports_map(&files, &mm).expect("map expected");
|
|
68
|
+
let root = m.exports.get(".").expect("root subpath");
|
|
69
|
+
|
|
70
|
+
assert_eq!(root.get("aliasing::Extra"), Some(&vec!["src/aliasing/extra.rs::Extra".to_string()]));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[test]
|
|
74
|
+
fn ambiguous_names_stay_empty_and_shape_is_uniform() {
|
|
75
|
+
let (files, mm) = parse_fixture();
|
|
76
|
+
let m = build_exports_map(&files, &mm).expect("map expected");
|
|
77
|
+
assert!(m.ambiguous_names.is_empty());
|
|
78
|
+
for (subpath, names) in &m.exports {
|
|
79
|
+
assert_eq!(subpath, ".");
|
|
80
|
+
for (name, ids) in names {
|
|
81
|
+
assert!(!ids.is_empty(), "{} has empty ids", name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn map_values_all_exist_in_nodes_array() {
|
|
88
|
+
let (files, mm) = parse_fixture();
|
|
89
|
+
let m = build_exports_map(&files, &mm).expect("map expected");
|
|
90
|
+
let mut node_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
91
|
+
let nodes: Vec<CodeNode> = crate::nodes::collect_code_nodes(&files)
|
|
92
|
+
.into_iter()
|
|
93
|
+
.chain(crate::nodes::collect_file_nodes(&files))
|
|
94
|
+
.collect();
|
|
95
|
+
for n in &nodes {
|
|
96
|
+
node_ids.insert(n.id.clone());
|
|
97
|
+
}
|
|
98
|
+
for names in m.exports.values() {
|
|
99
|
+
for ids in names.values() {
|
|
100
|
+
for id in ids {
|
|
101
|
+
assert!(node_ids.contains(id), "dangling map value {}", id);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -167,11 +167,13 @@ pub fn run(repo_path: &str, opts: &Options) -> ExtractorResult {
|
|
|
167
167
|
all_nodes.sort_by(|a, b| a.id.cmp(&b.id));
|
|
168
168
|
|
|
169
169
|
let total_nodes = all_nodes.len() as i64;
|
|
170
|
+
let exports = crate::exports::build_exports_map(&parsed.files, &module_map);
|
|
170
171
|
ExtractorResult {
|
|
171
172
|
fingerprint,
|
|
172
173
|
nodes: all_nodes,
|
|
173
174
|
edges,
|
|
174
175
|
routes,
|
|
176
|
+
exports,
|
|
175
177
|
stats: Stats {
|
|
176
178
|
total_files: parsed.files.len() as i64,
|
|
177
179
|
total_nodes,
|
|
@@ -69,6 +69,66 @@ impl LookupMaps {
|
|
|
69
69
|
.cloned()
|
|
70
70
|
.or_else(|| ids.first().cloned())
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
/// Arity-aware plain-name resolution: when several same-name candidates
|
|
74
|
+
/// exist, prefer those whose declared params accept `arg_count` before
|
|
75
|
+
/// falling back to path proximity. Returns (node_id, matched_by).
|
|
76
|
+
/// Rust has no overloading, so this only disambiguates cross-file
|
|
77
|
+
/// same-name functions; arg_count == 0 skips the filter entirely.
|
|
78
|
+
pub fn closest_by_path_arity(
|
|
79
|
+
&self,
|
|
80
|
+
name: &str,
|
|
81
|
+
rel_path: &str,
|
|
82
|
+
arg_count: usize,
|
|
83
|
+
) -> Option<(String, &'static str)> {
|
|
84
|
+
let ids = self.nodes_by_name.get(name)?;
|
|
85
|
+
if ids.is_empty() {
|
|
86
|
+
return None;
|
|
87
|
+
}
|
|
88
|
+
if ids.len() == 1 {
|
|
89
|
+
return Some((ids[0].clone(), "name"));
|
|
90
|
+
}
|
|
91
|
+
if arg_count > 0 {
|
|
92
|
+
let compatible: Vec<&String> = ids
|
|
93
|
+
.iter()
|
|
94
|
+
.filter(|id| {
|
|
95
|
+
self.node_by_id
|
|
96
|
+
.get(*id)
|
|
97
|
+
.map(|n| params_accept(&n.metadata, arg_count))
|
|
98
|
+
.unwrap_or(true)
|
|
99
|
+
})
|
|
100
|
+
.collect();
|
|
101
|
+
if !compatible.is_empty() && compatible.len() < ids.len() {
|
|
102
|
+
let picked = compatible
|
|
103
|
+
.iter()
|
|
104
|
+
.find(|id| id.starts_with(&format!("{}::", rel_path)))
|
|
105
|
+
.map(|s| (*s).clone())
|
|
106
|
+
.or_else(|| compatible.first().map(|s| (*s).clone()));
|
|
107
|
+
if let Some(id) = picked {
|
|
108
|
+
return Some((id, "arity"));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
let id = ids
|
|
113
|
+
.iter()
|
|
114
|
+
.find(|id| id.starts_with(&format!("{}::", rel_path)))
|
|
115
|
+
.cloned()
|
|
116
|
+
.or_else(|| ids.first().cloned())?;
|
|
117
|
+
Some((id, "name"))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// True when a node's declared params (metadata.params type strings) accept a
|
|
122
|
+
/// call with `arg_count` arguments. Nodes without params metadata are
|
|
123
|
+
/// permissive.
|
|
124
|
+
fn params_accept(metadata: &serde_json::Map<String, serde_json::Value>, arg_count: usize) -> bool {
|
|
125
|
+
let Some(serde_json::Value::Array(params)) = metadata.get("params") else {
|
|
126
|
+
return true;
|
|
127
|
+
};
|
|
128
|
+
if params.is_empty() {
|
|
129
|
+
return arg_count == 0;
|
|
130
|
+
}
|
|
131
|
+
params.len() == arg_count
|
|
72
132
|
}
|
|
73
133
|
|
|
74
134
|
// ── node id schemes (deterministic, file-scoped — mirrors go) ──
|
|
@@ -190,5 +190,24 @@ fn item_node(pf: &ParsedFile, item: &ParsedItem) -> CodeNode {
|
|
|
190
190
|
let calls: Vec<Value> = item.calls.iter().map(|c| Value::from(c.clone())).collect();
|
|
191
191
|
n.metadata.insert("calls".to_string(), Value::Array(calls));
|
|
192
192
|
}
|
|
193
|
+
if !item.params.is_empty() {
|
|
194
|
+
let params: Vec<Value> = item.params.iter().map(|p| Value::from(p.clone())).collect();
|
|
195
|
+
n.metadata.insert("params".to_string(), Value::Array(params));
|
|
196
|
+
}
|
|
197
|
+
if !item.call_sites.is_empty() {
|
|
198
|
+
let sites: Vec<Value> = item
|
|
199
|
+
.call_sites
|
|
200
|
+
.iter()
|
|
201
|
+
.map(|cs| {
|
|
202
|
+
let mut m = serde_json::Map::new();
|
|
203
|
+
m.insert("name".to_string(), Value::from(cs.name.clone()));
|
|
204
|
+
m.insert("argCount".to_string(), Value::from(cs.arg_count as u64));
|
|
205
|
+
let types: Vec<Value> = cs.arg_types.iter().map(|t| Value::from(t.clone())).collect();
|
|
206
|
+
m.insert("argTypes".to_string(), Value::Array(types));
|
|
207
|
+
Value::Object(m)
|
|
208
|
+
})
|
|
209
|
+
.collect();
|
|
210
|
+
n.metadata.insert("callSites".to_string(), Value::Array(sites));
|
|
211
|
+
}
|
|
193
212
|
n
|
|
194
213
|
}
|