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.
- 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 +30 -42
- package/dist/scoring/index.js +8 -0
- package/dist/scoring/index.test.js +43 -0
- package/dist/scoring/nodeScorer.js +2 -0
- package/dist/scoring/pruneDisconnected.d.ts +8 -0
- package/dist/scoring/pruneDisconnected.js +66 -0
- package/dist/scoring/pruneDisconnected.test.d.ts +1 -0
- package/dist/scoring/pruneDisconnected.test.js +123 -0
- package/dist/summarizer/prompts.d.ts +1 -1
- package/dist/types.d.ts +5 -5
- package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
- package/extractors/go/build.mjs +43 -0
- package/extractors/go/calls.go +289 -0
- package/extractors/go/contract.go +140 -0
- package/extractors/go/extractor.go +161 -0
- package/extractors/go/fingerprint.go +167 -0
- package/extractors/go/go.mod +3 -0
- package/extractors/go/imports.go +90 -0
- package/extractors/go/inheritance.go +138 -0
- package/extractors/go/lookup.go +106 -0
- package/extractors/go/main.go +57 -0
- package/extractors/go/nodes.go +177 -0
- package/extractors/go/orm_edges.go +277 -0
- package/extractors/go/parser.go +469 -0
- package/extractors/go/routes.go +521 -0
- package/extractors/go/tests.go +45 -0
- package/extractors/go/thirdparty.go +138 -0
- package/extractors/go/typeload.go +178 -0
- package/extractors/go/walker.go +67 -0
- package/extractors/java/build.mjs +90 -0
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/extractors/java/src/devlens/extractor/Contract.java +171 -0
- package/extractors/java/src/devlens/extractor/Extractor.java +262 -0
- package/extractors/java/src/devlens/extractor/ExtractorResult.java +12 -0
- package/extractors/java/src/devlens/extractor/Fingerprint.java +239 -0
- package/extractors/java/src/devlens/extractor/LookupMaps.java +123 -0
- package/extractors/java/src/devlens/extractor/Main.java +66 -0
- package/extractors/java/src/devlens/extractor/Parser.java +522 -0
- package/extractors/java/src/devlens/extractor/SourceWalker.java +82 -0
- package/extractors/java/src/devlens/extractor/ThirdParty.java +141 -0
- package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +43 -0
- package/extractors/java/src/devlens/extractor/edges/Calls.java +266 -0
- package/extractors/java/src/devlens/extractor/edges/Enrich.java +54 -0
- package/extractors/java/src/devlens/extractor/edges/Imports.java +154 -0
- package/extractors/java/src/devlens/extractor/edges/Inheritance.java +79 -0
- package/extractors/java/src/devlens/extractor/edges/OrmEdges.java +209 -0
- package/extractors/java/src/devlens/extractor/edges/Routes.java +143 -0
- package/extractors/java/src/devlens/extractor/edges/Tests.java +53 -0
- package/extractors/python/devlens_extractors_python/__init__.py +3 -0
- package/extractors/python/devlens_extractors_python/__main__.py +33 -0
- package/extractors/python/devlens_extractors_python/contract.py +94 -0
- package/extractors/python/devlens_extractors_python/edges/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/calls.py +112 -0
- package/extractors/python/devlens_extractors_python/edges/enrich.py +44 -0
- package/extractors/python/devlens_extractors_python/edges/imports.py +155 -0
- package/extractors/python/devlens_extractors_python/edges/inheritance.py +97 -0
- package/extractors/python/devlens_extractors_python/edges/orm_edges.py +225 -0
- package/extractors/python/devlens_extractors_python/edges/routes/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/routes/common.py +79 -0
- package/extractors/python/devlens_extractors_python/edges/routes/decorators.py +212 -0
- package/extractors/python/devlens_extractors_python/edges/routes/django_urls.py +213 -0
- package/extractors/python/devlens_extractors_python/edges/routes/drf.py +128 -0
- package/extractors/python/devlens_extractors_python/edges/tests.py +53 -0
- package/extractors/python/devlens_extractors_python/extractor.py +107 -0
- package/extractors/python/devlens_extractors_python/fingerprint.py +201 -0
- package/extractors/python/devlens_extractors_python/lookup.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/__init__.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/classes.py +109 -0
- package/extractors/python/devlens_extractors_python/parser/functions.py +163 -0
- package/extractors/python/devlens_extractors_python/parser/walker.py +30 -0
- package/extractors/python/devlens_extractors_python/third_party.py +103 -0
- package/extractors/python/pyproject.toml +16 -0
- package/extractors/python/setup.mjs +54 -0
- package/extractors/rust/Cargo.toml +29 -0
- package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
- package/extractors/rust/build.mjs +85 -0
- package/extractors/rust/src/calls.rs +295 -0
- package/extractors/rust/src/contract.rs +236 -0
- package/extractors/rust/src/enrich.rs +107 -0
- package/extractors/rust/src/extractor.rs +199 -0
- package/extractors/rust/src/fingerprint.rs +238 -0
- package/extractors/rust/src/imports.rs +101 -0
- package/extractors/rust/src/inheritance.rs +233 -0
- package/extractors/rust/src/lookup.rs +226 -0
- package/extractors/rust/src/main.rs +53 -0
- package/extractors/rust/src/module_map.rs +131 -0
- package/extractors/rust/src/nodes.rs +174 -0
- package/extractors/rust/src/orm_edges.rs +125 -0
- package/extractors/rust/src/parser.rs +859 -0
- package/extractors/rust/src/routes.rs +1265 -0
- package/extractors/rust/src/tests.rs +109 -0
- package/extractors/rust/src/thirdparty.rs +114 -0
- package/extractors/rust/src/walker.rs +82 -0
- package/package.json +22 -4
|
@@ -0,0 +1,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 ¤t_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
|
+
}
|