weavatrix 0.2.15 → 0.2.18
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 +99 -13
- package/docs/adr/0001-v0.3-offline-online-split.md +58 -0
- package/docs/releases/v0.2.18.md +41 -0
- package/package.json +7 -2
- package/src/analysis/cargo-dependency-evidence.js +105 -0
- package/src/analysis/cargo-manifests.js +74 -0
- package/src/analysis/dep-check-ecosystems.js +3 -0
- package/src/analysis/dependency/scoped-dependencies.js +13 -7
- package/src/analysis/duplicates.tokenize.js +13 -6
- package/src/analysis/endpoints-java.js +2 -5
- package/src/analysis/findings.js +12 -0
- package/src/analysis/git-history/options.js +2 -4
- package/src/analysis/go-dependency-evidence.js +62 -0
- package/src/analysis/health-capabilities.js +1 -1
- package/src/analysis/hot-path-review.js +2 -4
- 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/http-contracts/shared.js +3 -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/repo-files.js +5 -6
- package/src/analysis/internal-audit/supply-chain.js +2 -0
- package/src/analysis/internal-audit.reach.js +14 -0
- package/src/analysis/internal-audit.run.js +15 -10
- package/src/analysis/jvm-dependency-evidence.js +102 -56
- package/src/analysis/jvm-manifests.js +114 -0
- package/src/analysis/manifests.js +0 -49
- package/src/analysis/source-correctness.js +2 -5
- package/src/analysis/structure/dependency-graph.js +3 -8
- 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.langs.js +19 -0
- package/src/graph/internal-builder.resolvers.js +18 -8
- package/src/graph/node-id.js +7 -0
- package/src/mcp/actions/advisories.mjs +2 -3
- package/src/mcp/architecture-bootstrap.mjs +3 -3
- package/src/mcp/catalog.mjs +7 -4
- package/src/mcp/company-contract-verdict.mjs +42 -0
- package/src/mcp/evidence-snapshot.common.mjs +1 -3
- package/src/mcp/evidence-snapshot.health.mjs +11 -4
- package/src/mcp/evidence-snapshot.mjs +1 -1
- package/src/mcp/graph-diff.mjs +2 -1
- package/src/mcp/tools-architecture.mjs +2 -6
- package/src/mcp/tools-company.mjs +28 -53
- package/src/mcp/tools-impact-change.mjs +3 -8
- 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/security/typosquat.js +15 -6
- package/src/util.js +13 -0
- package/docs/releases/v0.2.15.md +0 -37
|
@@ -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
|
+
}
|
|
@@ -99,55 +99,6 @@ export function parsePyprojectDeps(text) {
|
|
|
99
99
|
return { present, deps };
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
// ---- poetry.lock → [{name, version, deps}] — [[package]] blocks + their [package.dependencies] keys ----
|
|
103
|
-
export function parsePoetryLockDeps(text) {
|
|
104
|
-
const out = [];
|
|
105
|
-
let cur = null, inDeps = false;
|
|
106
|
-
for (const raw of String(text || "").split(/\r?\n/)) {
|
|
107
|
-
const line = raw.trim();
|
|
108
|
-
if (line === "[[package]]") { cur = { name: "", version: "", deps: [] }; out.push(cur); inDeps = false; continue; }
|
|
109
|
-
if (line.startsWith("[")) { inDeps = cur != null && line === "[package.dependencies]"; if (line.startsWith("[[")) cur = null; continue; }
|
|
110
|
-
if (!cur) continue;
|
|
111
|
-
if (inDeps) { const m = line.match(/^["']?([A-Za-z0-9][\w.-]*)["']?\s*=/); if (m) cur.deps.push(m[1]); continue; }
|
|
112
|
-
let m = line.match(/^name\s*=\s*"([^"]+)"/); if (m && !cur.name) { cur.name = m[1]; continue; }
|
|
113
|
-
m = line.match(/^version\s*=\s*"([^"]+)"/); if (m && !cur.version) cur.version = m[1];
|
|
114
|
-
}
|
|
115
|
-
return out.filter((p) => p.name);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ---- uv.lock → [{name, version, deps}] — [[package]] blocks with dependencies = [{ name = "x" }, …] ----
|
|
119
|
-
export function parseUvLockDeps(text) {
|
|
120
|
-
const out = [];
|
|
121
|
-
let cur = null, inArr = false;
|
|
122
|
-
for (const raw of String(text || "").split(/\r?\n/)) {
|
|
123
|
-
const line = raw.trim();
|
|
124
|
-
if (line === "[[package]]") { cur = { name: "", version: "", deps: [] }; out.push(cur); inArr = false; continue; }
|
|
125
|
-
if (line.startsWith("[") && !inArr) { if (line.startsWith("[[")) cur = null; continue; }
|
|
126
|
-
if (!cur) continue;
|
|
127
|
-
if (inArr) { for (const m of line.matchAll(/name\s*=\s*"([^"]+)"/g)) cur.deps.push(m[1]); if (/\]\s*$/.test(line)) inArr = false; continue; }
|
|
128
|
-
if (/^dependencies\s*=\s*\[/.test(line)) { for (const m of line.matchAll(/name\s*=\s*"([^"]+)"/g)) cur.deps.push(m[1]); inArr = !/\]\s*$/.test(line); continue; }
|
|
129
|
-
let m = line.match(/^name\s*=\s*"([^"]+)"/); if (m && !cur.name) { cur.name = m[1]; continue; }
|
|
130
|
-
m = line.match(/^version\s*=\s*"([^"]+)"/); if (m && !cur.version) cur.version = m[1];
|
|
131
|
-
}
|
|
132
|
-
return out.filter((p) => p.name);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ---- installed dist-info METADATA → { name, version, deps, repository } (headers until the first blank
|
|
136
|
-
// line; Requires-Dist entries gated behind `extra ==` markers are optional extras — skipped) ----
|
|
137
|
-
export function parseDistMetadata(text) {
|
|
138
|
-
const out = { name: "", version: "", deps: [], repository: "" };
|
|
139
|
-
for (const raw of String(text || "").split(/\r?\n/)) {
|
|
140
|
-
if (raw.trim() === "") break;
|
|
141
|
-
let m = raw.match(/^Name:\s*(.+)$/i); if (m) { out.name = m[1].trim(); continue; }
|
|
142
|
-
m = raw.match(/^Version:\s*(.+)$/i); if (m) { out.version = m[1].trim(); continue; }
|
|
143
|
-
m = raw.match(/^Requires-Dist:\s*([A-Za-z0-9][\w.-]*)(.*)$/i);
|
|
144
|
-
if (m) { if (!/extra\s*==/.test(m[2])) out.deps.push(m[1]); continue; }
|
|
145
|
-
m = raw.match(/^(?:Home-page:\s*|Project-URL:\s*(?:Source|Repository|Homepage|Home|Code)[^,]*,\s*)(https?:\/\/\S+)/i);
|
|
146
|
-
if (m && !out.repository) out.repository = m[1];
|
|
147
|
-
}
|
|
148
|
-
return out;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
102
|
// ---- Pipfile → { present, deps: [{name, dev}] } — [packages] / [dev-packages] table keys ----
|
|
152
103
|
export function parsePipfileDeps(text) {
|
|
153
104
|
const deps = [];
|
|
@@ -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;
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import {ENTRY_FILE} from '../dead-check.js'
|
|
2
|
+
import {fileOfId, graphEndpointId} from '../../graph/node-id.js'
|
|
2
3
|
|
|
3
4
|
const TEST_FILE_RE = /(^|[/])(test|tests|__tests__|spec|e2e|__mocks__)([/]|$)|[._-](test|spec)\.[a-z0-9]+$/i
|
|
4
5
|
const NON_CODE_RE = /\.(json|ya?ml|sh|ps1|md|txt|html?|css|scss|less)$|(^|[/])(dockerfile|containerfile)/i
|
|
5
|
-
const endpoint = (value) => String(value && typeof value === 'object' ? value.id : value)
|
|
6
|
-
const fileOf = (value) => {
|
|
7
|
-
const id = endpoint(value)
|
|
8
|
-
const hash = id.indexOf('#')
|
|
9
|
-
return hash < 0 ? id : id.slice(0, hash)
|
|
10
|
-
}
|
|
11
6
|
const dirOf = (file) => file.includes('/') ? file.slice(0, file.lastIndexOf('/')) : ''
|
|
12
7
|
|
|
13
8
|
export function buildFileImportGraph(graph, {includeTypeOnly = false, includeCompileOnly = false} = {}) {
|
|
@@ -23,7 +18,7 @@ export function buildFileImportGraph(graph, {includeTypeOnly = false, includeCom
|
|
|
23
18
|
}
|
|
24
19
|
for (const link of graph.links || []) {
|
|
25
20
|
if (link.relation !== 'imports' && link.relation !== 're_exports') continue
|
|
26
|
-
const source =
|
|
21
|
+
const source = graphEndpointId(link.source), target = graphEndpointId(link.target)
|
|
27
22
|
if (!fileIds.has(source) || !fileIds.has(target) || source === target) continue
|
|
28
23
|
if (source.endsWith('.go') && target.endsWith('.go') && dirOf(source) === dirOf(target)) continue
|
|
29
24
|
const key = `${source}\0${target}`
|
|
@@ -112,7 +107,7 @@ export function findOrphans(graph, {entrySet = new Set(), externalImportFiles =
|
|
|
112
107
|
const degree = new Map()
|
|
113
108
|
for (const link of graph.links || []) {
|
|
114
109
|
if (link.relation === 'contains') continue
|
|
115
|
-
const source =
|
|
110
|
+
const source = fileOfId(graphEndpointId(link.source)), target = fileOfId(graphEndpointId(link.target))
|
|
116
111
|
if (source === target) continue
|
|
117
112
|
degree.set(source, (degree.get(source) || 0) + 1)
|
|
118
113
|
degree.set(target, (degree.get(target) || 0) + 1)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createRepoBoundary } from "../repo-path.js";
|
|
2
|
+
import { lineNumberAt, uniqueBy } from "../util.js";
|
|
3
|
+
import { listRepoFiles, readRepoText } from "./internal-audit/repo-files.js";
|
|
4
|
+
import { affectedForEndpoint, reverseRuntimeImports } from "./http-contracts/graph-context.js";
|
|
5
|
+
|
|
6
|
+
const TEST_RE = /(^|\/)(?:test|tests|__tests__|spec|e2e|fixtures?)(\/|$)|[._-](?:test|spec)\.[a-z0-9]+$/i;
|
|
7
|
+
const SOURCE_RE = /\.(?:[cm]?[jt]sx?|py|go|java|rs|cs|proto|graphql|gql)$/i;
|
|
8
|
+
const lineAt = lineNumberAt;
|
|
9
|
+
const norm = (value) => String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
10
|
+
|
|
11
|
+
function sourcesFor(descriptor, { includeTests, maxFiles }) {
|
|
12
|
+
const boundary = createRepoBoundary(descriptor.repoRoot);
|
|
13
|
+
if (!boundary.root) return { sources: [], reasons: [`${descriptor.id}: repository boundary is unreadable`] };
|
|
14
|
+
const candidates = listRepoFiles(boundary.root).filter((file) => SOURCE_RE.test(file) && (includeTests || !TEST_RE.test(file))).sort();
|
|
15
|
+
const sources = [];
|
|
16
|
+
for (const file of candidates.slice(0, maxFiles)) {
|
|
17
|
+
const text = readRepoText(boundary, file);
|
|
18
|
+
if (text != null) sources.push({ file, text });
|
|
19
|
+
}
|
|
20
|
+
return { sources, reasons: candidates.length > maxFiles ? [`${descriptor.id}: transport file cap reached`] : [] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function graphQlEvidence(source, role) {
|
|
24
|
+
const contracts = [], uncertain = [];
|
|
25
|
+
for (const schema of source.text.matchAll(/\btype\s+(Query|Mutation|Subscription)\s*(?:extends\s+\w+\s*)?\{([\s\S]*?)\}/gi)) {
|
|
26
|
+
for (const field of schema[2].matchAll(/^\s*([A-Za-z_]\w*)\s*(?:\([^)]*\))?\s*:/gm)) contracts.push({
|
|
27
|
+
transport: "graphql", side: "server", operation: schema[1].toUpperCase(), name: field[1], file: source.file,
|
|
28
|
+
line: lineAt(source.text, schema.index + schema[0].indexOf(field[0])), detector: "graphql-schema",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
for (const operation of source.text.matchAll(/\b(query|mutation|subscription)\s*(?:[A-Za-z_]\w*\s*)?(?:\([^)]*\)\s*)?\{\s*([A-Za-z_]\w*)/gi)) contracts.push({
|
|
32
|
+
transport: "graphql", side: role === "backend" ? "server-call" : "client", operation: operation[1].toUpperCase(), name: operation[2],
|
|
33
|
+
file: source.file, line: lineAt(source.text, operation.index), detector: "graphql-operation",
|
|
34
|
+
});
|
|
35
|
+
for (const dynamic of source.text.matchAll(/\b(?:gql|graphql)\s*(?:\(|`)\s*\$\{|\b(?:query|mutate)\s*\(\s*(?!["'`])/gi)) uncertain.push({
|
|
36
|
+
transport: "graphql", file: source.file, line: lineAt(source.text, dynamic.index), reason: "GraphQL document or operation is runtime-computed",
|
|
37
|
+
});
|
|
38
|
+
return { contracts, uncertain };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function grpcEvidence(source, role) {
|
|
42
|
+
const contracts = [], uncertain = [], aliases = new Map();
|
|
43
|
+
for (const service of source.text.matchAll(/\bservice\s+([A-Za-z_]\w*)\s*\{([\s\S]*?)\}/g)) {
|
|
44
|
+
for (const rpc of service[2].matchAll(/\brpc\s+([A-Za-z_]\w*)\s*\(/g)) contracts.push({
|
|
45
|
+
transport: "grpc", side: "server", service: service[1], name: rpc[1], file: source.file,
|
|
46
|
+
line: lineAt(source.text, service.index + service[0].indexOf(rpc[0])), detector: "proto-service",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const aliasPatterns = [
|
|
50
|
+
/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*new\s+([A-Za-z_]\w*(?:Service)?Client)\b/g,
|
|
51
|
+
/\b([A-Za-z_]\w*)\s*=\s*([A-Za-z_]\w*Stub)\s*\(/g,
|
|
52
|
+
/\b([A-Za-z_]\w*)\s*:?=\s*\w*\.New([A-Za-z_]\w*)Client\s*\(/g,
|
|
53
|
+
/\b(?:[A-Za-z_]\w*\.)*([A-Za-z_]\w*(?:Blocking|Future|Async)?Stub)\s+([A-Za-z_]\w*)\s*[=;]/g,
|
|
54
|
+
];
|
|
55
|
+
for (const [index, pattern] of aliasPatterns.entries()) for (const match of source.text.matchAll(pattern)) {
|
|
56
|
+
const alias = index === 3 ? match[2] : match[1];
|
|
57
|
+
const type = index === 3 ? match[1] : match[2];
|
|
58
|
+
aliases.set(alias, type.replace(/(?:Service)?Client$|(?:Blocking|Future|Async)?Stub$/i, ""));
|
|
59
|
+
}
|
|
60
|
+
for (const call of source.text.matchAll(/\b([A-Za-z_$][\w$]*)\s*(?:\.|->)\s*([A-Za-z_]\w*)\s*\(/g)) {
|
|
61
|
+
if (!aliases.has(call[1])) continue;
|
|
62
|
+
contracts.push({ transport: "grpc", side: role === "backend" ? "server-call" : "client", service: aliases.get(call[1]), name: call[2], file: source.file, line: lineAt(source.text, call.index), detector: "grpc-stub-call" });
|
|
63
|
+
}
|
|
64
|
+
for (const dynamic of source.text.matchAll(/\b(?:ServerReflection|ProtoReflection|grpc\.reflection|Class\.forName|Method\.invoke|reflect\.Value|dynamicStub)\b/g)) uncertain.push({
|
|
65
|
+
transport: "grpc", file: source.file, line: lineAt(source.text, dynamic.index), reason: "gRPC/reflection target is runtime-resolved",
|
|
66
|
+
});
|
|
67
|
+
return { contracts, uncertain };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function eventEvidence(source, role) {
|
|
71
|
+
const contracts = [], uncertain = [];
|
|
72
|
+
const add = (side, kind, match, topicIndex = 2) => contracts.push({
|
|
73
|
+
transport: "event", side, kind, name: match[topicIndex], file: source.file, line: lineAt(source.text, match.index), detector: "static-topic",
|
|
74
|
+
});
|
|
75
|
+
const subscriptions = [
|
|
76
|
+
/\b(?:consumer|kafka|bus|events?|eventBus)\s*(?:\.|->)\s*(subscribe|on|consume)\s*\(\s*["'`]([^"'`]+)["'`]/gi,
|
|
77
|
+
/@KafkaListener\s*\([^)]*\btopics?\s*=\s*["']([^"']+)["']/gi,
|
|
78
|
+
/\bsubscribe\s*\(\s*\{\s*topic\s*:\s*["'`]([^"'`]+)["'`]/gi,
|
|
79
|
+
];
|
|
80
|
+
for (const [index, pattern] of subscriptions.entries()) for (const match of source.text.matchAll(pattern)) add("subscriber", index === 0 ? match[1] : "subscribe", match, index === 0 ? 2 : 1);
|
|
81
|
+
const publications = [
|
|
82
|
+
/\b(?:producer|kafka|bus|events?|eventBus|kafkaTemplate)\s*(?:\.|->)\s*(publish|emit|send|produce)\s*\(\s*["'`]([^"'`]+)["'`]/gi,
|
|
83
|
+
/\b(?:producer|kafka)\s*(?:\.|->)\s*send\s*\(\s*\{\s*topic\s*:\s*["'`]([^"'`]+)["'`]/gi,
|
|
84
|
+
];
|
|
85
|
+
for (const [index, pattern] of publications.entries()) for (const match of source.text.matchAll(pattern)) add("publisher", index === 0 ? match[1] : "send", match, index === 0 ? 2 : 1);
|
|
86
|
+
for (const dynamic of source.text.matchAll(/\b(?:subscribe|publish|emit|consume|produce)\s*\(\s*(?!["'`{])([A-Za-z_$][\w$]*)/gi)) uncertain.push({
|
|
87
|
+
transport: "event", file: source.file, line: lineAt(source.text, dynamic.index), reason: `Event topic is runtime-computed (${dynamic[1]})`, role,
|
|
88
|
+
});
|
|
89
|
+
return { contracts, uncertain };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function detect(descriptor, role, options) {
|
|
93
|
+
const loaded = sourcesFor(descriptor, options), contracts = [], uncertain = [];
|
|
94
|
+
for (const source of loaded.sources) {
|
|
95
|
+
for (const detector of [graphQlEvidence, grpcEvidence, eventEvidence]) {
|
|
96
|
+
const result = detector(source, role);
|
|
97
|
+
contracts.push(...result.contracts); uncertain.push(...result.uncertain);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
contracts: uniqueBy(contracts, (item) => `${item.transport}|${item.side}|${item.service || ""}|${item.operation || ""}|${item.name}|${item.file}|${item.line}`),
|
|
102
|
+
uncertain: uniqueBy(uncertain, (item) => `${item.transport}|${item.file}|${item.line}|${item.reason}`),
|
|
103
|
+
reasons: loaded.reasons,
|
|
104
|
+
filesScanned: loaded.sources.length,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function contractMatch(server, caller, backendServers) {
|
|
109
|
+
if (server.transport !== caller.transport) return null;
|
|
110
|
+
if (server.transport === "graphql" && caller.side === "client" && server.operation === caller.operation && server.name === caller.name) return { confidence: "high", kind: "operation-field" };
|
|
111
|
+
if (server.transport === "grpc" && caller.side === "client" && norm(server.name) === norm(caller.name)) {
|
|
112
|
+
if (caller.service && norm(server.service) === norm(caller.service)) return { confidence: "high", kind: "service-method" };
|
|
113
|
+
const sameMethod = backendServers.filter((item) => item.transport === "grpc" && norm(item.name) === norm(caller.name));
|
|
114
|
+
if (!caller.service && sameMethod.length === 1) return { confidence: "medium", kind: "unique-method" };
|
|
115
|
+
}
|
|
116
|
+
if (server.transport === "event" && server.name === caller.name && server.side !== caller.side) return { confidence: "high", kind: "topic-direction" };
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function analyzeTransportContracts(input = {}) {
|
|
121
|
+
const transport = ["all", "graphql", "grpc", "event"].includes(input.transport) ? input.transport : "all";
|
|
122
|
+
const options = { includeTests: input.includeTests === true, maxFiles: Math.max(1, Math.min(10_000, Number(input.maxFiles) || 3_000)) };
|
|
123
|
+
const backend = detect(input.backend, "backend", options);
|
|
124
|
+
const clients = (input.clients || []).map((descriptor) => ({ descriptor, evidence: detect(descriptor, "client", options), reverse: reverseRuntimeImports(descriptor.graph) }));
|
|
125
|
+
const selected = (item) => transport === "all" || item.transport === transport;
|
|
126
|
+
const backendContracts = backend.contracts.filter(selected);
|
|
127
|
+
const servers = backendContracts.filter((item) => item.side === "server" || item.transport === "event");
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const server of servers) {
|
|
130
|
+
const callsites = [];
|
|
131
|
+
for (const client of clients) for (const caller of client.evidence.contracts.filter(selected)) {
|
|
132
|
+
const match = contractMatch(server, caller, servers);
|
|
133
|
+
if (!match) continue;
|
|
134
|
+
callsites.push({ clientRepo: client.descriptor.id, file: caller.file, line: caller.line, detector: caller.detector, match });
|
|
135
|
+
}
|
|
136
|
+
const affected = affectedForEndpoint(callsites, clients.map((item) => ({ id: item.descriptor.id, reverse: item.reverse })), {
|
|
137
|
+
maxImpactDepth: Math.max(0, Math.min(5, Number(input.maxImpactDepth) || 2)),
|
|
138
|
+
maxAffectedFiles: Math.max(1, Math.min(500, Number(input.maxAffectedFiles) || 100)), maxScreens: 50, maxModules: 50,
|
|
139
|
+
});
|
|
140
|
+
results.push({ ...server, callsites, affected, liveness: callsites.length ? "NOT_DEAD_EXTERNAL_USE" : "UNKNOWN" });
|
|
141
|
+
}
|
|
142
|
+
const uncertain = [
|
|
143
|
+
...backend.uncertain.map((item) => ({ repository: input.backend.id, ...item })),
|
|
144
|
+
...clients.flatMap((item) => item.evidence.uncertain.map((entry) => ({ repository: item.descriptor.id, ...entry }))),
|
|
145
|
+
].filter(selected);
|
|
146
|
+
const reasons = [...backend.reasons, ...clients.flatMap((item) => item.evidence.reasons)];
|
|
147
|
+
if (uncertain.length) reasons.push(`${uncertain.length} dynamic/reflection contract expression(s) remain UNKNOWN`);
|
|
148
|
+
return {
|
|
149
|
+
transportContractsV: 1,
|
|
150
|
+
transport,
|
|
151
|
+
status: reasons.length ? "PARTIAL" : "COMPLETE",
|
|
152
|
+
completeness: { complete: reasons.length === 0, reasons: [...new Set(reasons)] },
|
|
153
|
+
totals: { contracts: results.length, matches: results.reduce((sum, item) => sum + item.callsites.length, 0), uncertain: uncertain.length, filesScanned: backend.filesScanned + clients.reduce((sum, item) => sum + item.evidence.filesScanned, 0) },
|
|
154
|
+
contracts: results,
|
|
155
|
+
uncertain: uncertain.slice(0, 200),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -72,7 +72,9 @@ export default {
|
|
|
72
72
|
heritage: [],
|
|
73
73
|
|
|
74
74
|
pass1(ctx) {
|
|
75
|
-
const { grammar, tree, fileRel, caps, addSym, addImportEdge, addExternalImport, imports, resolveGoImport, dirFiles, goModule, goRequires } = ctx;
|
|
75
|
+
const { grammar, tree, fileRel, caps, addSym, addImportEdge, addExternalImport, imports, resolveGoImport, dirFiles, goModule, goModules, goRequires } = ctx;
|
|
76
|
+
const owningModule = (goModules || []).filter((item) => !item.root || fileRel === item.root || fileRel.startsWith(`${item.root}/`))
|
|
77
|
+
.sort((left, right) => right.root.length - left.root.length)[0];
|
|
76
78
|
|
|
77
79
|
// ---- symbols ----
|
|
78
80
|
for (const cap of caps(grammar, `(function_declaration name: (identifier) @fn)`, tree.rootNode)) {
|
|
@@ -124,7 +126,7 @@ export default {
|
|
|
124
126
|
if (!d) {
|
|
125
127
|
// not a repo package: stdlib (builtin) or an external module — record for dependency analysis
|
|
126
128
|
const line = pathNode.startPosition.row + 1;
|
|
127
|
-
const r = goSpecToPkg(importPath, { requires: goRequires || [], ownModule: goModule || "" });
|
|
129
|
+
const r = goSpecToPkg(importPath, { requires: owningModule?.requires || goRequires || [], ownModule: owningModule?.module || goModule || "" });
|
|
128
130
|
if (r) addExternalImport({ spec: importPath, pkg: r.pkg, builtin: r.builtin, ecosystem: "Go", kind: "go-import", line });
|
|
129
131
|
else addExternalImport({ spec: importPath, kind: "go-import", line, unresolved: true }); // own-module path with no matching dir
|
|
130
132
|
continue;
|
|
@@ -83,7 +83,7 @@ export default {
|
|
|
83
83
|
pass1(ctx) {
|
|
84
84
|
const {
|
|
85
85
|
grammar, tree, fileRel, caps, field, addSym, addImportEdge, imports,
|
|
86
|
-
resolveJavaImport, fileSet, links, nodeIds,
|
|
86
|
+
resolveJavaImport, fileSet, links, nodeIds, addExternalImport,
|
|
87
87
|
} = ctx;
|
|
88
88
|
const ownerIds = new Map();
|
|
89
89
|
const addJavaSym = (nameNode, callable, extra) => {
|
|
@@ -143,11 +143,17 @@ export default {
|
|
|
143
143
|
const line = lineOf(cap.node);
|
|
144
144
|
if (!isStatic && wildcard) {
|
|
145
145
|
wildcardPackages.push(parts);
|
|
146
|
+
const packagePath = `${parts.join("/")}/`;
|
|
147
|
+
const projectLocal = [...fileSet].some((file) => file.startsWith(packagePath) || file.includes(`/${packagePath}`));
|
|
148
|
+
if (!projectLocal) addExternalImport({ spec: `${parts.join(".")}.*`, pkg: parts.join("."), builtin: ["java", "jdk", "sun"].includes(parts[0]), ecosystem: "Maven", kind: "java-import", line });
|
|
146
149
|
continue;
|
|
147
150
|
}
|
|
148
151
|
if (!isStatic) {
|
|
149
152
|
const target = exactJavaTarget(resolveJavaImport, parts);
|
|
150
|
-
if (!target)
|
|
153
|
+
if (!target) {
|
|
154
|
+
addExternalImport({ spec: parts.join("."), pkg: parts.join("."), builtin: ["java", "jdk", "sun"].includes(parts[0]), ecosystem: "Maven", kind: "java-import", line });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
151
157
|
const local = parts[parts.length - 1];
|
|
152
158
|
addImportEdge(target, { line, specifier: parts.join("."), compileOnly: true });
|
|
153
159
|
imports.set(local, { imported: local, targetFile: target });
|
|
@@ -162,7 +168,10 @@ export default {
|
|
|
162
168
|
target = exactJavaTarget(resolveJavaImport, candidate);
|
|
163
169
|
if (target) classParts = candidate;
|
|
164
170
|
}
|
|
165
|
-
if (!target)
|
|
171
|
+
if (!target) {
|
|
172
|
+
addExternalImport({ spec: parts.join("."), pkg: parts.join("."), builtin: ["java", "jdk", "sun"].includes(parts[0]), ecosystem: "Maven", kind: "java-static-import", line });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
166
175
|
addImportEdge(target, { line, specifier: `${isStatic ? "static " : ""}${parts.join(".")}${wildcard ? ".*" : ""}`, compileOnly: true });
|
|
167
176
|
if (wildcard) staticWildcardTargets.push(target);
|
|
168
177
|
else {
|