devlensio 1.0.0 → 1.0.2
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/extractors/index.js +78 -39
- 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/java/devlens_java_extractor.jar +0 -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 +1 -1
- package/extractors/rust/src/enrich.rs +1 -1
- package/extractors/rust/src/imports.rs +1 -1
- package/extractors/rust/src/inheritance.rs +1 -1
- package/extractors/rust/src/nodes.rs +30 -10
- package/extractors/rust/src/orm_edges.rs +2 -2
- package/extractors/rust/src/parser.rs +8 -3
- package/extractors/rust/src/routes.rs +2 -2
- package/extractors/rust/src/tests.rs +1 -1
- package/package.json +1 -1
package/dist/extractors/index.js
CHANGED
|
@@ -11,6 +11,83 @@ export function defaultParseResult(stdout) {
|
|
|
11
11
|
}
|
|
12
12
|
// Subprocess Extractor Registry
|
|
13
13
|
// These extractors are spawned as child processes.
|
|
14
|
+
// ── extractor artifact resolution ────────────────────────────────────────────
|
|
15
|
+
// The subprocess extractors (python venv, java jar, go/rust static binaries)
|
|
16
|
+
// are DATA files inside the devlensio package. In a normal install they sit at
|
|
17
|
+
// `<pkg>/extractors/…` and resolve relative to this module. But when devlensio
|
|
18
|
+
// is BUNDLED into a standalone binary (`bun build --compile`, which is how the
|
|
19
|
+
// `@devlensio/cli` and MCP server ship), `import.meta.url` points at the binary
|
|
20
|
+
// itself, so the URL-relative path resolves to `/extractors/…` (nonexistent).
|
|
21
|
+
// The fallbacks below find the real extractor root for that case.
|
|
22
|
+
function resolveExtractorsRoot() {
|
|
23
|
+
// 1. Env override (CI / unusual layouts): DEVLENS_EXTRACTORS_DIR = the
|
|
24
|
+
// `extractors/` directory itself.
|
|
25
|
+
const env = process.env.DEVLENS_EXTRACTORS_DIR;
|
|
26
|
+
if (env) {
|
|
27
|
+
const candidate = path.resolve(env);
|
|
28
|
+
if (fs.existsSync(path.join(candidate, "python")) && fs.existsSync(path.join(candidate, "java"))) {
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// 2. Normal install: `<pkg>/dist/extractors/../../extractors` = `<pkg>/extractors`.
|
|
33
|
+
const viaModule = fileURLToPath(new URL("../../extractors", import.meta.url));
|
|
34
|
+
if (fs.existsSync(viaModule))
|
|
35
|
+
return viaModule;
|
|
36
|
+
// 3. Bundled binary: walk up from this bundle's own directory AND from the
|
|
37
|
+
// cwd to find `<…>/node_modules/devlensio/extractors`. Covers running the
|
|
38
|
+
// compiled CLI inside a project that depends on devlensio, and global
|
|
39
|
+
// installs where devlensio sits hoisted beside the CLI package.
|
|
40
|
+
const scanRoots = [
|
|
41
|
+
fileURLToPath(new URL(".", import.meta.url)),
|
|
42
|
+
process.cwd(),
|
|
43
|
+
];
|
|
44
|
+
for (const start of scanRoots) {
|
|
45
|
+
let dir = path.resolve(start);
|
|
46
|
+
for (;;) {
|
|
47
|
+
const candidate = path.join(dir, "node_modules", "devlensio", "extractors");
|
|
48
|
+
if (fs.existsSync(candidate))
|
|
49
|
+
return candidate;
|
|
50
|
+
const parent = path.dirname(dir);
|
|
51
|
+
if (parent === dir)
|
|
52
|
+
break;
|
|
53
|
+
dir = parent;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const extractorsRoot = resolveExtractorsRoot();
|
|
59
|
+
/** Absolute path to an extractor artifact under the resolved root (or null). */
|
|
60
|
+
function extractorArtifact(rel) {
|
|
61
|
+
return extractorsRoot ? path.join(extractorsRoot, rel) : null;
|
|
62
|
+
}
|
|
63
|
+
function resolvePythonCommand() {
|
|
64
|
+
const venvBin = process.platform === "win32" ? "Scripts/python.exe" : "bin/python";
|
|
65
|
+
const venvPython = extractorArtifact(`python/.venv/${venvBin}`) ??
|
|
66
|
+
// Last resort: URL-relative (pre-fix behavior for direct-from-dist runs).
|
|
67
|
+
fileURLToPath(new URL(`../../extractors/python/.venv/${venvBin}`, import.meta.url));
|
|
68
|
+
return fs.existsSync(venvPython) ? venvPython : "python3";
|
|
69
|
+
}
|
|
70
|
+
function platformDir() {
|
|
71
|
+
return process.platform === "win32"
|
|
72
|
+
? "windows-amd64"
|
|
73
|
+
: process.platform === "darwin"
|
|
74
|
+
? `darwin-${process.arch === "arm64" ? "arm64" : "amd64"}`
|
|
75
|
+
: `linux-${process.arch === "arm64" ? "arm64" : "amd64"}`;
|
|
76
|
+
}
|
|
77
|
+
function resolveJavaJarPath() {
|
|
78
|
+
const rel = "java/devlens_java_extractor.jar";
|
|
79
|
+
return extractorArtifact(rel) ?? fileURLToPath(new URL(`../../extractors/${rel}`, import.meta.url));
|
|
80
|
+
}
|
|
81
|
+
function resolveGoBinaryPath() {
|
|
82
|
+
const exe = process.platform === "win32" ? ".exe" : "";
|
|
83
|
+
const rel = `go/bin/${platformDir()}/devlens_go_extractor${exe}`;
|
|
84
|
+
return extractorArtifact(rel) ?? fileURLToPath(new URL(`../../extractors/${rel}`, import.meta.url));
|
|
85
|
+
}
|
|
86
|
+
function resolveRustBinaryPath() {
|
|
87
|
+
const exe = process.platform === "win32" ? ".exe" : "";
|
|
88
|
+
const rel = `rust/bin/${platformDir()}/devlens_rust_extractor${exe}`;
|
|
89
|
+
return extractorArtifact(rel) ?? fileURLToPath(new URL(`../../extractors/${rel}`, import.meta.url));
|
|
90
|
+
}
|
|
14
91
|
const SUBPROCESS_EXTRACTORS = {
|
|
15
92
|
python: {
|
|
16
93
|
language: "python",
|
|
@@ -26,7 +103,7 @@ const SUBPROCESS_EXTRACTORS = {
|
|
|
26
103
|
// Absolute path — the runner spawns with cwd=repoPath, so a bare jar
|
|
27
104
|
// name would be looked up inside the analyzed repo. fileURLToPath
|
|
28
105
|
// also decodes percent-escapes (spaces in the path).
|
|
29
|
-
args: ["-jar",
|
|
106
|
+
args: ["-jar", resolveJavaJarPath()],
|
|
30
107
|
parseResult: defaultParseResult,
|
|
31
108
|
},
|
|
32
109
|
go: {
|
|
@@ -52,44 +129,6 @@ const SUBPROCESS_EXTRACTORS = {
|
|
|
52
129
|
export function getExtractor(language) {
|
|
53
130
|
return SUBPROCESS_EXTRACTORS[language];
|
|
54
131
|
}
|
|
55
|
-
// The python extractor is a pip package installed into a venv that the
|
|
56
|
-
// postinstall script creates (extractors/python/setup.mjs). Resolve that
|
|
57
|
-
// venv's interpreter absolutely — same pattern as the java jar path —
|
|
58
|
-
// because the runner spawns with cwd=repoPath. Platform-aware: Windows
|
|
59
|
-
// venvs put the interpreter at Scripts\python.exe, Unix at bin/python.
|
|
60
|
-
function resolvePythonCommand() {
|
|
61
|
-
const venvBin = process.platform === "win32" ? "Scripts/python.exe" : "bin/python";
|
|
62
|
-
const venvPython = fileURLToPath(new URL(`../../extractors/python/.venv/${venvBin}`, import.meta.url));
|
|
63
|
-
return fs.existsSync(venvPython) ? venvPython : "python3";
|
|
64
|
-
}
|
|
65
|
-
// The Go extractor ships as a static binary per platform, cross-compiled at
|
|
66
|
-
// publish time (prepack → extractors/go/build.mjs). Resolve the CURRENT
|
|
67
|
-
// platform's binary absolutely — same reason as the java jar path (the runner
|
|
68
|
-
// spawns with cwd=repoPath). fileURLToPath also decodes percent-escapes
|
|
69
|
-
// (spaces in the path).
|
|
70
|
-
function resolveGoBinaryPath() {
|
|
71
|
-
const platformDir = process.platform === "win32"
|
|
72
|
-
? "windows-amd64"
|
|
73
|
-
: process.platform === "darwin"
|
|
74
|
-
? `darwin-${process.arch === "arm64" ? "arm64" : "amd64"}`
|
|
75
|
-
: `linux-${process.arch === "arm64" ? "arm64" : "amd64"}`;
|
|
76
|
-
const exe = process.platform === "win32" ? ".exe" : "";
|
|
77
|
-
return fileURLToPath(new URL(`../../extractors/go/bin/${platformDir}/devlens_go_extractor${exe}`, import.meta.url));
|
|
78
|
-
}
|
|
79
|
-
// The Rust extractor ships as a static binary per platform, cross-compiled at
|
|
80
|
-
// publish time (prepack → extractors/rust/build.mjs). Resolve the CURRENT
|
|
81
|
-
// platform's binary absolutely — same reason as the go binary path (the runner
|
|
82
|
-
// spawns with cwd=repoPath). fileURLToPath also decodes percent-escapes
|
|
83
|
-
// (spaces in the path).
|
|
84
|
-
function resolveRustBinaryPath() {
|
|
85
|
-
const platformDir = process.platform === "win32"
|
|
86
|
-
? "windows-amd64"
|
|
87
|
-
: process.platform === "darwin"
|
|
88
|
-
? `darwin-${process.arch === "arm64" ? "arm64" : "amd64"}`
|
|
89
|
-
: `linux-${process.arch === "arm64" ? "arm64" : "amd64"}`;
|
|
90
|
-
const exe = process.platform === "win32" ? ".exe" : "";
|
|
91
|
-
return fileURLToPath(new URL(`../../extractors/rust/bin/${platformDir}/devlens_rust_extractor${exe}`, import.meta.url));
|
|
92
|
-
}
|
|
93
132
|
// langauges handled inline meaning JS/TS
|
|
94
133
|
export const INLINE_LANGUAGES = new Set(["javascript", "typescript"]);
|
|
95
134
|
// Does `command` resolve? Absolute path → existsSync; bare name → PATH scan
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -34,7 +34,7 @@ pub fn detect_imports(
|
|
|
34
34
|
let mut seen: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
|
|
35
35
|
|
|
36
36
|
for pf in &repo.files {
|
|
37
|
-
if pf.
|
|
37
|
+
if pf.is_pure_test_file {
|
|
38
38
|
// test files ARE leaf nodes, but they still import — emit edges
|
|
39
39
|
// from the TEST node (same file id — parentFile covers it)
|
|
40
40
|
}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Contract rules enforced here:
|
|
4
4
|
// - FILE id `file::rel/path.rs`; file nodes parent themselves
|
|
5
|
-
// -
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// - PURE test files (tests/ integration files, or files whose every item is
|
|
6
|
+
// a #[test] fn) = LEAF TEST nodes; Rust's idiomatic inline #[cfg(test)]
|
|
7
|
+
// mod tests must NOT collapse a mixed production file — its non-test items
|
|
8
|
+
// emit normally and #[test] fns emit as FUNCTION nodes flagged
|
|
9
|
+
// metadata.isTestFunction (test bodies stay out of prod call edges there)
|
|
8
10
|
// - rawCode + codeHash on every FUNCTION/METHOD/STRUCT/ENUM/TRAIT/IMPL_BLOCK
|
|
9
11
|
// - deterministic ids (file-scoped; trait-impl method suffix for the
|
|
10
12
|
// same-name-different-trait collision class)
|
|
@@ -17,7 +19,7 @@ use serde_json::{Map, Value};
|
|
|
17
19
|
pub fn collect_file_nodes(files: &[ParsedFile]) -> Vec<CodeNode> {
|
|
18
20
|
let mut out = vec![];
|
|
19
21
|
for pf in files {
|
|
20
|
-
if pf.
|
|
22
|
+
if pf.is_pure_test_file {
|
|
21
23
|
let mut n = file_node(&pf.rel_path, pf.line_count, NODE_TEST);
|
|
22
24
|
let meta = n
|
|
23
25
|
.metadata
|
|
@@ -30,25 +32,43 @@ pub fn collect_file_nodes(files: &[ParsedFile]) -> Vec<CodeNode> {
|
|
|
30
32
|
}
|
|
31
33
|
out.push(n);
|
|
32
34
|
} else {
|
|
33
|
-
|
|
35
|
+
let mut n = file_node(&pf.rel_path, pf.line_count, NODE_FILE);
|
|
36
|
+
// Mixed files keep a testCases index so consumers that never walk
|
|
37
|
+
// FUNCTION nodes still see the file's tests.
|
|
38
|
+
if !pf.test_fns.is_empty() {
|
|
39
|
+
let meta = n
|
|
40
|
+
.metadata
|
|
41
|
+
.entry("testCases".to_string())
|
|
42
|
+
.or_insert_with(|| Value::Array(vec![]));
|
|
43
|
+
if let Value::Array(arr) = meta {
|
|
44
|
+
for t in &pf.test_fns {
|
|
45
|
+
arr.push(Value::from(t.clone()));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
out.push(n);
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
52
|
out
|
|
37
53
|
}
|
|
38
54
|
|
|
39
|
-
/// Code nodes
|
|
40
|
-
///
|
|
55
|
+
/// Code nodes. PURE test files stay leaves (their fns ride in testCases).
|
|
56
|
+
/// Mixed files emit ALL items; #[test] fns are flagged metadata.isTestFunction
|
|
57
|
+
/// instead of being dropped, so a test function is addressable without a new
|
|
58
|
+
/// node type.
|
|
41
59
|
pub fn collect_code_nodes(files: &[ParsedFile]) -> Vec<CodeNode> {
|
|
42
60
|
let mut out = vec![];
|
|
43
61
|
for pf in files {
|
|
44
|
-
if pf.
|
|
62
|
+
if pf.is_pure_test_file {
|
|
45
63
|
continue;
|
|
46
64
|
}
|
|
47
65
|
for item in &pf.items {
|
|
66
|
+
let mut n = item_node(pf, item);
|
|
48
67
|
if item.is_test {
|
|
49
|
-
|
|
68
|
+
n.metadata
|
|
69
|
+
.insert("isTestFunction".to_string(), Value::from(true));
|
|
50
70
|
}
|
|
51
|
-
out.push(
|
|
71
|
+
out.push(n);
|
|
52
72
|
}
|
|
53
73
|
}
|
|
54
74
|
out
|
|
@@ -21,7 +21,7 @@ use std::collections::HashMap;
|
|
|
21
21
|
pub fn table_to_model_map(repo: &ParsedRepo) -> HashMap<String, String> {
|
|
22
22
|
let mut map = HashMap::new();
|
|
23
23
|
for pf in &repo.files {
|
|
24
|
-
if pf.
|
|
24
|
+
if pf.is_pure_test_file {
|
|
25
25
|
continue;
|
|
26
26
|
}
|
|
27
27
|
for item in &pf.items {
|
|
@@ -89,7 +89,7 @@ pub fn detect_orm_edges(
|
|
|
89
89
|
let table_to_schema_file = table_to_schema_file_map(repo);
|
|
90
90
|
|
|
91
91
|
for pf in &repo.files {
|
|
92
|
-
if pf.
|
|
92
|
+
if pf.is_pure_test_file {
|
|
93
93
|
continue;
|
|
94
94
|
}
|
|
95
95
|
for item in &pf.items {
|
|
@@ -162,7 +162,12 @@ pub struct ParsedFile {
|
|
|
162
162
|
pub items: Vec<ParsedItem>,
|
|
163
163
|
pub tables: Vec<TableDecl>,
|
|
164
164
|
pub test_fns: Vec<String>,
|
|
165
|
-
|
|
165
|
+
/// True only when the file is EXCLUSIVELY tests (every top-level item is a
|
|
166
|
+
/// #[test] fn, or the file only holds inline #[cfg(test)] mods). Rust's
|
|
167
|
+
/// idiomatic inline `#[cfg(test)] mod tests` must NOT collapse a mixed
|
|
168
|
+
/// production file into a TEST leaf — only pure test files get leaf
|
|
169
|
+
/// treatment (mirrors the intent of go's _test.go rule).
|
|
170
|
+
pub is_pure_test_file: bool,
|
|
166
171
|
pub line_count: i64,
|
|
167
172
|
}
|
|
168
173
|
|
|
@@ -326,7 +331,7 @@ pub fn parse_file(rel_path: &str, source: &str) -> Result<ParsedFile, String> {
|
|
|
326
331
|
items: vec![],
|
|
327
332
|
tables: vec![],
|
|
328
333
|
test_fns: vec![],
|
|
329
|
-
|
|
334
|
+
is_pure_test_file: false,
|
|
330
335
|
line_count: line_index.line_count() as i64,
|
|
331
336
|
};
|
|
332
337
|
|
|
@@ -342,7 +347,7 @@ pub fn parse_file(rel_path: &str, source: &str) -> Result<ParsedFile, String> {
|
|
|
342
347
|
}
|
|
343
348
|
pf.ast.items = ast_items;
|
|
344
349
|
|
|
345
|
-
pf.
|
|
350
|
+
pf.is_pure_test_file = !pf.test_fns.is_empty() && pf.items.iter().all(|i| i.is_test);
|
|
346
351
|
Ok(pf)
|
|
347
352
|
}
|
|
348
353
|
|
|
@@ -96,7 +96,7 @@ pub fn detect_routes(
|
|
|
96
96
|
let mut func_lit_vars: HashMap<(String, String), String> = HashMap::new();
|
|
97
97
|
|
|
98
98
|
for pf in &repo.files {
|
|
99
|
-
if pf.
|
|
99
|
+
if pf.is_pure_test_file {
|
|
100
100
|
continue;
|
|
101
101
|
}
|
|
102
102
|
for item in &pf.items {
|
|
@@ -195,7 +195,7 @@ pub fn detect_routes(
|
|
|
195
195
|
// ── actix-web / rocket: attribute macros ──
|
|
196
196
|
if framework == "actix-web" || framework == "rocket" {
|
|
197
197
|
for pf in &repo.files {
|
|
198
|
-
if pf.
|
|
198
|
+
if pf.is_pure_test_file {
|
|
199
199
|
continue;
|
|
200
200
|
}
|
|
201
201
|
for item in &pf.items {
|
package/package.json
CHANGED