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.
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 +36 -48
  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,199 @@
1
+ // extractor.rs — pipeline orchestration (skeleton; modules wired in as they
2
+ // land — order mirrors go/extractor.go):
3
+ //
4
+ // walk → parse (facts) → module_map → lookup → imports (+[crate] nodes) →
5
+ // calls → routes → orm → inheritance → tests → enrich →
6
+ // dedupe → deterministic sort → result.
7
+ //
8
+ // Edge detectors resolve from facts via lookup maps — never re-walk ASTs
9
+ // (playbook rule).
10
+
11
+ use crate::contract::{ExtractorError, ExtractorResult, Stats};
12
+ use crate::fingerprint;
13
+ use crate::parser::ParsedFile;
14
+ use serde_json::Value;
15
+
16
+ /// All parsed facts for the repo — the single source consumed by every edge
17
+ /// detector (mirrors go's parsedRepo).
18
+ pub struct ParsedRepo {
19
+ pub files: Vec<ParsedFile>,
20
+ pub skipped: i64,
21
+ }
22
+
23
+ /// Options — the engine forwards includeThirdPartyLibs (both spellings).
24
+ #[derive(Default, Clone)]
25
+ pub struct Options {
26
+ /// Third-party crates the user selected in the frontend. Absent/empty →
27
+ /// ZERO third-party nodes (gate, not default-on).
28
+ pub include_third_party_libs: Vec<String>,
29
+ }
30
+
31
+ impl Options {
32
+ pub fn from_json(options: &Value) -> Self {
33
+ let mut o = Options::default();
34
+ for key in ["includeThirdPartyLibs", "includedThirdPartyLibs"] {
35
+ if let Some(v) = options.get(key).and_then(|v| v.as_array()) {
36
+ for item in v {
37
+ if let Some(s) = item.as_str() {
38
+ o.include_third_party_libs.push(s.to_string());
39
+ }
40
+ }
41
+ }
42
+ if !o.include_third_party_libs.is_empty() {
43
+ break;
44
+ }
45
+ }
46
+ o
47
+ }
48
+ }
49
+
50
+ pub fn run(repo_path: &str, opts: &Options) -> ExtractorResult {
51
+ let _ = opts;
52
+
53
+ let mut errors: Vec<ExtractorError> = vec![];
54
+
55
+ // ── fingerprint (Cargo.toml only; no stdlib web framework in Rust, so no
56
+ // post-walk upgrades like Go's net-http) ──
57
+ let mf = fingerprint::parse_cargo_toml(repo_path);
58
+ let fingerprint = fingerprint::fingerprint_from_manifest(&mf, repo_path);
59
+
60
+ // ── walk + parse (facts; non-fatal per-file errors) ──
61
+ let mut parsed = ParsedRepo {
62
+ files: vec![],
63
+ skipped: 0,
64
+ };
65
+ for rel in crate::walker::collect_rs_files(repo_path) {
66
+ let abs = format!("{}/{}", repo_path, rel);
67
+ match std::fs::read_to_string(&abs) {
68
+ Ok(source) => match crate::parser::parse_file(&rel, &source) {
69
+ Ok(pf) => parsed.files.push(pf),
70
+ Err(e) => {
71
+ parsed.skipped += 1;
72
+ errors.push(ExtractorError {
73
+ file: rel.clone(),
74
+ error: format!("parse error: {}", e),
75
+ });
76
+ }
77
+ },
78
+ Err(e) => {
79
+ parsed.skipped += 1;
80
+ errors.push(ExtractorError {
81
+ file: rel.clone(),
82
+ error: format!("read error: {}", e),
83
+ });
84
+ }
85
+ }
86
+ }
87
+
88
+ // ── module map + lookup (ONE shared index) ──
89
+ let module_map = crate::module_map::ModuleMap::build(&parsed.files);
90
+
91
+ // file + code nodes (test files = leaf: testCases only)
92
+ let file_nodes = crate::nodes::collect_file_nodes(&parsed.files);
93
+ let mut all_nodes: Vec<crate::contract::CodeNode> = file_nodes;
94
+ all_nodes.extend(crate::nodes::collect_code_nodes(&parsed.files));
95
+
96
+ let mut lookup = crate::lookup::build_lookup_maps(&parsed.files, &all_nodes);
97
+
98
+ // third-party registry — gate from options (absent/empty → ZERO [crate])
99
+ let mut tp = crate::thirdparty::ThirdPartyRegistry::new(&opts.include_third_party_libs);
100
+
101
+ // ── edges, in dependency order ──
102
+ let mut all_edges: Vec<crate::contract::CodeEdge> = vec![];
103
+
104
+ // IMPORTS + [crate] nodes
105
+ let imports = crate::imports::detect_imports(&parsed, &mf, &module_map, &mut tp, opts);
106
+ all_edges.extend(imports.edges);
107
+
108
+ // CALLS
109
+ // detect_calls writes metadata.resolvedCalls back into lookup.node_by_id
110
+ // (it clones→mutates→reinserts). all_nodes was built BEFORE that, so we
111
+ // must sync the resolvedCalls forward — else the output nodes carry calls
112
+ // (parser-time) but never resolvedCalls, and the CALLS parity census reads 0.
113
+ let calls = crate::calls::detect_calls(&parsed, &module_map, &mut lookup, &mf, &mut tp, opts);
114
+ all_edges.extend(calls.edges);
115
+ for n in &mut all_nodes {
116
+ if let Some(reg) = lookup.node_by_id.get(&n.id) {
117
+ if let Some(rc) = reg.metadata.get("resolvedCalls") {
118
+ n.metadata.insert("resolvedCalls".to_string(), rc.clone());
119
+ }
120
+ }
121
+ }
122
+
123
+ // ROUTE nodes + HANDLES edges + BackendRouteNodes
124
+ let routes_out = crate::routes::detect_routes(
125
+ &parsed,
126
+ &module_map,
127
+ &lookup,
128
+ &fingerprint.framework,
129
+ opts,
130
+ &mf,
131
+ &mut tp,
132
+ );
133
+ all_nodes.extend(routes_out.route_nodes);
134
+ all_edges.extend(routes_out.handles_edges);
135
+
136
+ // EXTENDS / IMPLEMENTS
137
+ let inheritance =
138
+ crate::inheritance::detect_inheritance(&parsed, &mf, &module_map, &lookup, &mut tp, opts);
139
+ all_edges.extend(inheritance.edges);
140
+ all_nodes.extend(inheritance.third_party_nodes);
141
+
142
+ // READS_FROM / WRITES_TO (Diesel)
143
+ all_edges.extend(crate::orm_edges::detect_orm_edges(&parsed, &lookup, opts));
144
+
145
+ // TESTS edges
146
+ all_edges.extend(crate::tests::detect_tests(
147
+ &parsed,
148
+ &module_map,
149
+ &lookup,
150
+ opts,
151
+ ));
152
+
153
+ // third-party nodes — the registry is the SINGLE source (imports/calls
154
+ // registered into it; collected last, go parity)
155
+ all_nodes.extend(tp.all_nodes());
156
+
157
+ // ── dedupe + deterministic sort ──
158
+ let edges = dedupe_and_sort_edges(all_edges);
159
+ all_nodes.sort_by(|a, b| a.id.cmp(&b.id));
160
+ let mut routes = routes_out.backend_routes;
161
+ routes.sort_by(|a, b| {
162
+ (&a.http_method, &a.url_path, &a.node_id).cmp(&(&b.http_method, &b.url_path, &b.node_id))
163
+ });
164
+
165
+ // semantic enrichment (isModel/isHandler) — needs the deduped edges
166
+ crate::enrich::enrich_nodes(&parsed, &mut all_nodes, &edges);
167
+ all_nodes.sort_by(|a, b| a.id.cmp(&b.id));
168
+
169
+ let total_nodes = all_nodes.len() as i64;
170
+ ExtractorResult {
171
+ fingerprint,
172
+ nodes: all_nodes,
173
+ edges,
174
+ routes,
175
+ stats: Stats {
176
+ total_files: parsed.files.len() as i64,
177
+ total_nodes,
178
+ skipped_files: parsed.skipped,
179
+ },
180
+ errors,
181
+ }
182
+ }
183
+
184
+ /// Dedupe (from, type, to) then sort by (from, type, to) — deterministic.
185
+ fn dedupe_and_sort_edges(edges: Vec<crate::contract::CodeEdge>) -> Vec<crate::contract::CodeEdge> {
186
+ let mut seen: std::collections::HashSet<(String, String, String)> =
187
+ std::collections::HashSet::new();
188
+ let mut out = vec![];
189
+ for e in edges {
190
+ let key = (e.from.clone(), e.edge_type.clone(), e.to.clone());
191
+ if seen.contains(&key) {
192
+ continue;
193
+ }
194
+ seen.insert(key);
195
+ out.push(e);
196
+ }
197
+ out.sort_by(|a, b| (&a.from, &a.edge_type, &a.to).cmp(&(&b.from, &b.edge_type, &b.to)));
198
+ out
199
+ }
@@ -0,0 +1,238 @@
1
+ // fingerprint.rs — Cargo.toml parsing → framework / projectType / databases /
2
+ // rawDependencies. Manifest is PARSED, never executed (security rule, same as
3
+ // setup.py/pom.xml/go.mod handling in the other phases).
4
+ //
5
+ // Workspace-aware: [workspace] members' Cargo.tomls and the root
6
+ // [workspace.dependencies] are unioned into one dependency view.
7
+
8
+ use crate::contract::Fingerprint;
9
+ use std::collections::BTreeMap;
10
+ use std::path::Path;
11
+
12
+ /// Parsed manifest facts — consumed by fingerprint AND import resolution
13
+ /// (dependency names = the external-crate set for [crate]/ gating).
14
+ pub struct ParsedManifest {
15
+ /// union of [dependencies] + [dev-dependencies] + [workspace.dependencies]
16
+ /// (name → version; version may be empty for path/git deps)
17
+ pub dependencies: BTreeMap<String, String>,
18
+ /// member crate directories (relative, from [workspace].members globs)
19
+ pub workspace_members: Vec<String>,
20
+ }
21
+
22
+ impl ParsedManifest {
23
+ pub fn is_external_crate(&self, name: &str) -> bool {
24
+ // strip a leading crate-name prefix (e.g. `axum::extract` → `axum`)
25
+ let root = name.split("::").next().unwrap_or(name);
26
+ if self.dependencies.contains_key(root) {
27
+ return true;
28
+ }
29
+ // Cargo normalization: the `actix-web` dependency provides the
30
+ // `actix_web` crate (hyphens → underscores in source)
31
+ let underscored = root.replace('-', "_");
32
+ if underscored != root && self.dependencies.contains_key(&underscored) {
33
+ return true;
34
+ }
35
+ self.dependencies
36
+ .keys()
37
+ .any(|k| k.replace('-', "_") == underscored)
38
+ }
39
+ }
40
+
41
+ /// Parse Cargo.toml at repoPath (if present). Never fails hard — a missing or
42
+ /// malformed manifest yields an empty ParsedManifest (fingerprint degrades to
43
+ /// unknown, documented non-fatal).
44
+ pub fn parse_cargo_toml(repo_path: &str) -> ParsedManifest {
45
+ let mut mf = ParsedManifest {
46
+ dependencies: BTreeMap::new(),
47
+ workspace_members: vec![],
48
+ };
49
+ let root_toml = Path::new(repo_path).join("Cargo.toml");
50
+ if !root_toml.exists() {
51
+ return mf;
52
+ }
53
+ let text = match std::fs::read_to_string(&root_toml) {
54
+ Ok(t) => t,
55
+ Err(_) => return mf,
56
+ };
57
+ let root: toml::Value = match toml::from_str(&text) {
58
+ Ok(v) => v,
59
+ Err(_) => return mf,
60
+ };
61
+
62
+ // root package deps
63
+ collect_deps(&root, &mut mf.dependencies);
64
+
65
+ // workspace: root [workspace.dependencies] + member crates
66
+ let mut member_dirs: Vec<String> = vec![];
67
+ if let Some(ws) = root.get("workspace") {
68
+ if let Some(wd) = ws.get("dependencies").and_then(|d| d.as_table()) {
69
+ for (name, spec) in wd {
70
+ insert_dep(&mut mf.dependencies, name, spec);
71
+ }
72
+ }
73
+ if let Some(members) = ws.get("members").and_then(|m| m.as_array()) {
74
+ for m in members {
75
+ if let Some(s) = m.as_str() {
76
+ expand_members(s, repo_path, &mut member_dirs);
77
+ }
78
+ }
79
+ }
80
+ }
81
+ mf.workspace_members = member_dirs;
82
+
83
+ // member crates' own [dependencies]
84
+ for dir in &mf.workspace_members {
85
+ let member_toml = Path::new(repo_path).join(dir).join("Cargo.toml");
86
+ if let Ok(t) = std::fs::read_to_string(&member_toml) {
87
+ if let Ok(v) = toml::from_str::<toml::Value>(&t) {
88
+ collect_deps(&v, &mut mf.dependencies);
89
+ }
90
+ }
91
+ }
92
+ mf
93
+ }
94
+
95
+ fn collect_deps(v: &toml::Value, out: &mut BTreeMap<String, String>) {
96
+ for section in ["dependencies", "dev-dependencies"] {
97
+ if let Some(t) = v.get(section).and_then(|d| d.as_table()) {
98
+ for (name, spec) in t {
99
+ insert_dep(out, name, spec);
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ /// dep spec forms: `foo = "1.0"` (string), `foo = { version = "1.0", ... }`
106
+ /// (inline table), `[dependencies.foo]` (table header) — all land as
107
+ /// Value::String or Value::Table in the toml crate.
108
+ fn insert_dep(out: &mut BTreeMap<String, String>, name: &str, spec: &toml::Value) {
109
+ match spec {
110
+ toml::Value::String(s) => {
111
+ out.insert(name.to_string(), s.clone());
112
+ }
113
+ toml::Value::Table(t) => {
114
+ let ver = t
115
+ .get("version")
116
+ .and_then(|v| v.as_str())
117
+ .unwrap_or("")
118
+ .to_string();
119
+ out.insert(name.to_string(), ver);
120
+ }
121
+ toml::Value::Array(a) => {
122
+ // multi-spec form: `foo = ["1.0", { ... }]` — take first string
123
+ for item in a {
124
+ if let toml::Value::String(s) = item {
125
+ out.insert(name.to_string(), s.clone());
126
+ return;
127
+ }
128
+ }
129
+ out.insert(name.to_string(), String::new());
130
+ }
131
+ _ => {
132
+ out.insert(name.to_string(), String::new());
133
+ }
134
+ }
135
+ }
136
+
137
+ /// `members = ["crates/*", "core", "libs/foo"]` — expand `*` against the
138
+ /// filesystem (a glob segment matches exactly one directory level).
139
+ fn expand_members(glob: &str, repo_path: &str, out: &mut Vec<String>) {
140
+ let mut segments: Vec<&str> = glob.split('/').collect();
141
+ let mut current_dirs: Vec<String> = vec![String::new()];
142
+ while let Some(seg) = segments.first() {
143
+ let seg = *seg;
144
+ segments.remove(0);
145
+ let mut next: Vec<String> = vec![];
146
+ for base in &current_dirs {
147
+ if seg == "*" {
148
+ let dir = Path::new(repo_path).join(base);
149
+ if let Ok(entries) = std::fs::read_dir(&dir) {
150
+ for e in entries.flatten() {
151
+ let name = e.file_name().to_string_lossy().to_string();
152
+ if name.starts_with('.') {
153
+ continue;
154
+ }
155
+ if e.file_type().map(|t| t.is_dir()).unwrap_or(false) {
156
+ next.push(if base.is_empty() {
157
+ name
158
+ } else {
159
+ format!("{}/{}", base, name)
160
+ });
161
+ }
162
+ }
163
+ }
164
+ } else {
165
+ next.push(if base.is_empty() {
166
+ seg.to_string()
167
+ } else {
168
+ format!("{}/{}", base, seg)
169
+ });
170
+ }
171
+ }
172
+ current_dirs = next;
173
+ }
174
+ for d in current_dirs {
175
+ // only actual crates (has Cargo.toml)
176
+ if Path::new(repo_path).join(&d).join("Cargo.toml").exists() {
177
+ out.push(d);
178
+ }
179
+ }
180
+ }
181
+
182
+ // ── framework / database / projectType detection ──
183
+
184
+ const FRAMEWORKS: [(&str, &str); 5] = [
185
+ ("axum", "axum"),
186
+ ("actix-web", "actix-web"),
187
+ ("rocket", "rocket"),
188
+ ("warp", "warp"),
189
+ ("poem", "poem"),
190
+ ];
191
+
192
+ const DATABASES: [(&str, &str); 8] = [
193
+ ("diesel", "diesel"),
194
+ ("sqlx", "sqlx"),
195
+ ("postgres", "postgresql"),
196
+ ("tokio-postgres", "postgresql"),
197
+ ("rusqlite", "sqlite"),
198
+ ("sqlite", "sqlite"),
199
+ ("mysql", "mysql"),
200
+ ("mongodb", "mongo"),
201
+ ];
202
+
203
+ pub fn fingerprint_from_manifest(mf: &ParsedManifest, repo_path: &str) -> Fingerprint {
204
+ let mut fp = Fingerprint::new();
205
+
206
+ for (crate_name, fw) in FRAMEWORKS {
207
+ if mf.is_external_crate(crate_name) {
208
+ fp.framework = fw.to_string();
209
+ fp.router = fw.to_string();
210
+ fp.project_type = "backend".to_string();
211
+ break;
212
+ }
213
+ }
214
+
215
+ let mut dbs: Vec<String> = vec![];
216
+ for (crate_name, db) in DATABASES {
217
+ if mf.is_external_crate(crate_name) && !dbs.contains(&db.to_string()) {
218
+ dbs.push(db.to_string());
219
+ }
220
+ }
221
+ fp.databases = dbs;
222
+
223
+ // projectType: main.rs → backend (bin crate); lib.rs only → library.
224
+ if fp.project_type == "unknown" {
225
+ let has_main = Path::new(repo_path).join("src/main.rs").exists();
226
+ let has_lib = Path::new(repo_path).join("src/lib.rs").exists();
227
+ fp.project_type = if has_main {
228
+ "backend".to_string()
229
+ } else if has_lib {
230
+ "library".to_string()
231
+ } else {
232
+ "unknown".to_string()
233
+ };
234
+ }
235
+
236
+ fp.raw_dependencies = mf.dependencies.clone();
237
+ fp
238
+ }
@@ -0,0 +1,101 @@
1
+ // imports.rs — IMPORTS edges from `use` declarations + [crate] third-party
2
+ // nodes (mirrors go/imports.go).
3
+ //
4
+ // Resolution (documented Rust semantics — the module tree, not a sys.path
5
+ // simulation):
6
+ // crate::a::b → absolute; super::/self:: → relative; bare → relative
7
+ // then crate root. Longest module prefix wins; the remainder is an item.
8
+ // Targets are FILES (Python model — no MODULE nodes in V1):
9
+ // `use crate::models::user::User;` → IMPORTS edge to src/models/user.rs
10
+ // External first segments (a Cargo.toml dep, not std, not local) →
11
+ // gated [crate]/name[::member] node + IMPORTS edge to it. Nodes are
12
+ // registered into the shared ThirdPartyRegistry (the pipeline collects
13
+ // tp.all_nodes() last — go parity); this module only emits edges.
14
+
15
+ use crate::contract::CodeEdge;
16
+ use crate::extractor::{Options, ParsedRepo};
17
+ use crate::fingerprint::ParsedManifest;
18
+ use crate::module_map::ModuleMap;
19
+ use crate::thirdparty::ThirdPartyRegistry;
20
+
21
+ pub struct ImportsOutput {
22
+ pub edges: Vec<CodeEdge>,
23
+ }
24
+
25
+ /// Emit IMPORTS edges for every use declaration.
26
+ pub fn detect_imports(
27
+ repo: &ParsedRepo,
28
+ mf: &ParsedManifest,
29
+ module_map: &ModuleMap,
30
+ tp: &mut ThirdPartyRegistry,
31
+ _opts: &Options,
32
+ ) -> ImportsOutput {
33
+ let mut edges = vec![];
34
+ let mut seen: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
35
+
36
+ for pf in &repo.files {
37
+ if pf.is_test_file {
38
+ // test files ARE leaf nodes, but they still import — emit edges
39
+ // from the TEST node (same file id — parentFile covers it)
40
+ }
41
+ let from = format!("file::{}", pf.rel_path);
42
+ let base_module = crate::walker::module_path_for_file(&pf.rel_path);
43
+ for u in &pf.uses {
44
+ // resolve the target path against the module map (use-semantics:
45
+ // crate-root absolute for bare paths — edition 2018)
46
+ if let Some((file, rest)) = module_map.resolve_use(&u.target, &base_module) {
47
+ // skip self-imports (`use super::*` inside a same-file mod,
48
+ // `use crate::foo::*` from within foo.rs) — noise edges
49
+ if file == pf.rel_path {
50
+ continue;
51
+ }
52
+ let to = format!("file::{}", file);
53
+ let key = (from.clone(), to.clone());
54
+ if !seen.contains(&key) {
55
+ seen.insert(key);
56
+ edges.push(crate::contract::edge(&from, &to, "IMPORTS"));
57
+ }
58
+ // rest (an item) is refined by calls/tests via symbol maps
59
+ let _ = rest;
60
+ continue;
61
+ }
62
+ // external crate or unresolved
63
+ let first = crate::thirdparty::first_segment(&u.target);
64
+ if crate::thirdparty::is_std(first) {
65
+ continue; // skip tier — no node, no edge
66
+ }
67
+ if !crate::thirdparty::is_external_crate(first, mf) {
68
+ continue; // unresolvable (typo / not a dep) — metadata only
69
+ }
70
+ // external: [crate]/name (+ member when the use names one) —
71
+ // registration happens inside the registry (gated)
72
+ if tp.crate_node(first).is_some() {
73
+ push_import(
74
+ &mut edges,
75
+ &mut seen,
76
+ &from,
77
+ &crate::lookup::third_party_id(first),
78
+ );
79
+ }
80
+ let rest: Vec<&str> = u.target.split("::").skip(1).collect();
81
+ if !rest.is_empty() && !u.glob {
82
+ let member = rest.join("::");
83
+ let _ = tp.member_node(first, &member);
84
+ }
85
+ }
86
+ }
87
+ ImportsOutput { edges }
88
+ }
89
+
90
+ fn push_import(
91
+ edges: &mut Vec<CodeEdge>,
92
+ seen: &mut std::collections::HashSet<(String, String)>,
93
+ from: &str,
94
+ to: &str,
95
+ ) {
96
+ let key = (from.to_string(), to.to_string());
97
+ if !seen.contains(&key) {
98
+ seen.insert(key);
99
+ edges.push(crate::contract::edge(from, to, "IMPORTS"));
100
+ }
101
+ }