weavatrix 0.3.14 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +231 -199
- package/bin/native/darwin-arm64/weavatrix +0 -0
- package/bin/native/darwin-x64/weavatrix +0 -0
- package/bin/native/linux-arm64/weavatrix +0 -0
- package/bin/native/linux-x64/weavatrix +0 -0
- package/bin/native/win32-arm64/weavatrix.exe +0 -0
- package/bin/native/win32-x64/weavatrix.exe +0 -0
- package/bin/resolve-binary.mjs +73 -0
- package/bin/run-native.mjs +26 -0
- package/bin/weavatrix-mcp.mjs +7 -6
- package/bin/weavatrix.mjs +5 -0
- package/package.json +8 -66
- package/skill/SKILL.md +62 -374
- package/skill/agents/openai.yaml +4 -0
- package/skill/references/tool-routing.md +45 -0
- package/SECURITY.md +0 -101
- package/docs/adr/0001-v0.3-offline-online-split.md +0 -60
- package/docs/agent-task-benchmark.md +0 -58
- package/docs/releases/v0.2.19.md +0 -33
- package/docs/releases/v0.3.0.md +0 -43
- package/docs/releases/v0.3.1.md +0 -30
- package/docs/releases/v0.3.10.md +0 -76
- package/docs/releases/v0.3.11.md +0 -55
- package/docs/releases/v0.3.12.md +0 -44
- package/docs/releases/v0.3.13.md +0 -34
- package/docs/releases/v0.3.14.md +0 -37
- package/docs/releases/v0.3.2.md +0 -15
- package/docs/releases/v0.3.3.md +0 -25
- package/docs/releases/v0.3.4.md +0 -40
- package/docs/releases/v0.3.5.md +0 -50
- package/docs/releases/v0.3.6.md +0 -44
- package/docs/releases/v0.3.7.md +0 -37
- package/docs/releases/v0.3.8.md +0 -21
- package/docs/releases/v0.3.9.md +0 -36
- package/scripts/run-agent-task-benchmark.mjs +0 -118
- package/src/analysis/allowed-test-runner.js +0 -70
- package/src/analysis/architecture/contract-graph.js +0 -119
- package/src/analysis/architecture/contract-schema.js +0 -110
- package/src/analysis/architecture/contract-storage.js +0 -35
- package/src/analysis/architecture/contract-verification.js +0 -168
- package/src/analysis/architecture-contract.js +0 -16
- package/src/analysis/audit-debt.js +0 -94
- package/src/analysis/audit-extensions.js +0 -60
- package/src/analysis/cargo-dependency-evidence.js +0 -108
- package/src/analysis/cargo-manifests.js +0 -81
- package/src/analysis/change-classification/diff-parser.js +0 -103
- package/src/analysis/change-classification/options.js +0 -40
- package/src/analysis/change-classification/symbol-classifier.js +0 -177
- package/src/analysis/change-classification.js +0 -188
- package/src/analysis/coverage-reports-tarpaulin.js +0 -112
- package/src/analysis/coverage-reports.js +0 -246
- package/src/analysis/cycle-route.js +0 -14
- package/src/analysis/data-flow-evidence.js +0 -114
- package/src/analysis/dead-check.js +0 -300
- package/src/analysis/dead-code-review/candidates.js +0 -194
- package/src/analysis/dead-code-review/policy.js +0 -23
- package/src/analysis/dead-code-review.js +0 -119
- package/src/analysis/dep-check-ecosystems.js +0 -205
- package/src/analysis/dep-check.js +0 -290
- package/src/analysis/dep-rules.js +0 -10
- package/src/analysis/dependency/conventions.js +0 -16
- package/src/analysis/dependency/scoped-dependencies.js +0 -51
- package/src/analysis/dependency/source-references.js +0 -21
- package/src/analysis/duplicate-groups.js +0 -104
- package/src/analysis/duplicates-worker.js +0 -11
- package/src/analysis/duplicates.compute.js +0 -269
- package/src/analysis/duplicates.js +0 -15
- package/src/analysis/duplicates.run.js +0 -52
- package/src/analysis/duplicates.tokenize.js +0 -199
- package/src/analysis/endpoints/common.js +0 -79
- package/src/analysis/endpoints/extract.js +0 -120
- package/src/analysis/endpoints/inventory.js +0 -84
- package/src/analysis/endpoints/mounts.js +0 -129
- package/src/analysis/endpoints-java.js +0 -260
- package/src/analysis/endpoints-rust.js +0 -124
- package/src/analysis/endpoints.js +0 -3
- package/src/analysis/findings.js +0 -70
- package/src/analysis/git-history/analytics.js +0 -177
- package/src/analysis/git-history/collector.js +0 -67
- package/src/analysis/git-history/options.js +0 -66
- package/src/analysis/git-history.js +0 -143
- package/src/analysis/git-ref-graph.js +0 -76
- package/src/analysis/go-dependency-evidence.js +0 -62
- package/src/analysis/graph-analysis.aggregate.js +0 -296
- package/src/analysis/graph-analysis.edges.js +0 -24
- package/src/analysis/graph-analysis.js +0 -7
- package/src/analysis/graph-analysis.refs.js +0 -202
- package/src/analysis/graph-analysis.summaries.js +0 -65
- package/src/analysis/health-capabilities.js +0 -83
- package/src/analysis/hot-path-review.js +0 -266
- package/src/analysis/http-contract-wrappers.js +0 -241
- package/src/analysis/http-contracts/analysis.js +0 -179
- package/src/analysis/http-contracts/client-call-detection.js +0 -82
- package/src/analysis/http-contracts/client-call-parser.js +0 -273
- package/src/analysis/http-contracts/graph-context.js +0 -143
- package/src/analysis/http-contracts/matching.js +0 -61
- package/src/analysis/http-contracts/shared.js +0 -85
- package/src/analysis/http-contracts.js +0 -6
- package/src/analysis/internal-audit/dependency-health.js +0 -113
- package/src/analysis/internal-audit/python-manifests.js +0 -78
- package/src/analysis/internal-audit/repo-files.js +0 -50
- package/src/analysis/internal-audit/supply-chain.js +0 -149
- package/src/analysis/internal-audit.collect.js +0 -213
- package/src/analysis/internal-audit.js +0 -9
- package/src/analysis/internal-audit.reach.js +0 -201
- package/src/analysis/internal-audit.run.js +0 -256
- package/src/analysis/java-source.js +0 -36
- package/src/analysis/jvm-artifact-index.js +0 -87
- package/src/analysis/jvm-dependency-evidence.js +0 -138
- package/src/analysis/jvm-manifests.js +0 -114
- package/src/analysis/manifests.js +0 -123
- package/src/analysis/package-reachability.js +0 -39
- package/src/analysis/source-complexity.ast.js +0 -97
- package/src/analysis/source-complexity.constants.js +0 -55
- package/src/analysis/source-complexity.js +0 -14
- package/src/analysis/source-complexity.report.js +0 -81
- package/src/analysis/source-complexity.walk.js +0 -228
- package/src/analysis/source-correctness/retry-patterns.js +0 -93
- package/src/analysis/source-correctness.js +0 -222
- package/src/analysis/static-test-reachability.js +0 -150
- package/src/analysis/structure/dependency-graph.js +0 -151
- package/src/analysis/structure/findings.js +0 -162
- package/src/analysis/task-retrieval.js +0 -107
- package/src/analysis/transport-contracts.js +0 -224
- package/src/analysis/transport-runtime-evidence.js +0 -155
- package/src/analysis-kit.mjs +0 -17
- package/src/bounds.js +0 -4
- package/src/build-graph.js +0 -209
- package/src/child-env.js +0 -7
- package/src/config.js +0 -5
- package/src/extension/local-services.mjs +0 -75
- package/src/git-exec.js +0 -67
- package/src/graph/build-worker.js +0 -49
- package/src/graph/builder/go-receiver-resolution.js +0 -112
- package/src/graph/builder/js/dynamic-import.js +0 -26
- package/src/graph/builder/js/queries.js +0 -48
- package/src/graph/builder/lang-csharp.js +0 -40
- package/src/graph/builder/lang-css.js +0 -29
- package/src/graph/builder/lang-go.js +0 -140
- package/src/graph/builder/lang-html.js +0 -29
- package/src/graph/builder/lang-java-calls.js +0 -142
- package/src/graph/builder/lang-java.js +0 -215
- package/src/graph/builder/lang-js.js +0 -291
- package/src/graph/builder/lang-python.js +0 -235
- package/src/graph/builder/lang-rust-calls.js +0 -63
- package/src/graph/builder/lang-rust.js +0 -299
- package/src/graph/builder/lang-solidity.js +0 -126
- package/src/graph/builder/lang-sql.js +0 -263
- package/src/graph/builder/pass2-resolution.js +0 -81
- package/src/graph/builder/spec-pkg.js +0 -87
- package/src/graph/community.js +0 -27
- package/src/graph/edge-provenance.js +0 -31
- package/src/graph/file-lock.js +0 -69
- package/src/graph/freshness-probe.js +0 -142
- package/src/graph/graph-filter.js +0 -63
- package/src/graph/incremental-refresh.js +0 -235
- package/src/graph/internal-builder.barrels.js +0 -235
- package/src/graph/internal-builder.build.js +0 -300
- package/src/graph/internal-builder.java.js +0 -33
- package/src/graph/internal-builder.js +0 -12
- package/src/graph/internal-builder.langs.js +0 -157
- package/src/graph/internal-builder.pass2.js +0 -163
- package/src/graph/internal-builder.resolvers.js +0 -286
- package/src/graph/layout.js +0 -51
- package/src/graph/node-id.js +0 -7
- package/src/graph/parser-artifact-boundary.js +0 -55
- package/src/graph/relations.js +0 -4
- package/src/graph/repo-registry.js +0 -124
- package/src/graph/resolvers/rust.js +0 -117
- package/src/infra/infra-items.js +0 -255
- package/src/infra/infra-registry.js +0 -184
- package/src/infra/infra.detect.js +0 -119
- package/src/infra/infra.js +0 -38
- package/src/infra/infra.match.js +0 -102
- package/src/infra/infra.scan.js +0 -93
- package/src/mcp/actions/graph-lifecycle.mjs +0 -148
- package/src/mcp/architecture-bootstrap.mjs +0 -168
- package/src/mcp/architecture-starter.mjs +0 -234
- package/src/mcp/catalog.mjs +0 -233
- package/src/mcp/company-contract-page.mjs +0 -142
- package/src/mcp/company-contract-reconcile.mjs +0 -87
- package/src/mcp/company-contract-verdict.mjs +0 -42
- package/src/mcp/evidence/bun-lock-graph.mjs +0 -209
- package/src/mcp/evidence/duplicate-member-order.mjs +0 -8
- package/src/mcp/evidence/package-graph-common.mjs +0 -115
- package/src/mcp/evidence/package-lock-graph.mjs +0 -92
- package/src/mcp/evidence-snapshot.architecture.mjs +0 -80
- package/src/mcp/evidence-snapshot.common.mjs +0 -158
- package/src/mcp/evidence-snapshot.duplicates.mjs +0 -213
- package/src/mcp/evidence-snapshot.health.mjs +0 -102
- package/src/mcp/evidence-snapshot.inventory.mjs +0 -148
- package/src/mcp/evidence-snapshot.mjs +0 -50
- package/src/mcp/evidence-snapshot.package-graph.mjs +0 -34
- package/src/mcp/evidence-snapshot.structure.mjs +0 -84
- package/src/mcp/extension-api.mjs +0 -77
- package/src/mcp/git-output.mjs +0 -7
- package/src/mcp/graph/context-core.mjs +0 -111
- package/src/mcp/graph/context-seeds.mjs +0 -207
- package/src/mcp/graph/context-state.mjs +0 -85
- package/src/mcp/graph/reverse-reach.mjs +0 -42
- package/src/mcp/graph/tools-core.mjs +0 -143
- package/src/mcp/graph/tools-query.mjs +0 -276
- package/src/mcp/graph-context.mjs +0 -5
- package/src/mcp/graph-diff.mjs +0 -218
- package/src/mcp/health/audit-format.mjs +0 -199
- package/src/mcp/health/audit.mjs +0 -199
- package/src/mcp/health/dead-code.mjs +0 -110
- package/src/mcp/health/duplicates.mjs +0 -121
- package/src/mcp/health/endpoints.mjs +0 -71
- package/src/mcp/health/structure.mjs +0 -220
- package/src/mcp/runtime-version.mjs +0 -31
- package/src/mcp/server/auto-refresh.mjs +0 -66
- package/src/mcp/server/runtime-config.mjs +0 -43
- package/src/mcp/server/shutdown.mjs +0 -40
- package/src/mcp/staleness-notice.mjs +0 -20
- package/src/mcp/sync/evidence-architecture.mjs +0 -54
- package/src/mcp/sync/evidence-common.mjs +0 -88
- package/src/mcp/sync/evidence-duplicates.mjs +0 -97
- package/src/mcp/sync/evidence-health.mjs +0 -56
- package/src/mcp/sync/evidence-packages.mjs +0 -95
- package/src/mcp/sync/payload-common.mjs +0 -97
- package/src/mcp/sync/payload-v2.mjs +0 -54
- package/src/mcp/sync/payload-v3.mjs +0 -79
- package/src/mcp/sync-evidence.mjs +0 -23
- package/src/mcp/sync-payload.mjs +0 -70
- package/src/mcp/tool-result.mjs +0 -54
- package/src/mcp/tools-actions.mjs +0 -11
- package/src/mcp/tools-architecture.mjs +0 -177
- package/src/mcp/tools-company.mjs +0 -251
- package/src/mcp/tools-context.mjs +0 -222
- package/src/mcp/tools-endpoints.mjs +0 -207
- package/src/mcp/tools-graph-hubs.mjs +0 -78
- package/src/mcp/tools-graph.mjs +0 -17
- package/src/mcp/tools-health.mjs +0 -24
- package/src/mcp/tools-history.mjs +0 -28
- package/src/mcp/tools-impact-change.mjs +0 -220
- package/src/mcp/tools-impact-precision.mjs +0 -89
- package/src/mcp/tools-impact.mjs +0 -169
- package/src/mcp/tools-source.mjs +0 -238
- package/src/mcp/tools-verified-change.mjs +0 -200
- package/src/mcp-rg.mjs +0 -50
- package/src/mcp-runtime.mjs +0 -2
- package/src/mcp-server.mjs +0 -298
- package/src/mcp-source-tools.mjs +0 -163
- package/src/path-classification.js +0 -226
- package/src/path-ignore.js +0 -69
- package/src/precision/lsp-client/constants.js +0 -12
- package/src/precision/lsp-client/environment.js +0 -20
- package/src/precision/lsp-client/errors.js +0 -15
- package/src/precision/lsp-client/lifecycle.js +0 -114
- package/src/precision/lsp-client/message-parser.js +0 -81
- package/src/precision/lsp-client/protocol.js +0 -212
- package/src/precision/lsp-client/registry.js +0 -59
- package/src/precision/lsp-client/repo-uri.js +0 -60
- package/src/precision/lsp-client/stdio-client.js +0 -140
- package/src/precision/lsp-client.js +0 -10
- package/src/precision/lsp-overlay/budget-policy.js +0 -78
- package/src/precision/lsp-overlay/build.js +0 -261
- package/src/precision/lsp-overlay/contract.js +0 -61
- package/src/precision/lsp-overlay/reference-results.js +0 -108
- package/src/precision/lsp-overlay/semantic-inputs.js +0 -62
- package/src/precision/lsp-overlay/source-session.js +0 -142
- package/src/precision/lsp-overlay/store.js +0 -150
- package/src/precision/lsp-overlay/target-index.js +0 -152
- package/src/precision/lsp-overlay/target-query.js +0 -55
- package/src/precision/lsp-overlay.js +0 -11
- package/src/precision/symbol-query.js +0 -224
- package/src/precision/typescript-lsp-provider.js +0 -12
- package/src/precision/typescript-provider/client.js +0 -89
- package/src/precision/typescript-provider/discovery.js +0 -124
- package/src/precision/typescript-provider/isolated-runtime.js +0 -39
- package/src/precision/typescript-provider/project-host.js +0 -254
- package/src/precision/typescript-provider/project-safety.js +0 -166
- package/src/precision/typescript-provider/reference-usage.js +0 -53
- package/src/process.js +0 -95
- package/src/repo-path.js +0 -53
- package/src/scan/discover.inventory.js +0 -78
- package/src/scan/discover.js +0 -5
- package/src/scan/discover.list.js +0 -79
- package/src/scan/discover.stack.js +0 -227
- package/src/scan/search.core.js +0 -24
- package/src/scan/search.git.js +0 -102
- package/src/scan/search.js +0 -9
- package/src/scan/search.node.js +0 -83
- package/src/scan/search.preview.js +0 -49
- package/src/scan/search.rg.js +0 -145
- package/src/security/advisory-store.js +0 -172
- package/src/security/installed-jvm-rust.js +0 -46
- package/src/security/installed.js +0 -280
- package/src/security/malware-file-heuristics.js +0 -121
- package/src/security/malware-heuristics.exclusions.js +0 -134
- package/src/security/malware-heuristics.js +0 -15
- package/src/security/malware-heuristics.roots.js +0 -145
- package/src/security/malware-heuristics.scan.js +0 -89
- package/src/security/malware-heuristics.sweep.js +0 -122
- package/src/security/malware-scoring.js +0 -105
- package/src/security/match.js +0 -85
- package/src/security/registry-sig.classify.js +0 -137
- package/src/security/registry-sig.js +0 -18
- package/src/security/registry-sig.rules.js +0 -192
- package/src/security/rust-advisory-report.js +0 -60
- package/src/security/typosquat.js +0 -81
- package/src/util.js +0 -40
- package/src/version.js +0 -7
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import {makeFinding} from '../findings.js'
|
|
2
|
-
|
|
3
|
-
const RETRY_SIGNAL = /\b(?:retry|retries|retrying|reconnect|backoff|attempts?)\b/i
|
|
4
|
-
const WAIT_SIGNAL = /\b(?:sleep|delay)\s*\(/i
|
|
5
|
-
const FAILURE_SIGNAL = /\b(?:catch|isError|error|exception|failed?|healthCheck)\b/i
|
|
6
|
-
const TERMINATION_POLICY = /\b(?:max(?:imum)?(?:Retries|Attempts)|retryLimit|attemptLimit|deadline|timeout|isInterrupted)\b|\bcontext\s*\.\s*Done\b|\bsignal\s*\.\s*aborted\b|\battempts?\s*(?:>=|>)\s*\w+|\bbreak\s*;/i
|
|
7
|
-
|
|
8
|
-
const lineAt = (text, index) => {
|
|
9
|
-
let line = 1
|
|
10
|
-
for (let i = 0; i < index && i < text.length; i++) if (text[i] === '\n') line++
|
|
11
|
-
return line
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const lineText = (text, index) => {
|
|
15
|
-
const start = text.lastIndexOf('\n', Math.max(0, index - 1)) + 1
|
|
16
|
-
const end = text.indexOf('\n', index)
|
|
17
|
-
return text.slice(start, end < 0 ? text.length : end).trim().slice(0, 300)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function balancedEnd(text, openAt) {
|
|
21
|
-
if (text[openAt] !== '{') return -1
|
|
22
|
-
let depth = 0
|
|
23
|
-
for (let i = openAt; i < text.length; i++) {
|
|
24
|
-
if (text[i] === '{') depth++
|
|
25
|
-
else if (text[i] === '}' && --depth === 0) return i + 1
|
|
26
|
-
}
|
|
27
|
-
return -1
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function retryFinding(text, file, index) {
|
|
31
|
-
return makeFinding({
|
|
32
|
-
category: 'structure',
|
|
33
|
-
rule: 'unbounded-retry-loop',
|
|
34
|
-
severity: 'low',
|
|
35
|
-
confidence: 'medium',
|
|
36
|
-
title: 'Retry/poll loop has no visible attempt, deadline, or cancellation bound',
|
|
37
|
-
detail: 'A repeating loop contains retry/failure plus wait behavior but no local maximum-attempt, deadline, cancellation, interrupt, or explicit break policy was recognized. This is a review signal, not proof of an infinite loop; success or a called helper may terminate it.',
|
|
38
|
-
file,
|
|
39
|
-
line: lineAt(text, index),
|
|
40
|
-
evidence: [{file, line: lineAt(text, index), snippet: lineText(text, index)}],
|
|
41
|
-
fixHint: 'confirm and document the retry termination/cancellation policy',
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function retryBehavior(source) {
|
|
46
|
-
return RETRY_SIGNAL.test(source) || (WAIT_SIGNAL.test(source) && FAILURE_SIGNAL.test(source))
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function bracedRetryFindings(text, masked, file) {
|
|
50
|
-
const findings = []
|
|
51
|
-
const re = /\bwhile\s*\(([^)]*)\)\s*\{|\bfor\s*\(\s*;\s*;\s*\)\s*\{|\bfor\s*\{|\bdo\s*\{/g
|
|
52
|
-
let match
|
|
53
|
-
while ((match = re.exec(masked))) {
|
|
54
|
-
const open = re.lastIndex - 1
|
|
55
|
-
const end = balancedEnd(masked, open)
|
|
56
|
-
if (end < 0) continue
|
|
57
|
-
const body = masked.slice(open + 1, end - 1)
|
|
58
|
-
const context = masked.slice(Math.max(0, match.index - 250), match.index)
|
|
59
|
-
let condition = match[1] || '', tailLength = 0
|
|
60
|
-
if (/^do\b/.test(match[0])) {
|
|
61
|
-
const tail = /^\s*while\s*\(([^)]*)\)\s*;/.exec(masked.slice(end))
|
|
62
|
-
condition = tail?.[1] || ''
|
|
63
|
-
tailLength = tail?.[0]?.length || 0
|
|
64
|
-
}
|
|
65
|
-
const evidence = `${context}\n${condition}\n${body}`
|
|
66
|
-
if (retryBehavior(evidence) && !TERMINATION_POLICY.test(`${condition}\n${body}`)) {
|
|
67
|
-
findings.push(retryFinding(text, file, match.index))
|
|
68
|
-
}
|
|
69
|
-
re.lastIndex = end + tailLength
|
|
70
|
-
}
|
|
71
|
-
return findings
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function pythonRetryFindings(text, masked, file) {
|
|
75
|
-
const findings = [], lines = masked.split(/\r?\n/)
|
|
76
|
-
for (let i = 0; i < lines.length; i++) {
|
|
77
|
-
const match = /^(\s*)while\s+True\s*:/.exec(lines[i])
|
|
78
|
-
if (!match) continue
|
|
79
|
-
const indent = match[1].length
|
|
80
|
-
let end = i + 1
|
|
81
|
-
while (end < lines.length && (!lines[end].trim() || /^\s*/.exec(lines[end])[0].length > indent)) end++
|
|
82
|
-
const body = lines.slice(i + 1, end).join('\n')
|
|
83
|
-
const context = lines.slice(Math.max(0, i - 8), i).join('\n')
|
|
84
|
-
if (retryBehavior(`${context}\n${body}`) && !TERMINATION_POLICY.test(body)) {
|
|
85
|
-
findings.push(retryFinding(text, file, text.split(/\r?\n/).slice(0, i).join('\n').length + (i ? 1 : 0)))
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return findings
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function retryFindings(text, masked, file, {python = false} = {}) {
|
|
92
|
-
return python ? pythonRetryFindings(text, masked, file) : bracedRetryFindings(text, masked, file)
|
|
93
|
-
}
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
// Conservative source-level correctness review signals. These checks deliberately recognize only
|
|
2
|
-
// a few grounded patterns with local evidence; they are not a compiler, race detector, or proof that
|
|
3
|
-
// the surrounding behavior is wrong.
|
|
4
|
-
import { makeFinding } from "./findings.js";
|
|
5
|
-
import {retryFindings} from './source-correctness/retry-patterns.js'
|
|
6
|
-
import {lineNumberAt} from '../util.js'
|
|
7
|
-
|
|
8
|
-
const lineAt = lineNumberAt;
|
|
9
|
-
|
|
10
|
-
const lineText = (text, index) => {
|
|
11
|
-
const start = text.lastIndexOf("\n", Math.max(0, index - 1)) + 1;
|
|
12
|
-
const end = text.indexOf("\n", index);
|
|
13
|
-
return text.slice(start, end < 0 ? text.length : end).trim().slice(0, 300);
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// Preserve byte offsets/newlines while removing comments and string contents from regex input.
|
|
17
|
-
function maskNonCode(text, { hashComments = false } = {}) {
|
|
18
|
-
const chars = String(text || "").split("");
|
|
19
|
-
let quote = "", escaped = false, lineComment = false, blockComment = false;
|
|
20
|
-
for (let i = 0; i < chars.length; i++) {
|
|
21
|
-
const ch = chars[i], next = chars[i + 1];
|
|
22
|
-
if (lineComment) {
|
|
23
|
-
if (ch === "\n" || ch === "\r") lineComment = false;
|
|
24
|
-
else chars[i] = " ";
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (blockComment) {
|
|
28
|
-
if (ch === "*" && next === "/") { chars[i] = chars[i + 1] = " "; i++; blockComment = false; }
|
|
29
|
-
else if (ch !== "\n" && ch !== "\r") chars[i] = " ";
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (quote) {
|
|
33
|
-
if (ch !== "\n" && ch !== "\r") chars[i] = " ";
|
|
34
|
-
if (escaped) escaped = false;
|
|
35
|
-
else if (ch === "\\") escaped = true;
|
|
36
|
-
else if (ch === quote) quote = "";
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (ch === '"' || ch === "'" || ch === "`") { quote = ch; chars[i] = " "; continue; }
|
|
40
|
-
if (hashComments && ch === "#") { chars[i] = " "; lineComment = true; continue; }
|
|
41
|
-
if (ch === "/" && next === "/") { chars[i] = chars[i + 1] = " "; i++; lineComment = true; continue; }
|
|
42
|
-
if (ch === "/" && next === "*") { chars[i] = chars[i + 1] = " "; i++; blockComment = true; }
|
|
43
|
-
}
|
|
44
|
-
return chars.join("");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function balancedEnd(text, openAt) {
|
|
48
|
-
if (text[openAt] !== "{") return -1;
|
|
49
|
-
let depth = 0;
|
|
50
|
-
for (let i = openAt; i < text.length; i++) {
|
|
51
|
-
if (text[i] === "{") depth++;
|
|
52
|
-
else if (text[i] === "}" && --depth === 0) return i + 1;
|
|
53
|
-
}
|
|
54
|
-
return -1;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const escapeRegex = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
58
|
-
const upperSnake = (value) => String(value || "")
|
|
59
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
60
|
-
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
61
|
-
.replace(/^_+|_+$/g, "")
|
|
62
|
-
.toUpperCase();
|
|
63
|
-
|
|
64
|
-
function goFunctions(masked) {
|
|
65
|
-
const functions = [];
|
|
66
|
-
const re = /\bfunc\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(([^)]*)\)[^{]*\{/g;
|
|
67
|
-
let match;
|
|
68
|
-
while ((match = re.exec(masked))) {
|
|
69
|
-
const open = re.lastIndex - 1;
|
|
70
|
-
const end = balancedEnd(masked, open);
|
|
71
|
-
if (end < 0) continue;
|
|
72
|
-
functions.push({ name: match[1], params: match[2], open, end, body: masked.slice(open + 1, end - 1) });
|
|
73
|
-
re.lastIndex = end;
|
|
74
|
-
}
|
|
75
|
-
return functions;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function goSliceFindings(text, masked, file) {
|
|
79
|
-
const findings = [];
|
|
80
|
-
for (const fn of goFunctions(masked)) {
|
|
81
|
-
const sliceParams = [...fn.params.matchAll(/(?:^|,)\s*([A-Za-z_]\w*)\s+\[\][^,)]*/g)].map((match) => match[1]);
|
|
82
|
-
for (const parameter of sliceParams) {
|
|
83
|
-
const slice = new RegExp(`\\b${escapeRegex(parameter)}\\s*\\[\\s*(?:0\\s*)?:\\s*(\\d+)\\s*\\]`, "g").exec(fn.body);
|
|
84
|
-
if (!slice || Number(slice[1]) < 1) continue;
|
|
85
|
-
const before = fn.body.slice(0, slice.index);
|
|
86
|
-
const lenGuard = new RegExp(`\\blen\\s*\\(\\s*${escapeRegex(parameter)}\\s*\\)`).test(before);
|
|
87
|
-
if (lenGuard) continue;
|
|
88
|
-
const index = fn.open + 1 + slice.index;
|
|
89
|
-
findings.push(makeFinding({
|
|
90
|
-
category: "structure",
|
|
91
|
-
rule: "go-unguarded-fixed-slice",
|
|
92
|
-
severity: "medium",
|
|
93
|
-
confidence: "high",
|
|
94
|
-
title: `Potential slice-bounds panic in ${fn.name}: ${parameter}[:${slice[1]}]`,
|
|
95
|
-
detail: `The slice parameter "${parameter}" is read to a fixed upper bound before any visible len(${parameter}) guard in this function. Confirm every caller guarantees at least ${slice[1]} element(s), or guard the access locally.`,
|
|
96
|
-
file,
|
|
97
|
-
line: lineAt(text, index),
|
|
98
|
-
symbol: `${fn.name}()`,
|
|
99
|
-
evidence: [{ file, line: lineAt(text, index), snippet: lineText(text, index) }],
|
|
100
|
-
fixHint: `validate len(${parameter}) >= ${slice[1]} before slicing`,
|
|
101
|
-
}));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return findings;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function goConstructorTypeFindings(text, masked, file) {
|
|
108
|
-
const findings = [];
|
|
109
|
-
for (const fn of goFunctions(masked)) {
|
|
110
|
-
if (!/^New[A-Z]/.test(fn.name)) continue;
|
|
111
|
-
const suffix = upperSnake(fn.name.slice(3));
|
|
112
|
-
if (!suffix) continue;
|
|
113
|
-
const assigned = /\b(?:Type|Kind)\s*:\s*([A-Z][A-Z0-9_]*(?:_TYPE_|_KIND_)[A-Z0-9_]+)\b/g.exec(fn.body);
|
|
114
|
-
if (!assigned) continue;
|
|
115
|
-
const markerAt = Math.max(assigned[1].lastIndexOf("_TYPE_"), assigned[1].lastIndexOf("_KIND_"));
|
|
116
|
-
const markerLength = 6;
|
|
117
|
-
const expected = `${assigned[1].slice(0, markerAt + markerLength)}${suffix}`;
|
|
118
|
-
if (expected === assigned[1] || !new RegExp(`\\b${escapeRegex(expected)}\\b`).test(masked)) continue;
|
|
119
|
-
const index = fn.open + 1 + assigned.index;
|
|
120
|
-
findings.push(makeFinding({
|
|
121
|
-
category: "structure",
|
|
122
|
-
rule: "constructor-enum-mismatch",
|
|
123
|
-
severity: "medium",
|
|
124
|
-
confidence: "high",
|
|
125
|
-
title: `Constructor/type discriminator mismatch: ${fn.name} uses ${assigned[1]}`,
|
|
126
|
-
detail: `The constructor name corresponds to the declared discriminator ${expected}, but the returned literal assigns ${assigned[1]}. This is a focused copy/paste review signal; confirm the intended wire type before changing it.`,
|
|
127
|
-
file,
|
|
128
|
-
line: lineAt(text, index),
|
|
129
|
-
symbol: `${fn.name}()`,
|
|
130
|
-
evidence: [{ file, line: lineAt(text, index), snippet: lineText(text, index) }],
|
|
131
|
-
fixHint: `verify whether the discriminator should be ${expected}`,
|
|
132
|
-
}));
|
|
133
|
-
}
|
|
134
|
-
return findings;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function javaInterruptFindings(text, masked, file) {
|
|
138
|
-
const findings = [];
|
|
139
|
-
const re = /\bcatch\s*\(\s*InterruptedException\s+([A-Za-z_]\w*)\s*\)\s*\{/g;
|
|
140
|
-
let match;
|
|
141
|
-
while ((match = re.exec(masked))) {
|
|
142
|
-
const open = re.lastIndex - 1;
|
|
143
|
-
const end = balancedEnd(masked, open);
|
|
144
|
-
if (end < 0) continue;
|
|
145
|
-
const body = masked.slice(open + 1, end - 1);
|
|
146
|
-
const restores = /\bThread\s*\.\s*currentThread\s*\(\s*\)\s*\.\s*interrupt\s*\(/.test(body);
|
|
147
|
-
const rethrows = new RegExp(`\\bthrow\\s+${escapeRegex(match[1])}\\s*;`).test(body)
|
|
148
|
-
|| /\bthrow\s+new\s+InterruptedException\b/.test(body);
|
|
149
|
-
if (restores || rethrows) continue;
|
|
150
|
-
findings.push(makeFinding({
|
|
151
|
-
category: "structure",
|
|
152
|
-
rule: "java-interrupt-status-not-restored",
|
|
153
|
-
severity: "medium",
|
|
154
|
-
confidence: "high",
|
|
155
|
-
title: "InterruptedException caught without a visible interrupt restore",
|
|
156
|
-
detail: "The catch block neither directly rethrows InterruptedException nor calls Thread.currentThread().interrupt(). A helper may restore it indirectly, so review before changing; otherwise cancellation can be lost.",
|
|
157
|
-
file,
|
|
158
|
-
line: lineAt(text, match.index),
|
|
159
|
-
evidence: [{ file, line: lineAt(text, match.index), snippet: lineText(text, match.index) }],
|
|
160
|
-
fixHint: "restore the interrupt status or propagate the interruption according to the method contract",
|
|
161
|
-
}));
|
|
162
|
-
re.lastIndex = end;
|
|
163
|
-
}
|
|
164
|
-
return findings;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function analyzeSourceCorrectness(sources, { isNonProductPath = () => false } = {}) {
|
|
168
|
-
const findings = [];
|
|
169
|
-
const supportedRuntimeFiles = new Set();
|
|
170
|
-
const concurrencyFiles = new Set();
|
|
171
|
-
const checks = {
|
|
172
|
-
goFixedSlice: { supportedFiles: 0, findings: 0 },
|
|
173
|
-
constructorDiscriminator: { supportedFiles: 0, findings: 0 },
|
|
174
|
-
javaInterrupt: { supportedFiles: 0, findings: 0 },
|
|
175
|
-
retryTermination: { supportedFiles: 0, findings: 0 },
|
|
176
|
-
};
|
|
177
|
-
for (const [file, source] of sources || []) {
|
|
178
|
-
if (isNonProductPath(file)) continue;
|
|
179
|
-
const text = String(source || "");
|
|
180
|
-
const go = /\.go$/i.test(file), java = /\.java$/i.test(file), python = /\.py$/i.test(file);
|
|
181
|
-
const masked = maskNonCode(text, { hashComments: python });
|
|
182
|
-
const retrySupported = /\.(?:[cm]?[jt]sx?|java|go|py)$/i.test(file);
|
|
183
|
-
if (go) {
|
|
184
|
-
supportedRuntimeFiles.add(file);
|
|
185
|
-
checks.goFixedSlice.supportedFiles++;
|
|
186
|
-
checks.constructorDiscriminator.supportedFiles++;
|
|
187
|
-
const slice = goSliceFindings(text, masked, file);
|
|
188
|
-
const discriminator = goConstructorTypeFindings(text, masked, file);
|
|
189
|
-
findings.push(...slice, ...discriminator);
|
|
190
|
-
checks.goFixedSlice.findings += slice.length;
|
|
191
|
-
checks.constructorDiscriminator.findings += discriminator.length;
|
|
192
|
-
}
|
|
193
|
-
if (java) {
|
|
194
|
-
supportedRuntimeFiles.add(file);
|
|
195
|
-
concurrencyFiles.add(file);
|
|
196
|
-
checks.javaInterrupt.supportedFiles++;
|
|
197
|
-
const interruption = javaInterruptFindings(text, masked, file);
|
|
198
|
-
findings.push(...interruption);
|
|
199
|
-
checks.javaInterrupt.findings += interruption.length;
|
|
200
|
-
}
|
|
201
|
-
if (retrySupported) {
|
|
202
|
-
supportedRuntimeFiles.add(file);
|
|
203
|
-
checks.retryTermination.supportedFiles++;
|
|
204
|
-
const retry = retryFindings(text, masked, file, {python});
|
|
205
|
-
findings.push(...retry);
|
|
206
|
-
checks.retryTermination.findings += retry.length;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return {
|
|
210
|
-
findings,
|
|
211
|
-
coverage: {
|
|
212
|
-
runtimeCorrectnessFiles: supportedRuntimeFiles.size,
|
|
213
|
-
concurrencyFiles: concurrencyFiles.size,
|
|
214
|
-
checks,
|
|
215
|
-
limitations: [
|
|
216
|
-
"bounded source patterns only",
|
|
217
|
-
"no compiler or runtime execution",
|
|
218
|
-
"no race detector; race freedom is not claimed",
|
|
219
|
-
],
|
|
220
|
-
},
|
|
221
|
-
};
|
|
222
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
// A deliberately weaker fallback than real coverage: follow runtime graph edges out of test files
|
|
2
|
-
// and report nearest test paths. This is evidence of static reachability only; it never claims a line,
|
|
3
|
-
// branch or symbol executed. The traversal is bounded for large repositories.
|
|
4
|
-
import { createPathClassifier, hasPathClass } from "../path-classification.js";
|
|
5
|
-
import { isStructuralRelation } from "../graph/relations.js";
|
|
6
|
-
|
|
7
|
-
const MAX_TEST_FILES = 250;
|
|
8
|
-
const MAX_NEAREST_TESTS = 3;
|
|
9
|
-
const MAX_DEPTH = 10;
|
|
10
|
-
const MAX_STATES = 100_000;
|
|
11
|
-
|
|
12
|
-
const normalize = (value) => String(value || "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
13
|
-
const endpoint = (value) => String(value && typeof value === "object" ? value.id : value || "");
|
|
14
|
-
const confidenceScore = (value) => {
|
|
15
|
-
const normalized = String(value || "").toUpperCase();
|
|
16
|
-
if (["EXTRACTED", "EXACT", "HIGH"].includes(normalized)) return 3;
|
|
17
|
-
if (["INFERRED", "MEDIUM"].includes(normalized)) return 2;
|
|
18
|
-
return 1;
|
|
19
|
-
};
|
|
20
|
-
const confidenceLabel = (score) => score >= 3 ? "HIGH" : score >= 2 ? "MEDIUM" : "LOW";
|
|
21
|
-
const pathInScope = (path, prefix) => !prefix || path === prefix || path.startsWith(`${prefix}/`);
|
|
22
|
-
|
|
23
|
-
export function computeStaticTestReachability(graph, {
|
|
24
|
-
repoRoot = null,
|
|
25
|
-
path = "",
|
|
26
|
-
maxDepth = MAX_DEPTH,
|
|
27
|
-
maxStates = MAX_STATES,
|
|
28
|
-
maxTests = MAX_TEST_FILES,
|
|
29
|
-
maxNearestTests = MAX_NEAREST_TESTS,
|
|
30
|
-
} = {}) {
|
|
31
|
-
const nodes = graph?.nodes || [];
|
|
32
|
-
const links = graph?.links || [];
|
|
33
|
-
const classifier = createPathClassifier(repoRoot);
|
|
34
|
-
const classificationCache = new Map();
|
|
35
|
-
const classify = (file) => {
|
|
36
|
-
const normalized = normalize(file);
|
|
37
|
-
if (!classificationCache.has(normalized)) classificationCache.set(normalized, classifier.explain(normalized, { content: "" }));
|
|
38
|
-
return classificationCache.get(normalized);
|
|
39
|
-
};
|
|
40
|
-
// Files that carry inline test surfaces (Rust `#[cfg(test)]` modules / `#[test]` fns live inside the
|
|
41
|
-
// production file, so path classification alone never sees them). The graph flags these per symbol.
|
|
42
|
-
// These files are NOT seeded as whole-file BFS sources: at file granularity we cannot tell an inline
|
|
43
|
-
// test's out-edge from a same-file production call, so seeding the file would propagate its PRODUCTION
|
|
44
|
-
// call chains as if a test reached them. They are self-covered at distance 0 instead (an honest,
|
|
45
|
-
// conservative signal: "this file has its own tests"), never over-claiming reach to other files.
|
|
46
|
-
const testSurfaceFiles = new Set();
|
|
47
|
-
const isTest = (file) => hasPathClass(classify(file), "test", "e2e");
|
|
48
|
-
const isProduct = (file) => {
|
|
49
|
-
const info = classify(file);
|
|
50
|
-
return !info.excluded && !hasPathClass(info, "test", "e2e", "generated", "vendored", "mock", "story", "docs", "benchmark", "temp");
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const idToFile = new Map();
|
|
54
|
-
const files = new Set();
|
|
55
|
-
for (const node of nodes) {
|
|
56
|
-
const file = normalize(node?.source_file || (!String(node?.id || "").includes("#") ? node?.id : ""));
|
|
57
|
-
if (!file) continue;
|
|
58
|
-
idToFile.set(String(node.id), file);
|
|
59
|
-
files.add(file);
|
|
60
|
-
if (node?.test_surface === true) testSurfaceFiles.add(file);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Collapse symbol/file edges to directed file dependencies. Multiple symbol edges retain the best
|
|
64
|
-
// confidence for the same hop; compile-time-only and ownership relations never imply test reach.
|
|
65
|
-
const adjacency = new Map();
|
|
66
|
-
for (const link of links) {
|
|
67
|
-
if (!link || link.typeOnly === true || link.compileOnly === true || isStructuralRelation(link.relation)) continue;
|
|
68
|
-
const from = idToFile.get(endpoint(link.source));
|
|
69
|
-
const to = idToFile.get(endpoint(link.target));
|
|
70
|
-
if (!from || !to || from === to) continue;
|
|
71
|
-
if (!adjacency.has(from)) adjacency.set(from, new Map());
|
|
72
|
-
const score = confidenceScore(link.confidence);
|
|
73
|
-
const previous = adjacency.get(from).get(to);
|
|
74
|
-
if (!previous || score > previous.score) adjacency.get(from).set(to, { score, relation: link.relation || "dependency" });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const allTests = [...files].filter(isTest).sort((a, b) => a.localeCompare(b));
|
|
78
|
-
const testLimit = Math.max(0, Math.min(MAX_TEST_FILES, Number(maxTests) || MAX_TEST_FILES));
|
|
79
|
-
const tests = allTests.slice(0, testLimit);
|
|
80
|
-
const scope = normalize(path).replace(/\/+$/, "");
|
|
81
|
-
const productFiles = [...files].filter(isProduct).filter((file) => pathInScope(file, scope)).sort((a, b) => a.localeCompare(b));
|
|
82
|
-
const productSet = new Set(productFiles);
|
|
83
|
-
const nearest = new Map();
|
|
84
|
-
// A production file that carries its own inline tests is self-covered at distance 0: its tests exercise
|
|
85
|
-
// its own production symbols. This is the honest floor that fixes a false "0 reachable" for repos whose
|
|
86
|
-
// tests are all inline (Rust); it never asserts reach to OTHER files, so it cannot over-claim.
|
|
87
|
-
const inlineCovered = productFiles.filter((file) => testSurfaceFiles.has(file));
|
|
88
|
-
for (const file of inlineCovered) {
|
|
89
|
-
nearest.set(file, [{ test: file, distance: 0, score: 3, confidence: confidenceLabel(3), path: [file] }]);
|
|
90
|
-
}
|
|
91
|
-
const queue = tests.map((test) => ({ file: test, test, distance: 0, score: 3, path: [test] }));
|
|
92
|
-
const seen = new Map(tests.map((test) => [`${test}\0${test}`, 0]));
|
|
93
|
-
let cursor = 0;
|
|
94
|
-
let traversedStates = 0;
|
|
95
|
-
const depthLimit = Math.max(1, Math.min(MAX_DEPTH, Number(maxDepth) || MAX_DEPTH));
|
|
96
|
-
const stateLimit = Math.max(100, Math.min(MAX_STATES, Number(maxStates) || MAX_STATES));
|
|
97
|
-
const nearestLimit = Math.max(1, Math.min(MAX_NEAREST_TESTS, Number(maxNearestTests) || MAX_NEAREST_TESTS));
|
|
98
|
-
|
|
99
|
-
while (cursor < queue.length && traversedStates < stateLimit) {
|
|
100
|
-
const current = queue[cursor++];
|
|
101
|
-
traversedStates++;
|
|
102
|
-
if (current.distance >= depthLimit) continue;
|
|
103
|
-
for (const [next, edge] of adjacency.get(current.file) || []) {
|
|
104
|
-
const distance = current.distance + 1;
|
|
105
|
-
const score = Math.min(current.score, edge.score);
|
|
106
|
-
const key = `${current.test}\0${next}`;
|
|
107
|
-
if ((seen.get(key) ?? Infinity) <= distance) continue;
|
|
108
|
-
seen.set(key, distance);
|
|
109
|
-
const record = { test: current.test, distance, score, confidence: confidenceLabel(score), path: [...current.path, next] };
|
|
110
|
-
if (productSet.has(next)) {
|
|
111
|
-
const records = nearest.get(next) || [];
|
|
112
|
-
records.push(record);
|
|
113
|
-
records.sort((a, b) => a.distance - b.distance || b.score - a.score || a.test.localeCompare(b.test));
|
|
114
|
-
if (records.length > nearestLimit) records.length = nearestLimit;
|
|
115
|
-
nearest.set(next, records);
|
|
116
|
-
}
|
|
117
|
-
// Once enough nearer test paths meet at the same file, a longer test prefix cannot become a
|
|
118
|
-
// nearest path downstream because every continuation from this point is shared.
|
|
119
|
-
const records = nearest.get(next);
|
|
120
|
-
if (records?.length >= nearestLimit && !records.some((item) => item.test === current.test) && distance > records[records.length - 1].distance) continue;
|
|
121
|
-
queue.push({ file: next, test: current.test, distance, score, path: record.path });
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const reachable = productFiles
|
|
126
|
-
.filter((file) => nearest.has(file))
|
|
127
|
-
.map((file) => ({ file, nearestTests: nearest.get(file) }))
|
|
128
|
-
.sort((a, b) => a.nearestTests[0].distance - b.nearestTests[0].distance || a.file.localeCompare(b.file));
|
|
129
|
-
const unreachable = productFiles.filter((file) => !nearest.has(file));
|
|
130
|
-
return {
|
|
131
|
-
kind: "staticTestReachability",
|
|
132
|
-
actualCoverage: "NOT_AVAILABLE",
|
|
133
|
-
// Inline-test files self-cover rather than seed the traversal, but they ARE test-bearing files, so
|
|
134
|
-
// the reported counts include them — otherwise a Rust repo would read "0 test files" beside real reach.
|
|
135
|
-
testFiles: tests.length + inlineCovered.length,
|
|
136
|
-
totalTestFiles: allTests.length + inlineCovered.length,
|
|
137
|
-
productFiles: productFiles.length,
|
|
138
|
-
reachableFiles: reachable.length,
|
|
139
|
-
unreachableFiles: unreachable.length,
|
|
140
|
-
reachable,
|
|
141
|
-
unreachable,
|
|
142
|
-
bounds: {
|
|
143
|
-
maxDepth: depthLimit,
|
|
144
|
-
maxStates: stateLimit,
|
|
145
|
-
traversedStates,
|
|
146
|
-
maxTests: testLimit,
|
|
147
|
-
truncated: allTests.length > tests.length || (cursor < queue.length && traversedStates >= stateLimit),
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import {ENTRY_FILE} from '../dead-check.js'
|
|
2
|
-
import {fileOfId, graphEndpointId} from '../../graph/node-id.js'
|
|
3
|
-
|
|
4
|
-
const TEST_FILE_RE = /(^|[/])(test|tests|__tests__|spec|e2e|__mocks__)([/]|$)|[._-](test|spec)\.[a-z0-9]+$/i
|
|
5
|
-
const NON_CODE_RE = /\.(json|ya?ml|sh|ps1|md|txt|html?|css|scss|less)$|(^|[/])(dockerfile|containerfile)/i
|
|
6
|
-
const dirOf = (file) => file.includes('/') ? file.slice(0, file.lastIndexOf('/')) : ''
|
|
7
|
-
|
|
8
|
-
export function buildFileImportGraph(graph, {includeTypeOnly = false, includeCompileOnly = false} = {}) {
|
|
9
|
-
const fileIds = new Set()
|
|
10
|
-
for (const node of graph.nodes || []) if (!String(node.id).includes('#')) fileIds.add(String(node.id))
|
|
11
|
-
const runtimeAdj = new Map(), allAdj = new Map()
|
|
12
|
-
const edges = [], allEdges = [], typeOnlyEdges = [], compileOnlyEdges = [], compileTimeEdges = []
|
|
13
|
-
const runtimeSeen = new Set(), allSeen = new Set(), typeSeen = new Set(), compileSeen = new Set(), compileTimeSeen = new Set()
|
|
14
|
-
const add = (map, source, target) => {
|
|
15
|
-
let targets = map.get(source)
|
|
16
|
-
if (!targets) map.set(source, (targets = new Set()))
|
|
17
|
-
targets.add(target)
|
|
18
|
-
}
|
|
19
|
-
for (const link of graph.links || []) {
|
|
20
|
-
if (link.relation !== 'imports' && link.relation !== 're_exports') continue
|
|
21
|
-
const source = graphEndpointId(link.source), target = graphEndpointId(link.target)
|
|
22
|
-
if (!fileIds.has(source) || !fileIds.has(target) || source === target) continue
|
|
23
|
-
if (source.endsWith('.go') && target.endsWith('.go') && dirOf(source) === dirOf(target)) continue
|
|
24
|
-
const key = `${source}\0${target}`
|
|
25
|
-
if (!allSeen.has(key)) { allSeen.add(key); add(allAdj, source, target); allEdges.push([source, target]) }
|
|
26
|
-
if (link.typeOnly === true || link.compileOnly === true) {
|
|
27
|
-
if (link.typeOnly === true && !typeSeen.has(key)) { typeSeen.add(key); typeOnlyEdges.push([source, target]) }
|
|
28
|
-
if (link.compileOnly === true && !compileSeen.has(key)) { compileSeen.add(key); compileOnlyEdges.push([source, target]) }
|
|
29
|
-
if (!compileTimeSeen.has(key)) { compileTimeSeen.add(key); compileTimeEdges.push([source, target]) }
|
|
30
|
-
continue
|
|
31
|
-
}
|
|
32
|
-
if (!runtimeSeen.has(key)) { runtimeSeen.add(key); add(runtimeAdj, source, target); edges.push([source, target]) }
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
fileIds,
|
|
36
|
-
adj: includeTypeOnly || includeCompileOnly ? allAdj : runtimeAdj,
|
|
37
|
-
edges: includeTypeOnly || includeCompileOnly ? allEdges : edges,
|
|
38
|
-
runtimeAdj,
|
|
39
|
-
runtimeEdges: edges,
|
|
40
|
-
allAdj,
|
|
41
|
-
allEdges,
|
|
42
|
-
typeOnlyEdges: typeOnlyEdges.filter(([a, b]) => !runtimeSeen.has(`${a}\0${b}`)),
|
|
43
|
-
compileOnlyEdges: compileOnlyEdges.filter(([a, b]) => !runtimeSeen.has(`${a}\0${b}`)),
|
|
44
|
-
compileTimeEdges: compileTimeEdges.filter(([a, b]) => !runtimeSeen.has(`${a}\0${b}`)),
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Iterative Tarjan: repository graphs can be deep enough to overflow recursive JavaScript.
|
|
49
|
-
export function findSccs(adjacency) {
|
|
50
|
-
const index = new Map(), low = new Map(), onStack = new Set(), nodes = []
|
|
51
|
-
let counter = 0
|
|
52
|
-
const components = []
|
|
53
|
-
for (const root of adjacency.keys()) {
|
|
54
|
-
if (index.has(root)) continue
|
|
55
|
-
index.set(root, counter); low.set(root, counter); counter++
|
|
56
|
-
nodes.push(root); onStack.add(root)
|
|
57
|
-
const stack = [{value: root, child: 0, neighbors: [...(adjacency.get(root) || [])]}]
|
|
58
|
-
while (stack.length) {
|
|
59
|
-
const frame = stack[stack.length - 1]
|
|
60
|
-
if (frame.child < frame.neighbors.length) {
|
|
61
|
-
const next = frame.neighbors[frame.child++]
|
|
62
|
-
if (!index.has(next)) {
|
|
63
|
-
index.set(next, counter); low.set(next, counter); counter++
|
|
64
|
-
nodes.push(next); onStack.add(next)
|
|
65
|
-
stack.push({value: next, child: 0, neighbors: [...(adjacency.get(next) || [])]})
|
|
66
|
-
} else if (onStack.has(next)) low.set(frame.value, Math.min(low.get(frame.value), index.get(next)))
|
|
67
|
-
continue
|
|
68
|
-
}
|
|
69
|
-
stack.pop()
|
|
70
|
-
if (stack.length) {
|
|
71
|
-
const parent = stack[stack.length - 1]
|
|
72
|
-
low.set(parent.value, Math.min(low.get(parent.value), low.get(frame.value)))
|
|
73
|
-
}
|
|
74
|
-
if (low.get(frame.value) !== index.get(frame.value)) continue
|
|
75
|
-
const component = []
|
|
76
|
-
let value
|
|
77
|
-
do { value = nodes.pop(); onStack.delete(value); component.push(value) } while (value !== frame.value)
|
|
78
|
-
if (component.length > 1) components.push(component)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return components
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function representativeCycle(adjacency, component) {
|
|
85
|
-
const members = new Set(component)
|
|
86
|
-
const start = [...component].sort()[0]
|
|
87
|
-
const previous = new Map([[start, null]])
|
|
88
|
-
const queue = [start]
|
|
89
|
-
while (queue.length) {
|
|
90
|
-
const current = queue.shift()
|
|
91
|
-
for (const next of [...(adjacency.get(current) || [])].sort()) {
|
|
92
|
-
if (next === start) {
|
|
93
|
-
const path = []
|
|
94
|
-
for (let value = current; value != null; value = previous.get(value)) path.push(value)
|
|
95
|
-
return [...path.reverse(), start]
|
|
96
|
-
}
|
|
97
|
-
if (!members.has(next) || previous.has(next)) continue
|
|
98
|
-
previous.set(next, current)
|
|
99
|
-
queue.push(next)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
const fallback = [...component].sort()
|
|
103
|
-
return [...fallback, fallback[0]]
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function findOrphans(graph, {entrySet = new Set(), externalImportFiles = new Set()} = {}) {
|
|
107
|
-
const degree = new Map()
|
|
108
|
-
for (const link of graph.links || []) {
|
|
109
|
-
if (link.relation === 'contains') continue
|
|
110
|
-
const source = fileOfId(graphEndpointId(link.source)), target = fileOfId(graphEndpointId(link.target))
|
|
111
|
-
if (source === target) continue
|
|
112
|
-
degree.set(source, (degree.get(source) || 0) + 1)
|
|
113
|
-
degree.set(target, (degree.get(target) || 0) + 1)
|
|
114
|
-
}
|
|
115
|
-
const out = []
|
|
116
|
-
for (const node of graph.nodes || []) {
|
|
117
|
-
const id = String(node.id)
|
|
118
|
-
if (id.includes('#') || (degree.get(id) || 0) > 0) continue
|
|
119
|
-
const file = node.source_file
|
|
120
|
-
if (entrySet.has(file) || ENTRY_FILE.test(file) || TEST_FILE_RE.test(file) || NON_CODE_RE.test(file)) continue
|
|
121
|
-
out.push({file, importsExternals: externalImportFiles.has(file)})
|
|
122
|
-
}
|
|
123
|
-
return out
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function globToRe(glob) {
|
|
127
|
-
const parts = String(glob).split('**').map((part) => part
|
|
128
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]'))
|
|
129
|
-
let source = parts.join('.*')
|
|
130
|
-
source = source.replace(/\/\.\*\//g, '/(?:.*/)?').replace(/^\.\*\//, '(?:.*/)?')
|
|
131
|
-
return new RegExp(`^${source}$`)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function checkBoundaries(edges, rules = {}) {
|
|
135
|
-
const violations = []
|
|
136
|
-
const forbidden = (rules.forbidden || []).map((rule) => ({...rule, fromRe: globToRe(rule.from), toRe: globToRe(rule.to)}))
|
|
137
|
-
const allowedOnly = (rules.allowedOnly || []).map((rule) => ({
|
|
138
|
-
...rule,
|
|
139
|
-
fromRe: globToRe(rule.from),
|
|
140
|
-
toRes: (Array.isArray(rule.to) ? rule.to : [rule.to]).map(globToRe),
|
|
141
|
-
}))
|
|
142
|
-
for (const [source, target] of edges) {
|
|
143
|
-
for (const rule of forbidden) if (rule.fromRe.test(source) && rule.toRe.test(target)) {
|
|
144
|
-
violations.push({name: rule.name, comment: rule.comment || '', severity: rule.severity, from: source, to: target, kind: 'forbidden'})
|
|
145
|
-
}
|
|
146
|
-
for (const rule of allowedOnly) if (rule.fromRe.test(source) && !rule.toRes.some((regexp) => regexp.test(target))) {
|
|
147
|
-
violations.push({name: rule.name, comment: rule.comment || '', severity: rule.severity, from: source, to: target, kind: 'allowedOnly'})
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return violations
|
|
151
|
-
}
|