weavatrix 0.2.15 → 0.2.16
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 +53 -13
- package/docs/releases/v0.2.16.md +53 -0
- package/package.json +6 -2
- package/src/analysis/cargo-dependency-evidence.js +111 -0
- package/src/analysis/cargo-manifests.js +74 -0
- package/src/analysis/dep-check-ecosystems.js +3 -0
- package/src/analysis/endpoints-java.js +2 -5
- package/src/analysis/findings.js +12 -0
- package/src/analysis/go-dependency-evidence.js +68 -0
- package/src/analysis/health-capabilities.js +1 -1
- package/src/analysis/http-contracts/analysis.js +2 -0
- package/src/analysis/http-contracts/client-call-detection.js +1 -0
- package/src/analysis/http-contracts/client-call-parser.js +18 -4
- package/src/analysis/internal-audit/dependency-health.js +15 -13
- package/src/analysis/internal-audit/python-manifests.js +18 -5
- package/src/analysis/internal-audit/supply-chain.js +2 -0
- package/src/analysis/internal-audit.run.js +12 -7
- package/src/analysis/jvm-dependency-evidence.js +102 -56
- package/src/analysis/jvm-manifests.js +114 -0
- package/src/analysis/source-correctness.js +2 -5
- package/src/analysis/transport-contracts.js +157 -0
- package/src/graph/builder/lang-go.js +4 -2
- package/src/graph/builder/lang-java.js +12 -3
- package/src/graph/builder/lang-rust.js +22 -3
- package/src/graph/freshness-probe.js +1 -1
- package/src/graph/internal-builder.build.js +2 -2
- package/src/graph/internal-builder.resolvers.js +18 -8
- package/src/mcp/actions/advisories.mjs +2 -3
- package/src/mcp/catalog.mjs +7 -4
- package/src/mcp/company-contract-verdict.mjs +42 -0
- package/src/mcp/tools-company.mjs +28 -53
- package/src/mcp/tools-impact-precision.mjs +89 -0
- package/src/mcp/tools-impact.mjs +52 -94
- package/src/precision/lsp-overlay/build.js +10 -0
- package/src/precision/lsp-overlay/contract.js +1 -1
- package/src/precision/symbol-query.js +39 -0
- package/src/precision/typescript-provider/client.js +16 -3
- package/src/precision/typescript-provider/discovery.js +1 -1
- package/src/precision/typescript-provider/isolated-runtime.js +39 -0
- package/src/precision/typescript-provider/project-host.js +11 -6
- package/src/precision/typescript-provider/project-safety.js +5 -0
- package/src/security/advisory-store.js +2 -2
- package/src/security/installed-jvm-rust.js +46 -0
- package/src/security/installed.js +21 -1
- package/src/util.js +8 -0
- package/docs/releases/v0.2.15.md +0 -37
|
@@ -10,6 +10,7 @@ export function buildDependencyHealth({
|
|
|
10
10
|
goDep,
|
|
11
11
|
pyDep,
|
|
12
12
|
jvmDependencies,
|
|
13
|
+
cargoDep,
|
|
13
14
|
externalImports,
|
|
14
15
|
findings,
|
|
15
16
|
packageScopes,
|
|
@@ -20,8 +21,8 @@ export function buildDependencyHealth({
|
|
|
20
21
|
const dependencyFindings = findings.filter((finding) => ["unused-dep", "missing-dep", "duplicate-dep"].includes(finding.rule));
|
|
21
22
|
const graphComplete = !((graph.graphBuildMode && graph.graphBuildMode !== "full") || graph.graphBuildScope);
|
|
22
23
|
const npmManifests = repoFiles.filter((file) => /(^|\/)package\.json$/i.test(file));
|
|
23
|
-
const goManifests = repoFiles.filter((file) => /(^|\/)go\.mod$/i.test(file));
|
|
24
24
|
const pythonManifests = [...new Set((pyManifest.scopes || []).flatMap((scope) => scope.manifests || []))];
|
|
25
|
+
const publicEvidence = ({ findings: _findings, ...item }) => item;
|
|
25
26
|
const ecosystems = {
|
|
26
27
|
npm: {
|
|
27
28
|
ecosystem: "npm",
|
|
@@ -36,28 +37,29 @@ export function buildDependencyHealth({
|
|
|
36
37
|
},
|
|
37
38
|
go: {
|
|
38
39
|
ecosystem: "go",
|
|
39
|
-
present:
|
|
40
|
-
status:
|
|
41
|
-
completeness:
|
|
42
|
-
manifests:
|
|
40
|
+
present: goDep.present,
|
|
41
|
+
status: goDep.status,
|
|
42
|
+
completeness: graphComplete && goDep.completeness === "COMPLETE" ? "COMPLETE" : goDep.completeness,
|
|
43
|
+
manifests: goDep.manifests,
|
|
43
44
|
declared: goDep.declared.size,
|
|
44
|
-
reason:
|
|
45
|
-
? "Go dependency checks use indexed imports and the root go.mod; nested modules and build-tag-specific resolution are not fully modeled."
|
|
46
|
-
: "No go.mod was discovered.",
|
|
45
|
+
reason: goDep.reason,
|
|
47
46
|
},
|
|
48
47
|
python: {
|
|
49
48
|
ecosystem: "python",
|
|
50
49
|
present: pyManifest.present,
|
|
51
50
|
status: pyManifest.present ? "CHECKED" : "NOT_PRESENT",
|
|
52
|
-
completeness: pyManifest.present ? "PARTIAL" : "NOT_APPLICABLE",
|
|
51
|
+
completeness: pyManifest.present ? (graphComplete && pyManifest.completeness === "COMPLETE" ? "COMPLETE" : "PARTIAL") : "NOT_APPLICABLE",
|
|
53
52
|
manifests: pythonManifests,
|
|
54
53
|
declared: pyDep.declared.size,
|
|
55
54
|
reason: pyManifest.present
|
|
56
|
-
?
|
|
55
|
+
? pyManifest.reasons?.length
|
|
56
|
+
? `Python declarations were compared with indexed imports, but ${pyManifest.reasons.join("; ")}.`
|
|
57
|
+
: "Every discovered supported Python manifest scope was compared with indexed imports. Environment markers and extras are normalized as declarations; runtime-computed imports remain outside static proof."
|
|
57
58
|
: "No supported Python dependency manifest was discovered.",
|
|
58
59
|
},
|
|
59
|
-
maven: { ecosystem: "maven", ...jvmDependencies.maven },
|
|
60
|
-
gradle: { ecosystem: "gradle", ...jvmDependencies.gradle },
|
|
60
|
+
maven: { ecosystem: "maven", ...publicEvidence(jvmDependencies.maven), completeness: jvmDependencies.maven.present && !graphComplete ? "PARTIAL" : jvmDependencies.maven.completeness },
|
|
61
|
+
gradle: { ecosystem: "gradle", ...publicEvidence(jvmDependencies.gradle), completeness: jvmDependencies.gradle.present && !graphComplete ? "PARTIAL" : jvmDependencies.gradle.completeness },
|
|
62
|
+
rust: { ecosystem: "rust", ...publicEvidence(cargoDep), declared: cargoDep.declared.size, completeness: cargoDep.present && !graphComplete ? "PARTIAL" : cargoDep.completeness },
|
|
61
63
|
};
|
|
62
64
|
const present = Object.values(ecosystems).filter((item) => item.present);
|
|
63
65
|
const status = present.length === 0
|
|
@@ -68,7 +70,7 @@ export function buildDependencyHealth({
|
|
|
68
70
|
const importedPackages = new Set(externalImports
|
|
69
71
|
.filter((entry) => entry?.pkg && !entry.builtin && !entry.unresolved)
|
|
70
72
|
.map((entry) => `${entry.ecosystem || (/\.java$/i.test(entry.file || "") ? "maven-unresolved" : "npm")}:${entry.pkg}`));
|
|
71
|
-
const supportedDeclared = dep.declared.size + goDep.declared.size + pyDep.declared.size;
|
|
73
|
+
const supportedDeclared = dep.declared.size + goDep.declared.size + pyDep.declared.size + cargoDep.declared.size;
|
|
72
74
|
const jvmDeclared = jvmDependencies.maven.declared + jvmDependencies.gradle.declared;
|
|
73
75
|
const measuredCoverage = readCoverageForRepo(repoPath, sourceFiles);
|
|
74
76
|
const healthCapabilities = buildHealthCapabilityMatrix({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {dirname} from 'node:path'
|
|
1
|
+
import {dirname, posix} from 'node:path'
|
|
2
2
|
import {createRepoBoundary} from '../../repo-path.js'
|
|
3
3
|
import {parseRequirementsNames, parsePyprojectDeps, parsePipfileDeps, pep503} from '../manifests.js'
|
|
4
4
|
import {listRepoFiles, readRepoText} from './repo-files.js'
|
|
@@ -25,6 +25,8 @@ export function collectPyManifest(repoRoot) {
|
|
|
25
25
|
scope.deps.push(...parsedDeps.map((dep) => ({...dep, manifest})))
|
|
26
26
|
}
|
|
27
27
|
const files = listRepoFiles(repoRoot)
|
|
28
|
+
const fileSet = new Set(files.map((file) => file.replace(/\\/g, '/')))
|
|
29
|
+
const reasons = []
|
|
28
30
|
for (const file of files.filter((name) => /(^|\/)requirements[\w.-]*\.(?:txt|in)$/i.test(name)
|
|
29
31
|
|| /(^|\/)requirements\/[^/]+\.(?:txt|in)$/i.test(name))) {
|
|
30
32
|
const text = readRepoText(boundary, file)
|
|
@@ -33,14 +35,23 @@ export function collectPyManifest(repoRoot) {
|
|
|
33
35
|
const root = /(^|\/)requirements$/i.test(parent) ? normalizeRoot(dirname(parent)) : parent
|
|
34
36
|
const dev = /dev|test|lint|doc|ci/i.test(file.slice(file.lastIndexOf('/') + 1))
|
|
35
37
|
addManifest(root, file, parseRequirementsNames(text).map((dep) => ({...dep, dev})))
|
|
38
|
+
for (const match of text.matchAll(/^\s*(?:-r|--requirement|-c|--constraint)\s+(?:=\s*)?([^\s#]+)/gmi)) {
|
|
39
|
+
const parentDir = normalizeRoot(dirname(file))
|
|
40
|
+
const target = normalizeRoot(posix.normalize(`${parentDir ? `${parentDir}/` : ''}${match[1].replace(/\\/g, '/')}`))
|
|
41
|
+
if (!target || target.startsWith('../') || !fileSet.has(target)) reasons.push(`${file}: referenced requirements file ${match[1]} was not discovered`)
|
|
42
|
+
}
|
|
36
43
|
}
|
|
37
44
|
for (const file of files.filter((name) => /(^|\/)pyproject\.toml$/i.test(name))) {
|
|
38
|
-
const
|
|
39
|
-
|
|
45
|
+
const text = readRepoText(boundary, file)
|
|
46
|
+
const parsed = parsePyprojectDeps(text)
|
|
47
|
+
const dependencyManifest = parsed.present || /^\s*\[(?:project|tool\.poetry)\]/m.test(text || '')
|
|
48
|
+
addManifest(dirname(file), file, parsed.deps, dependencyManifest)
|
|
49
|
+
if (/\bdynamic\s*=\s*\[[^\]]*["']dependencies["']/s.test(text || '')) reasons.push(`${file}: project dependencies are dynamic`)
|
|
40
50
|
}
|
|
41
51
|
for (const file of files.filter((name) => /(^|\/)Pipfile$/i.test(name))) {
|
|
42
|
-
const
|
|
43
|
-
|
|
52
|
+
const text = readRepoText(boundary, file)
|
|
53
|
+
const parsed = parsePipfileDeps(text)
|
|
54
|
+
addManifest(dirname(file), file, parsed.deps, parsed.present || /^\s*\[(?:packages|dev-packages)\]/mi.test(text || ''))
|
|
44
55
|
}
|
|
45
56
|
const normalizedScopes = [...scopes.values()]
|
|
46
57
|
.map((scope) => {
|
|
@@ -61,5 +72,7 @@ export function collectPyManifest(repoRoot) {
|
|
|
61
72
|
present: normalizedScopes.some((scope) => scope.present),
|
|
62
73
|
deps: normalizedScopes.flatMap((scope) => scope.deps),
|
|
63
74
|
scopes: normalizedScopes,
|
|
75
|
+
completeness: reasons.length ? 'PARTIAL' : 'COMPLETE',
|
|
76
|
+
reasons: [...new Set(reasons)].sort(),
|
|
64
77
|
}
|
|
65
78
|
}
|
|
@@ -12,6 +12,8 @@ const removalHint = (pkg) => {
|
|
|
12
12
|
if (pkg?.ecosystem === "npm") return `npm uninstall ${pkg.name} and audit what it touched`;
|
|
13
13
|
if (pkg?.ecosystem === "PyPI") return `remove ${pkg.name} from the Python environment/manifest and audit what it touched`;
|
|
14
14
|
if (pkg?.ecosystem === "Go") return `remove ${pkg.name} from the Go module graph, run go mod tidy, and audit what it touched`;
|
|
15
|
+
if (pkg?.ecosystem === "crates.io") return `cargo remove ${pkg.name}, rebuild every feature/target, and audit what it touched`;
|
|
16
|
+
if (pkg?.ecosystem === "Maven") return `remove or upgrade ${pkg.name} in Maven/Gradle, refresh the resolved graph, and audit what it touched`;
|
|
15
17
|
return `remove ${pkg?.name || "the package"} with its ecosystem's package manager and audit what it touched`;
|
|
16
18
|
};
|
|
17
19
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
import { computeDead, computeUnusedExports } from "./dead-check.js";
|
|
4
|
-
import {
|
|
4
|
+
import { computePyDepFindings, computeScopedDepFindings } from "./dep-check.js";
|
|
5
5
|
import { computeStructureFindings } from "./dep-rules.js";
|
|
6
6
|
import { makeFinding, sortFindings, summarizeFindings } from "./findings.js";
|
|
7
7
|
import { graphOutDirForRepo } from "../graph/layout.js";
|
|
@@ -15,17 +15,17 @@ import {
|
|
|
15
15
|
listRepoFiles,
|
|
16
16
|
readJson,
|
|
17
17
|
readRepoJson,
|
|
18
|
-
readRepoText,
|
|
19
18
|
workspacePkgNames,
|
|
20
19
|
} from "./internal-audit.collect.js";
|
|
21
20
|
import { computeReachability, entryFiles } from "./internal-audit.reach.js";
|
|
22
|
-
import { parseGoMod } from "./manifests.js";
|
|
23
21
|
import { PATH_CLASS_NAMES, createPathClassifier, hasPathClass } from "../path-classification.js";
|
|
24
22
|
import { createRepoBoundary } from "../repo-path.js";
|
|
25
23
|
import { analyzeSourceCorrectness } from "./source-correctness.js";
|
|
26
24
|
import { collectJvmDependencyEvidence } from "./jvm-dependency-evidence.js";
|
|
27
25
|
import { buildDependencyHealth } from "./internal-audit/dependency-health.js";
|
|
28
26
|
import { runSupplyChainChecks } from "./internal-audit/supply-chain.js";
|
|
27
|
+
import { collectGoDependencyEvidence } from "./go-dependency-evidence.js";
|
|
28
|
+
import { collectCargoDependencyEvidence } from "./cargo-dependency-evidence.js";
|
|
29
29
|
|
|
30
30
|
export async function runInternalAudit(repoPath, {
|
|
31
31
|
graph,
|
|
@@ -90,8 +90,7 @@ export async function runInternalAudit(repoPath, {
|
|
|
90
90
|
nonRuntimeRoots,
|
|
91
91
|
sourceFiles: [...sources.keys()],
|
|
92
92
|
});
|
|
93
|
-
const
|
|
94
|
-
const goDep = computeGoDepFindings({ externalImports, goMod: goModText != null ? parseGoMod(goModText) : null, nonRuntimeRoots });
|
|
93
|
+
const goDep = collectGoDependencyEvidence(repoPath, { files: repoFiles, externalImports, nonRuntimeRoots });
|
|
95
94
|
const asList = (value) => Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
96
95
|
const pyRules = rules.python || {}, depRules = rules.dependencies || {};
|
|
97
96
|
const managedPython = [...new Set([
|
|
@@ -109,12 +108,17 @@ export async function runInternalAudit(repoPath, {
|
|
|
109
108
|
ignoredDependencies: ignoredPython,
|
|
110
109
|
nonRuntimeRoots,
|
|
111
110
|
});
|
|
112
|
-
const jvmDependencies = collectJvmDependencyEvidence(repoPath, { files: repoFiles });
|
|
111
|
+
const jvmDependencies = collectJvmDependencyEvidence(repoPath, { files: repoFiles, externalImports });
|
|
112
|
+
const cargoDep = collectCargoDependencyEvidence(repoPath, { files: repoFiles, externalImports });
|
|
113
113
|
|
|
114
114
|
const externalImportFiles = new Set(externalImports.filter((entry) => entry.pkg && !entry.builtin).map((entry) => entry.file));
|
|
115
115
|
const structure = computeStructureFindings(graph, { rules, entrySet: entries, externalImportFiles });
|
|
116
116
|
const correctness = analyzeSourceCorrectness(sources, { isNonProductPath });
|
|
117
|
-
const findings = [
|
|
117
|
+
const findings = [
|
|
118
|
+
...dep.findings, ...goDep.findings, ...pyDep.findings,
|
|
119
|
+
...jvmDependencies.maven.findings, ...jvmDependencies.gradle.findings, ...cargoDep.findings,
|
|
120
|
+
...correctness.findings,
|
|
121
|
+
];
|
|
118
122
|
const deadFileSet = new Set(dead.deadFiles.map((finding) => finding.file));
|
|
119
123
|
for (const finding of structure.findings) {
|
|
120
124
|
if (!(finding.rule === "orphan-file" && deadFileSet.has(finding.file))) findings.push(finding);
|
|
@@ -190,6 +194,7 @@ export async function runInternalAudit(repoPath, {
|
|
|
190
194
|
goDep,
|
|
191
195
|
pyDep,
|
|
192
196
|
jvmDependencies,
|
|
197
|
+
cargoDep,
|
|
193
198
|
externalImports,
|
|
194
199
|
findings: sorted,
|
|
195
200
|
packageScopes,
|
|
@@ -1,69 +1,115 @@
|
|
|
1
|
-
// Maven/Gradle manifest presence and bounded declaration counts. We intentionally do not map Java
|
|
2
|
-
// imports to artifacts: package-to-artifact resolution needs a real build model and claiming 0/0 from
|
|
3
|
-
// source regexes would be worse than an explicit NOT_SUPPORTED/PARTIAL state.
|
|
4
1
|
import { createRepoBoundary } from "../repo-path.js";
|
|
2
|
+
import { dependencyVerification, makeFinding } from "./findings.js";
|
|
3
|
+
import { parseGradleDependencies, parseGradleVersionCatalog, parseMavenPom } from "./jvm-manifests.js";
|
|
5
4
|
import { listRepoFiles, readRepoText } from "./internal-audit.collect.js";
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
const normalize = (value) => String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
7
|
+
const javaBuiltin = (name) => /^(?:java|jdk|sun)\.|^(?:org\.w3c\.dom|org\.xml\.sax)(?:\.|$)/.test(name);
|
|
8
|
+
|
|
9
|
+
function mappingScore(spec, dependency) {
|
|
10
|
+
const imported = String(spec || "").replace(/\.\*$/, "");
|
|
11
|
+
if (!imported || !dependency.group) return 0;
|
|
12
|
+
if (imported === dependency.group || imported.startsWith(`${dependency.group}.`)) return 1_000 + dependency.group.length;
|
|
13
|
+
const compactImport = normalize(imported), compactArtifact = normalize(dependency.artifact);
|
|
14
|
+
if (compactArtifact.length >= 4 && compactImport.includes(compactArtifact)) return 500 + compactArtifact.length;
|
|
15
|
+
const artifactTokens = String(dependency.artifact).toLowerCase().split(/[-_.]+/).filter((part) => part.length >= 4 && !["core", "java", "client", "common", "api"].includes(part));
|
|
16
|
+
const hits = artifactTokens.filter((part) => imported.toLowerCase().split(".").some((segment) => normalize(segment) === normalize(part))).length;
|
|
17
|
+
return hits ? 100 + hits : 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function bestDependency(spec, dependencies) {
|
|
21
|
+
const ranked = dependencies.map((dependency) => ({ dependency, score: mappingScore(spec, dependency) }))
|
|
22
|
+
.filter((item) => item.score > 0).sort((left, right) => right.score - left.score || left.dependency.name.localeCompare(right.dependency.name));
|
|
23
|
+
if (!ranked.length || (ranked[1] && ranked[1].score === ranked[0].score)) return null;
|
|
24
|
+
return ranked[0].dependency;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
function
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
function analyze(ecosystem, manifests, dependencies, imports, unresolvedDeclarations, mappingDependencies = dependencies, includeMissing = true) {
|
|
28
|
+
const findings = [], used = new Map(), missing = new Map();
|
|
29
|
+
const owned = new Set(dependencies.map((dependency) => dependency.name));
|
|
30
|
+
for (const item of imports) {
|
|
31
|
+
const dependency = bestDependency(item.spec || item.pkg, mappingDependencies);
|
|
32
|
+
if (dependency) {
|
|
33
|
+
if (owned.has(dependency.name)) {
|
|
34
|
+
const list = used.get(dependency.name) || [];
|
|
35
|
+
list.push(item); used.set(dependency.name, list);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
const key = item.spec || item.pkg;
|
|
39
|
+
const list = missing.get(key) || [];
|
|
40
|
+
list.push(item); missing.set(key, list);
|
|
41
|
+
}
|
|
35
42
|
}
|
|
36
|
-
|
|
43
|
+
const runtimeOnly = /runtime|provided|classpath|annotationProcessor|kapt/i;
|
|
44
|
+
for (const dependency of dependencies) {
|
|
45
|
+
const evidence = used.get(dependency.name) || [];
|
|
46
|
+
if (evidence.length || dependency.optional || runtimeOnly.test(dependency.scope || "")) continue;
|
|
47
|
+
findings.push(makeFinding({
|
|
48
|
+
category: "unused", rule: "unused-dep", severity: "low", confidence: "low",
|
|
49
|
+
title: `Unused ${ecosystem} dependency: ${dependency.name}`,
|
|
50
|
+
reason: "No indexed Java import mapped to this declared artifact; reflection, service loading, generated code and runtime-only use remain possible.",
|
|
51
|
+
detail: `"${dependency.name}" is declared in ${dependency.file}, but no indexed Java import maps to it. Review framework, reflection, ServiceLoader and generated-source use before removal.`,
|
|
52
|
+
package: dependency.name, version: dependency.version, manifest: dependency.file, source: "internal",
|
|
53
|
+
verification: dependencyVerification(dependency.file, [], "REVIEW_REQUIRED", "group-prefix/artifact-token"),
|
|
54
|
+
fixHint: `remove ${dependency.name} only after the ${ecosystem} build and tests confirm it is unused`,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
for (const [spec, evidence] of includeMissing ? missing : []) {
|
|
58
|
+
if (javaBuiltin(spec)) continue;
|
|
59
|
+
findings.push(makeFinding({
|
|
60
|
+
category: "unused", rule: "missing-dep", severity: "medium", confidence: "medium",
|
|
61
|
+
title: `Unmapped Java import: ${spec}`,
|
|
62
|
+
reason: `The indexed Java import did not map to any declared ${ecosystem} artifact.`,
|
|
63
|
+
detail: `"${spec}" is imported by ${evidence.length} file(s), but no ${ecosystem} declaration has a matching group prefix or artifact token. Add the owning artifact or configure/build the source that supplies it.`,
|
|
64
|
+
package: spec, file: evidence[0].file, line: evidence[0].line || 0,
|
|
65
|
+
evidence: evidence.slice(0, 5).map((item) => ({ file: item.file, line: item.line || 0, snippet: item.spec || "" })),
|
|
66
|
+
source: "internal",
|
|
67
|
+
verification: { evidenceModel: "MANIFEST_PLUS_INDEXED_SOURCE", decision: "ACTION_REQUIRED", manifestDeclaration: { status: "NOT_FOUND", files: manifests }, indexedSourceImports: { status: "FOUND", count: evidence.length, files: evidence.map((item) => item.file).slice(0, 10) }, mapping: "group-prefix/artifact-token" },
|
|
68
|
+
fixHint: `identify the artifact that owns ${spec} and declare it in the nearest ${ecosystem} manifest`,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
const present = manifests.length > 0;
|
|
72
|
+
return {
|
|
73
|
+
present,
|
|
74
|
+
status: present ? "CHECKED" : "NOT_PRESENT",
|
|
75
|
+
completeness: present ? (unresolvedDeclarations ? "PARTIAL" : "COMPLETE") : "NOT_APPLICABLE",
|
|
76
|
+
manifests,
|
|
77
|
+
declared: dependencies.length,
|
|
78
|
+
mappedImports: [...used.values()].reduce((sum, list) => sum + list.length, 0),
|
|
79
|
+
unmappedImports: [...missing.values()].reduce((sum, list) => sum + list.length, 0),
|
|
80
|
+
unresolvedDeclarations,
|
|
81
|
+
sample: dependencies.slice(0, 20).map(({ file, name, version }) => ({ file, identity: name, version })),
|
|
82
|
+
reason: !present
|
|
83
|
+
? `No ${ecosystem === "maven" ? "pom.xml" : "Gradle build file"} was discovered.`
|
|
84
|
+
: unresolvedDeclarations
|
|
85
|
+
? `${dependencies.length} declarations and all indexed Java imports were checked, but ${unresolvedDeclarations} dynamic/property/catalog declaration(s) could not be resolved statically.`
|
|
86
|
+
: `${dependencies.length} declarations were compared with every indexed non-JDK Java import using bounded group-prefix and artifact-token evidence. Missing and unused results remain review evidence, not compiler proof.`,
|
|
87
|
+
findings,
|
|
88
|
+
};
|
|
37
89
|
}
|
|
38
90
|
|
|
39
|
-
export function collectJvmDependencyEvidence(repoRoot, { files = listRepoFiles(repoRoot) } = {}) {
|
|
91
|
+
export function collectJvmDependencyEvidence(repoRoot, { files = listRepoFiles(repoRoot), externalImports = [] } = {}) {
|
|
40
92
|
const boundary = createRepoBoundary(repoRoot);
|
|
41
93
|
const mavenFiles = files.filter((file) => /(^|\/)pom\.xml$/i.test(file));
|
|
42
|
-
const gradleFiles = files.filter((file) => /(^|\/)build\.gradle(?:\.kts)?$/i.test(file));
|
|
43
|
-
const
|
|
44
|
-
const
|
|
94
|
+
const gradleFiles = files.filter((file) => /(^|\/)(?:build|settings|[^/]+)\.gradle(?:\.kts)?$/i.test(file));
|
|
95
|
+
const catalogFiles = files.filter((file) => /(^|\/)libs\.versions\.toml$/i.test(file));
|
|
96
|
+
const catalog = new Map();
|
|
97
|
+
for (const file of catalogFiles) for (const [alias, entry] of parseGradleVersionCatalog(readRepoText(boundary, file))) catalog.set(alias, entry);
|
|
98
|
+
let mavenUnresolved = 0, gradleUnresolved = 0;
|
|
99
|
+
const mavenDependencies = mavenFiles.flatMap((file) => {
|
|
100
|
+
const parsed = parseMavenPom(readRepoText(boundary, file));
|
|
101
|
+
mavenUnresolved += parsed.unresolvedDeclarations;
|
|
102
|
+
return parsed.dependencies.map((dependency) => ({ ...dependency, file }));
|
|
103
|
+
});
|
|
104
|
+
const gradleDependencies = gradleFiles.flatMap((file) => {
|
|
105
|
+
const parsed = parseGradleDependencies(readRepoText(boundary, file), catalog);
|
|
106
|
+
gradleUnresolved += parsed.unresolvedDeclarations || 0;
|
|
107
|
+
return parsed.map((dependency) => ({ ...dependency, file }));
|
|
108
|
+
});
|
|
109
|
+
const javaImports = externalImports.filter((entry) => entry.ecosystem === "Maven" && entry.pkg && !entry.builtin && !entry.unresolved);
|
|
110
|
+
const allDependencies = [...mavenDependencies, ...gradleDependencies];
|
|
45
111
|
return {
|
|
46
|
-
maven:
|
|
47
|
-
|
|
48
|
-
status: mavenFiles.length ? "NOT_SUPPORTED" : "NOT_PRESENT",
|
|
49
|
-
completeness: mavenFiles.length ? "PARTIAL" : "NOT_APPLICABLE",
|
|
50
|
-
manifests: mavenFiles,
|
|
51
|
-
declared: maven.length,
|
|
52
|
-
sample: maven.slice(0, 20),
|
|
53
|
-
reason: mavenFiles.length
|
|
54
|
-
? "Maven manifests and declaration counts were detected, but Java package imports were not mapped to Maven artifacts; unused/missing dependency verdicts are not supported."
|
|
55
|
-
: "No pom.xml was discovered.",
|
|
56
|
-
},
|
|
57
|
-
gradle: {
|
|
58
|
-
present: gradleFiles.length > 0,
|
|
59
|
-
status: gradleFiles.length ? "NOT_SUPPORTED" : "NOT_PRESENT",
|
|
60
|
-
completeness: gradleFiles.length ? "PARTIAL" : "NOT_APPLICABLE",
|
|
61
|
-
manifests: gradleFiles,
|
|
62
|
-
declared: gradle.length,
|
|
63
|
-
sample: gradle.slice(0, 20),
|
|
64
|
-
reason: gradleFiles.length
|
|
65
|
-
? "Gradle manifests and bounded dependency declarations were detected, but version catalogs/build logic and Java package-to-artifact mapping were not resolved; unused/missing dependency verdicts are not supported."
|
|
66
|
-
: "No build.gradle/build.gradle.kts was discovered.",
|
|
67
|
-
},
|
|
112
|
+
maven: analyze("maven", mavenFiles, mavenDependencies, javaImports, mavenUnresolved, allDependencies, true),
|
|
113
|
+
gradle: analyze("gradle", [...gradleFiles, ...catalogFiles], gradleDependencies, javaImports, gradleUnresolved, allDependencies, mavenFiles.length === 0),
|
|
68
114
|
};
|
|
69
115
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const stripComments = (value) => String(value || "").replace(/<!--[\s\S]*?-->/g, " ");
|
|
2
|
+
const clean = (value) => String(value || "").trim().replace(/^['"]|['"]$/g, "");
|
|
3
|
+
|
|
4
|
+
function xmlValue(body, name) {
|
|
5
|
+
return new RegExp(`<${name}>\\s*([^<]+?)\\s*</${name}>`, "i").exec(body)?.[1]?.trim() || "";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function resolveMavenValue(value, properties) {
|
|
9
|
+
let output = String(value || "");
|
|
10
|
+
for (let pass = 0; pass < 8; pass++) {
|
|
11
|
+
let changed = false;
|
|
12
|
+
output = output.replace(/\$\{([^}]+)}/g, (whole, key) => {
|
|
13
|
+
if (!properties.has(key)) return whole;
|
|
14
|
+
changed = true;
|
|
15
|
+
return properties.get(key);
|
|
16
|
+
});
|
|
17
|
+
if (!changed) break;
|
|
18
|
+
}
|
|
19
|
+
return /\$\{/.test(output) ? "" : output.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseMavenPom(text) {
|
|
23
|
+
const source = stripComments(text);
|
|
24
|
+
const properties = new Map();
|
|
25
|
+
const propertyBlock = /<properties\b[^>]*>([\s\S]*?)<\/properties>/i.exec(source)?.[1] || "";
|
|
26
|
+
for (const match of propertyBlock.matchAll(/<([A-Za-z0-9_.-]+)>\s*([^<]+?)\s*<\/\1>/g)) properties.set(match[1], match[2].trim());
|
|
27
|
+
const parent = /<parent\b[^>]*>([\s\S]*?)<\/parent>/i.exec(source)?.[1] || "";
|
|
28
|
+
const beforeDependencies = source.split(/<dependencies\b/i)[0];
|
|
29
|
+
const projectVersion = xmlValue(beforeDependencies, "version") || xmlValue(parent, "version");
|
|
30
|
+
const projectGroup = xmlValue(beforeDependencies, "groupId") || xmlValue(parent, "groupId");
|
|
31
|
+
properties.set("project.version", projectVersion); properties.set("pom.version", projectVersion);
|
|
32
|
+
properties.set("project.groupId", projectGroup); properties.set("pom.groupId", projectGroup);
|
|
33
|
+
const managed = new Map();
|
|
34
|
+
for (const block of source.matchAll(/<dependencyManagement\b[^>]*>([\s\S]*?)<\/dependencyManagement>/gi)) {
|
|
35
|
+
for (const dep of block[1].matchAll(/<dependency\b[^>]*>([\s\S]*?)<\/dependency>/gi)) {
|
|
36
|
+
const group = resolveMavenValue(xmlValue(dep[1], "groupId"), properties);
|
|
37
|
+
const artifact = resolveMavenValue(xmlValue(dep[1], "artifactId"), properties);
|
|
38
|
+
const version = resolveMavenValue(xmlValue(dep[1], "version"), properties);
|
|
39
|
+
if (group && artifact && version) managed.set(`${group}:${artifact}`, version);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const directSource = source.replace(/<dependencyManagement\b[^>]*>[\s\S]*?<\/dependencyManagement>/gi, " ");
|
|
43
|
+
const dependencies = [];
|
|
44
|
+
const directBlocks = [...directSource.matchAll(/<dependency\b[^>]*>([\s\S]*?)<\/dependency>/gi)];
|
|
45
|
+
for (const match of directBlocks) {
|
|
46
|
+
const group = resolveMavenValue(xmlValue(match[1], "groupId"), properties);
|
|
47
|
+
const artifact = resolveMavenValue(xmlValue(match[1], "artifactId"), properties);
|
|
48
|
+
if (!group || !artifact) continue;
|
|
49
|
+
dependencies.push({
|
|
50
|
+
group, artifact, name: `${group}:${artifact}`,
|
|
51
|
+
version: resolveMavenValue(xmlValue(match[1], "version"), properties) || managed.get(`${group}:${artifact}`) || "",
|
|
52
|
+
scope: xmlValue(match[1], "scope") || "compile",
|
|
53
|
+
optional: xmlValue(match[1], "optional") === "true",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return { projectGroup, projectVersion, dependencies, unresolvedDeclarations: directBlocks.length - dependencies.length };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function parseGradleVersionCatalog(text) {
|
|
60
|
+
const versions = new Map(), libraries = new Map();
|
|
61
|
+
let section = "";
|
|
62
|
+
for (const raw of String(text || "").split(/\r?\n/)) {
|
|
63
|
+
const line = raw.replace(/(^|\s)#.*$/, "").trim();
|
|
64
|
+
const header = /^\[([^\]]+)]$/.exec(line);
|
|
65
|
+
if (header) { section = header[1]; continue; }
|
|
66
|
+
const kv = /^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/.exec(line);
|
|
67
|
+
if (!kv) continue;
|
|
68
|
+
if (section === "versions") { versions.set(kv[1], clean(kv[2])); continue; }
|
|
69
|
+
if (section !== "libraries") continue;
|
|
70
|
+
const alias = kv[1], value = kv[2];
|
|
71
|
+
const module = /\bmodule\s*=\s*["']([^"']+)["']/.exec(value)?.[1]
|
|
72
|
+
|| /\bgroup\s*=\s*["']([^"']+)["'][\s\S]*?\bname\s*=\s*["']([^"']+)["']/.exec(value)?.slice(1, 3).join(":")
|
|
73
|
+
|| (/^["'][^"']+:[^"']+["']$/.test(value) ? clean(value) : "");
|
|
74
|
+
if (!module) continue;
|
|
75
|
+
const explicit = /\bversion\s*=\s*["']([^"']+)["']/.exec(value)?.[1] || "";
|
|
76
|
+
const reference = /\bversion\.ref\s*=\s*["']([^"']+)["']/.exec(value)?.[1] || "";
|
|
77
|
+
libraries.set(alias.replace(/[_.]/g, "-"), { module, version: explicit || versions.get(reference) || "" });
|
|
78
|
+
}
|
|
79
|
+
return libraries;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parseGradleDependencies(text, catalog = new Map()) {
|
|
83
|
+
const source = String(text || "").replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|\s)\/\/.*$/gm, "$1");
|
|
84
|
+
const dependencies = [];
|
|
85
|
+
const configurations = "api|implementation|compile|compileOnly|runtimeOnly|annotationProcessor|kapt|classpath|testImplementation|testCompileOnly|testRuntimeOnly|androidTestImplementation";
|
|
86
|
+
const matcher = new RegExp(`^\\s*(${configurations})\\s*(?:\\(\\s*)?([^\\r\\n]+)`, "gmi");
|
|
87
|
+
const declarations = [...source.matchAll(matcher)];
|
|
88
|
+
for (const match of declarations) {
|
|
89
|
+
const expression = match[2].trim().replace(/[),;]+\s*$/, "");
|
|
90
|
+
const coordinate = /["']([^"']+:[^"']+)["']/.exec(expression)?.[1] || "";
|
|
91
|
+
if (coordinate) {
|
|
92
|
+
const [group, artifact, version = ""] = coordinate.split(":");
|
|
93
|
+
if (group && artifact) dependencies.push({ group, artifact, name: `${group}:${artifact}`, version, scope: match[1] });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const alias = /\blibs((?:\.[A-Za-z_]\w*)+)/.exec(expression)?.[1]?.slice(1).replace(/\./g, "-") || "";
|
|
97
|
+
const entry = catalog.get(alias);
|
|
98
|
+
if (entry) {
|
|
99
|
+
const [group, artifact] = entry.module.split(":");
|
|
100
|
+
dependencies.push({ group, artifact, name: entry.module, version: entry.version, scope: match[1], catalogAlias: alias });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
dependencies.unresolvedDeclarations = declarations.length - dependencies.length;
|
|
104
|
+
return dependencies;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function parseGradleLockPackages(text) {
|
|
108
|
+
const packages = [];
|
|
109
|
+
for (const raw of String(text || "").split(/\r?\n/)) {
|
|
110
|
+
const match = /^([^#=\s:]+(?:\.[^#=\s:]+)*):([^#=\s:]+):([^#=\s=]+)=/.exec(raw.trim());
|
|
111
|
+
if (match) packages.push({ ecosystem: "Maven", name: `${match[1]}:${match[2]}`, version: match[3], dev: false, integrity: "", source: "gradle-lock" });
|
|
112
|
+
}
|
|
113
|
+
return packages;
|
|
114
|
+
}
|
|
@@ -3,12 +3,9 @@
|
|
|
3
3
|
// the surrounding behavior is wrong.
|
|
4
4
|
import { makeFinding } from "./findings.js";
|
|
5
5
|
import {retryFindings} from './source-correctness/retry-patterns.js'
|
|
6
|
+
import {lineNumberAt} from '../util.js'
|
|
6
7
|
|
|
7
|
-
const lineAt =
|
|
8
|
-
let line = 1;
|
|
9
|
-
for (let i = 0; i < index && i < text.length; i++) if (text[i] === "\n") line++;
|
|
10
|
-
return line;
|
|
11
|
-
};
|
|
8
|
+
const lineAt = lineNumberAt;
|
|
12
9
|
|
|
13
10
|
const lineText = (text, index) => {
|
|
14
11
|
const start = text.lastIndexOf("\n", Math.max(0, index - 1)) + 1;
|