devlensio 0.6.2 → 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.
Files changed (129) hide show
  1. package/README.md +89 -31
  2. package/dist/extractors/detectLanguage.d.ts +2 -0
  3. package/dist/extractors/detectLanguage.js +33 -0
  4. package/dist/extractors/index.d.ts +6 -0
  5. package/dist/extractors/index.js +116 -0
  6. package/dist/extractors/runner.d.ts +7 -0
  7. package/dist/extractors/runner.js +194 -0
  8. package/dist/extractors/types.d.ts +37 -0
  9. package/dist/extractors/types.js +8 -0
  10. package/dist/graph/buildLookup.d.ts +1 -0
  11. package/dist/graph/buildLookup.js +2 -1
  12. package/dist/graph/edges/callEdges.js +19 -5
  13. package/dist/graph/edges/callEdges.test.d.ts +1 -0
  14. package/dist/graph/edges/callEdges.test.js +200 -0
  15. package/dist/graph/edges/importEdges.js +12 -0
  16. package/dist/graph/edges/inheritanceEdges.d.ts +3 -0
  17. package/dist/graph/edges/inheritanceEdges.js +73 -0
  18. package/dist/graph/edges/inheritanceEdges.test.d.ts +1 -0
  19. package/dist/graph/edges/inheritanceEdges.test.js +140 -0
  20. package/dist/graph/index.js +4 -0
  21. package/dist/parser/classes.test.d.ts +1 -0
  22. package/dist/parser/classes.test.js +360 -0
  23. package/dist/parser/extractors/classes.d.ts +5 -0
  24. package/dist/parser/extractors/classes.js +241 -0
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +7 -1
  27. package/dist/pipeline/index.d.ts +2 -1
  28. package/dist/pipeline/index.js +30 -42
  29. package/dist/scoring/index.js +8 -0
  30. package/dist/scoring/index.test.js +43 -0
  31. package/dist/scoring/nodeScorer.js +2 -0
  32. package/dist/scoring/pruneDisconnected.d.ts +8 -0
  33. package/dist/scoring/pruneDisconnected.js +66 -0
  34. package/dist/scoring/pruneDisconnected.test.d.ts +1 -0
  35. package/dist/scoring/pruneDisconnected.test.js +123 -0
  36. package/dist/summarizer/prompts.d.ts +1 -1
  37. package/dist/types.d.ts +5 -5
  38. package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
  39. package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
  40. package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
  41. package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
  42. package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
  43. package/extractors/go/build.mjs +43 -0
  44. package/extractors/go/calls.go +289 -0
  45. package/extractors/go/contract.go +140 -0
  46. package/extractors/go/extractor.go +161 -0
  47. package/extractors/go/fingerprint.go +167 -0
  48. package/extractors/go/go.mod +3 -0
  49. package/extractors/go/imports.go +90 -0
  50. package/extractors/go/inheritance.go +138 -0
  51. package/extractors/go/lookup.go +106 -0
  52. package/extractors/go/main.go +57 -0
  53. package/extractors/go/nodes.go +177 -0
  54. package/extractors/go/orm_edges.go +277 -0
  55. package/extractors/go/parser.go +469 -0
  56. package/extractors/go/routes.go +521 -0
  57. package/extractors/go/tests.go +45 -0
  58. package/extractors/go/thirdparty.go +138 -0
  59. package/extractors/go/typeload.go +178 -0
  60. package/extractors/go/walker.go +67 -0
  61. package/extractors/java/build.mjs +90 -0
  62. package/extractors/java/devlens_java_extractor.jar +0 -0
  63. package/extractors/java/src/devlens/extractor/Contract.java +171 -0
  64. package/extractors/java/src/devlens/extractor/Extractor.java +262 -0
  65. package/extractors/java/src/devlens/extractor/ExtractorResult.java +12 -0
  66. package/extractors/java/src/devlens/extractor/Fingerprint.java +239 -0
  67. package/extractors/java/src/devlens/extractor/LookupMaps.java +123 -0
  68. package/extractors/java/src/devlens/extractor/Main.java +66 -0
  69. package/extractors/java/src/devlens/extractor/Parser.java +522 -0
  70. package/extractors/java/src/devlens/extractor/SourceWalker.java +82 -0
  71. package/extractors/java/src/devlens/extractor/ThirdParty.java +141 -0
  72. package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +43 -0
  73. package/extractors/java/src/devlens/extractor/edges/Calls.java +266 -0
  74. package/extractors/java/src/devlens/extractor/edges/Enrich.java +54 -0
  75. package/extractors/java/src/devlens/extractor/edges/Imports.java +154 -0
  76. package/extractors/java/src/devlens/extractor/edges/Inheritance.java +79 -0
  77. package/extractors/java/src/devlens/extractor/edges/OrmEdges.java +209 -0
  78. package/extractors/java/src/devlens/extractor/edges/Routes.java +143 -0
  79. package/extractors/java/src/devlens/extractor/edges/Tests.java +53 -0
  80. package/extractors/python/devlens_extractors_python/__init__.py +3 -0
  81. package/extractors/python/devlens_extractors_python/__main__.py +33 -0
  82. package/extractors/python/devlens_extractors_python/contract.py +94 -0
  83. package/extractors/python/devlens_extractors_python/edges/__init__.py +28 -0
  84. package/extractors/python/devlens_extractors_python/edges/calls.py +112 -0
  85. package/extractors/python/devlens_extractors_python/edges/enrich.py +44 -0
  86. package/extractors/python/devlens_extractors_python/edges/imports.py +155 -0
  87. package/extractors/python/devlens_extractors_python/edges/inheritance.py +97 -0
  88. package/extractors/python/devlens_extractors_python/edges/orm_edges.py +225 -0
  89. package/extractors/python/devlens_extractors_python/edges/routes/__init__.py +28 -0
  90. package/extractors/python/devlens_extractors_python/edges/routes/common.py +79 -0
  91. package/extractors/python/devlens_extractors_python/edges/routes/decorators.py +212 -0
  92. package/extractors/python/devlens_extractors_python/edges/routes/django_urls.py +213 -0
  93. package/extractors/python/devlens_extractors_python/edges/routes/drf.py +128 -0
  94. package/extractors/python/devlens_extractors_python/edges/tests.py +53 -0
  95. package/extractors/python/devlens_extractors_python/extractor.py +107 -0
  96. package/extractors/python/devlens_extractors_python/fingerprint.py +201 -0
  97. package/extractors/python/devlens_extractors_python/lookup.py +72 -0
  98. package/extractors/python/devlens_extractors_python/parser/__init__.py +72 -0
  99. package/extractors/python/devlens_extractors_python/parser/classes.py +109 -0
  100. package/extractors/python/devlens_extractors_python/parser/functions.py +163 -0
  101. package/extractors/python/devlens_extractors_python/parser/walker.py +30 -0
  102. package/extractors/python/devlens_extractors_python/third_party.py +103 -0
  103. package/extractors/python/pyproject.toml +16 -0
  104. package/extractors/python/setup.mjs +54 -0
  105. package/extractors/rust/Cargo.toml +29 -0
  106. package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
  107. package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
  108. package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
  109. package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
  110. package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
  111. package/extractors/rust/build.mjs +85 -0
  112. package/extractors/rust/src/calls.rs +295 -0
  113. package/extractors/rust/src/contract.rs +236 -0
  114. package/extractors/rust/src/enrich.rs +107 -0
  115. package/extractors/rust/src/extractor.rs +199 -0
  116. package/extractors/rust/src/fingerprint.rs +238 -0
  117. package/extractors/rust/src/imports.rs +101 -0
  118. package/extractors/rust/src/inheritance.rs +233 -0
  119. package/extractors/rust/src/lookup.rs +226 -0
  120. package/extractors/rust/src/main.rs +53 -0
  121. package/extractors/rust/src/module_map.rs +131 -0
  122. package/extractors/rust/src/nodes.rs +174 -0
  123. package/extractors/rust/src/orm_edges.rs +125 -0
  124. package/extractors/rust/src/parser.rs +859 -0
  125. package/extractors/rust/src/routes.rs +1265 -0
  126. package/extractors/rust/src/tests.rs +109 -0
  127. package/extractors/rust/src/thirdparty.rs +114 -0
  128. package/extractors/rust/src/walker.rs +82 -0
  129. 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
+ }