vibecodedshit 0.0.0-stage → 0.3.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 +95 -0
- package/README.md +73 -2
- package/assets/fonts/NotoEmoji-OFL.txt +93 -0
- package/assets/fonts/NotoEmoji-Poop.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-OFL.txt +93 -0
- package/dist/bin.js +11 -0
- package/dist/card/previous.js +38 -0
- package/dist/card/render.js +71 -0
- package/dist/cli.js +138 -0
- package/dist/commands/card.js +69 -0
- package/dist/commands/common.js +1 -0
- package/dist/commands/diff.js +54 -0
- package/dist/commands/footer.js +28 -0
- package/dist/commands/report.js +49 -0
- package/dist/commands/route.js +62 -0
- package/dist/commands/submit.js +125 -0
- package/dist/commands/summary.js +77 -0
- package/dist/diff/compare.js +73 -0
- package/dist/diff/conditions.js +87 -0
- package/dist/diff/format.js +23 -0
- package/dist/exec/docker.js +158 -0
- package/dist/exec/findings.js +73 -0
- package/dist/exec/plan.js +50 -0
- package/dist/exec/reports.js +90 -0
- package/dist/exec/roots.js +49 -0
- package/dist/exec/run.js +182 -0
- package/dist/exec/steps/build.js +45 -0
- package/dist/exec/steps/coverage-install.js +12 -0
- package/dist/exec/steps/install.js +41 -0
- package/dist/exec/steps/knip.js +28 -0
- package/dist/exec/steps/lint.js +25 -0
- package/dist/exec/steps/lockfiles.js +25 -0
- package/dist/exec/steps/test.js +17 -0
- package/dist/exec/steps/typecheck.js +40 -0
- package/dist/exec/test-reports/coverage-py.js +23 -0
- package/dist/exec/test-reports/coverage-ranges.js +65 -0
- package/dist/exec/test-reports/coverage-summary.js +23 -0
- package/dist/exec/test-reports/jest-json.js +31 -0
- package/dist/exec/test-reports/junit.js +24 -0
- package/dist/exec/test-reports/mocha-json.js +18 -0
- package/dist/exec/test-reports/vitest-json.js +5 -0
- package/dist/exec/test-runners/detect.js +99 -0
- package/dist/exec/test-runners/index.js +6 -0
- package/dist/exec/test-runners/jest.js +27 -0
- package/dist/exec/test-runners/mocha.js +28 -0
- package/dist/exec/test-runners/node-test.js +24 -0
- package/dist/exec/test-runners/pytest.js +18 -0
- package/dist/exec/test-runners/script.js +48 -0
- package/dist/exec/test-runners/spec.js +1 -0
- package/dist/exec/test-runners/vitest.js +28 -0
- package/dist/exec/tests.js +120 -0
- package/dist/exec/types.js +1 -0
- package/dist/interactive/after-scan.js +121 -0
- package/dist/interactive/docker-setup.js +97 -0
- package/dist/interactive/prompt.js +17 -0
- package/dist/interactive/system.js +10 -0
- package/dist/manifest/package-json.js +36 -0
- package/dist/manifest/python.js +106 -0
- package/dist/measure/d3/index.js +45 -0
- package/dist/measure/d3/presets.js +100 -0
- package/dist/measure/d3/py/adapter.js +67 -0
- package/dist/measure/d3/py/config.js +80 -0
- package/dist/measure/d3/py/contracts.js +57 -0
- package/dist/measure/d3/py/domain.js +57 -0
- package/dist/measure/d3/py/graph.js +214 -0
- package/dist/measure/d3/py/interpreter.js +31 -0
- package/dist/measure/d3/py/layers.js +120 -0
- package/dist/measure/d3/py/roots.js +58 -0
- package/dist/measure/d3/py/types.js +1 -0
- package/dist/measure/d3/scc.js +64 -0
- package/dist/measure/d3/ts/adapter.js +371 -0
- package/dist/measure/d3/ts/classify.js +107 -0
- package/dist/measure/d3/ts/config.js +318 -0
- package/dist/measure/d3/ts/domain.js +23 -0
- package/dist/measure/d3/ts/rules.js +65 -0
- package/dist/measure/d3/types.js +5 -0
- package/dist/measure/deps/cvss.js +66 -0
- package/dist/measure/deps/declared.js +195 -0
- package/dist/measure/deps/files.js +40 -0
- package/dist/measure/deps/lockfiles.js +308 -0
- package/dist/measure/deps/osv.js +122 -0
- package/dist/measure/deps/registry.js +142 -0
- package/dist/measure/deps/resolved.js +78 -0
- package/dist/measure/deps/units.js +5 -0
- package/dist/measure/eslint/health-plugin.js +19 -0
- package/dist/measure/ops/docs-commands.js +73 -0
- package/dist/measure/ops/docs-resolve.js +217 -0
- package/dist/measure/ops/entry-points.js +49 -0
- package/dist/measure/ops/env-example.js +20 -0
- package/dist/measure/ops/env-reads.js +46 -0
- package/dist/measure/ops/runtime.js +73 -0
- package/dist/measure/p1-1.js +8 -0
- package/dist/measure/p1-2.js +8 -0
- package/dist/measure/p1-3.js +8 -0
- package/dist/measure/p1-4.js +41 -0
- package/dist/measure/p1-5.js +59 -0
- package/dist/measure/p2-1.js +8 -0
- package/dist/measure/p2-2.js +44 -0
- package/dist/measure/p2-3.js +81 -0
- package/dist/measure/p2-4.js +288 -0
- package/dist/measure/p2-5.js +8 -0
- package/dist/measure/p3-1.js +16 -0
- package/dist/measure/p3-2.js +26 -0
- package/dist/measure/p3-3.js +37 -0
- package/dist/measure/p3-4.js +26 -0
- package/dist/measure/p3-5.js +26 -0
- package/dist/measure/p4-1.js +184 -0
- package/dist/measure/p4-2.js +27 -0
- package/dist/measure/p4-3.js +106 -0
- package/dist/measure/p4-4.js +309 -0
- package/dist/measure/p5-1.js +117 -0
- package/dist/measure/p5-2.js +80 -0
- package/dist/measure/p5-3.js +21 -0
- package/dist/measure/p5-4.js +161 -0
- package/dist/measure/p5-5.js +158 -0
- package/dist/measure/p6-1.js +20 -0
- package/dist/measure/p6-2.js +20 -0
- package/dist/measure/p6-3.js +40 -0
- package/dist/measure/p6-4.js +31 -0
- package/dist/measure/p6-5.js +60 -0
- package/dist/measure/p6-6.js +54 -0
- package/dist/measure/py/typecheck-config.js +95 -0
- package/dist/measure/registry.js +31 -0
- package/dist/measure/safety/py-tests.js +124 -0
- package/dist/measure/safety/ts-barrels.js +68 -0
- package/dist/measure/safety/ts-cases.js +111 -0
- package/dist/measure/safety/ts-credits.js +101 -0
- package/dist/measure/safety/ts-imports.js +63 -0
- package/dist/measure/safety/ts-resolve.js +43 -0
- package/dist/measure/ts/directives.js +37 -0
- package/dist/measure/ts/exports.js +60 -0
- package/dist/measure/ts/tsconfig.js +179 -0
- package/dist/measure/types.js +1 -0
- package/dist/measure/units.js +41 -0
- package/dist/methodology/load.js +159 -0
- package/dist/methodology/schema.js +105 -0
- package/dist/python/deadcode.js +27 -0
- package/dist/python/env.js +13 -0
- package/dist/python/helper.js +24 -0
- package/dist/python/imports.js +40 -0
- package/dist/python/nox.js +16 -0
- package/dist/python/scope.js +35 -0
- package/dist/python/tests.js +19 -0
- package/dist/python/typing.js +27 -0
- package/dist/report/escape.js +5 -0
- package/dist/report/format.js +75 -0
- package/dist/report/render.js +213 -0
- package/dist/report/roadmap.js +58 -0
- package/dist/report/scares.js +83 -0
- package/dist/report/style.js +27 -0
- package/dist/report-only/hotspots.js +48 -0
- package/dist/route/check.js +37 -0
- package/dist/route/files.js +74 -0
- package/dist/route/order.js +22 -0
- package/dist/route/select.js +114 -0
- package/dist/route/write.js +13 -0
- package/dist/run/read.js +89 -0
- package/dist/run/recompute.js +19 -0
- package/dist/run/scan.js +196 -0
- package/dist/run/schema.js +1 -0
- package/dist/run/score-run.js +42 -0
- package/dist/run/write.js +4 -0
- package/dist/scope/build.js +173 -0
- package/dist/scope/packages.js +87 -0
- package/dist/scope/rules.js +21 -0
- package/dist/scope/tracked.js +34 -0
- package/dist/scope/ts-lines.js +63 -0
- package/dist/scope/types.js +7 -0
- package/dist/scoring/aggregate.js +38 -0
- package/dist/scoring/pool.js +65 -0
- package/dist/scoring/score.js +81 -0
- package/dist/scoring/what-if.js +38 -0
- package/dist/scripts/checks.js +53 -0
- package/dist/scripts/ci.js +139 -0
- package/dist/scripts/entry-points/find.js +31 -0
- package/dist/scripts/entry-points/index.js +18 -0
- package/dist/scripts/entry-points/justfile.js +62 -0
- package/dist/scripts/entry-points/makefile.js +66 -0
- package/dist/scripts/entry-points/nox.js +29 -0
- package/dist/scripts/entry-points/package-json.js +21 -0
- package/dist/scripts/entry-points/pyproject.js +73 -0
- package/dist/scripts/entry-points/tox.js +130 -0
- package/dist/scripts/expand.js +300 -0
- package/dist/scripts/triggers.js +151 -0
- package/dist/scripts/types.js +1 -0
- package/dist/submit/constants.js +10 -0
- package/dist/submit/payload.js +42 -0
- package/dist/submit/repo-identity.js +46 -0
- package/dist/submit/send.js +29 -0
- package/dist/tools/catalog.js +46 -0
- package/dist/tools/image.js +180 -0
- package/dist/tools/kit.js +71 -0
- package/dist/tools/knip.js +169 -0
- package/dist/tools/outcome.js +21 -0
- package/dist/tools/resolve.js +66 -0
- package/dist/tools/workcopy.js +27 -0
- package/dist/util/ini.js +37 -0
- package/dist/util/pep440.js +163 -0
- package/dist/util/toml.js +27 -0
- package/docker/Dockerfile +142 -0
- package/docker/bin/health-capture +5 -0
- package/docker/bin/health-coverage-install +5 -0
- package/docker/bin/health-eslint +66 -0
- package/docker/bin/health-pytest +10 -0
- package/docker/bin/health-tsc +10 -0
- package/docker/bin/health-venv +11 -0
- package/docker/config/eslint.config.mjs +4 -0
- package/docker/config/eslint.rules.mjs +65 -0
- package/docker/config/eslint.untyped.config.mjs +4 -0
- package/docker/config/knip.json +1 -0
- package/docker/config/ruff.toml +7 -0
- package/methodology/LICENSE +396 -0
- package/methodology/v1.0/METHODOLOGY.md +1176 -0
- package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
- package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
- package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
- package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
- package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
- package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
- package/methodology/v1.0/methodology.yaml +242 -0
- package/methodology/v1.0/presets/django.yaml +43 -0
- package/methodology/v1.0/presets/express.yaml +47 -0
- package/methodology/v1.0/presets/fastapi.yaml +52 -0
- package/methodology/v1.0/presets/generic.yaml +41 -0
- package/methodology/v1.0/presets/nextjs.yaml +50 -0
- package/methodology/v1.0/scope.yaml +115 -0
- package/package.json +61 -4
- package/python/dead_code_helper.py +265 -0
- package/python/env_helper.py +71 -0
- package/python/graph_helper.py +277 -0
- package/python/imports_helper.py +132 -0
- package/python/nox_helper.py +79 -0
- package/python/scope_helper.py +96 -0
- package/python/tests_helper.py +171 -0
- package/python/typing_helper.py +235 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { packageOf } from '../scope/packages.js';
|
|
5
|
+
import { trackedPaths } from '../scope/tracked.js';
|
|
6
|
+
import { failureReason, unavailable } from '../tools/outcome.js';
|
|
7
|
+
import { createWorkCopy } from '../tools/workcopy.js';
|
|
8
|
+
import { scopeUnits } from './deps/units.js';
|
|
9
|
+
function parseFindings(text) {
|
|
10
|
+
if (text.trim() === '' || text.trim() === 'null')
|
|
11
|
+
return [];
|
|
12
|
+
try {
|
|
13
|
+
const json = JSON.parse(text);
|
|
14
|
+
return Array.isArray(json) ? json : undefined;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function relative(file, root) {
|
|
21
|
+
const posix = file.split(path.sep).join('/');
|
|
22
|
+
const rootPosix = root.split(path.sep).join('/');
|
|
23
|
+
const rel = posix.startsWith(`${rootPosix}/`) ? posix.slice(rootPosix.length + 1) : posix;
|
|
24
|
+
return path.posix.normalize(rel).replace(/^\.\//, '');
|
|
25
|
+
}
|
|
26
|
+
const JWT = /eyJ[\w-]+\.(eyJ[\w-]+)\.[\w-]+/;
|
|
27
|
+
/**
|
|
28
|
+
* Report only: the Supabase role a finding's line carries, or null. gitleaks runs with --redact, so the token is read
|
|
29
|
+
* from the work copy, only the role claim is kept, and neither the token nor the line is ever stored.
|
|
30
|
+
*/
|
|
31
|
+
export function supabaseRoleOf(line) {
|
|
32
|
+
if (/\bsb_secret_[\w-]+/.test(line))
|
|
33
|
+
return 'service_role';
|
|
34
|
+
if (/\bsb_publishable_[\w-]+/.test(line))
|
|
35
|
+
return 'anon';
|
|
36
|
+
const payload = JWT.exec(line)?.[1];
|
|
37
|
+
if (!payload)
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
41
|
+
if (typeof claims !== 'object' || claims === null)
|
|
42
|
+
return null;
|
|
43
|
+
const { iss, role } = claims;
|
|
44
|
+
return typeof iss === 'string' && iss.includes('supabase') && typeof role === 'string' ? role : null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function supabaseRole(dir, finding) {
|
|
51
|
+
if (typeof finding.File !== 'string' || typeof finding.StartLine !== 'number')
|
|
52
|
+
return null;
|
|
53
|
+
try {
|
|
54
|
+
const lines = (await readFile(path.join(dir, finding.File), 'utf8')).split('\n');
|
|
55
|
+
return supabaseRoleOf(lines[finding.StartLine - 1] ?? '');
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function scanWorkingTree(ctx, outDir, config) {
|
|
62
|
+
const tool = await ctx.tools.resolve('gitleaks');
|
|
63
|
+
if (!tool)
|
|
64
|
+
return unavailable(ctx.tools.notFoundReason('gitleaks'));
|
|
65
|
+
const copy = await createWorkCopy(ctx.repoPath, trackedPaths(ctx.repoPath));
|
|
66
|
+
try {
|
|
67
|
+
const report = path.join(outDir, 'tree.json');
|
|
68
|
+
// --exit-code 0: any non-zero exit means gitleaks itself failed.
|
|
69
|
+
const result = await ctx.tools.run(tool, ['dir', '.', '--config', config, '--report-format', 'json', '--report-path', report, '--exit-code', '0', '--redact', '--no-banner'], { cwd: copy.dir });
|
|
70
|
+
const failure = failureReason(tool, result, [0]);
|
|
71
|
+
if (failure)
|
|
72
|
+
return unavailable(failure);
|
|
73
|
+
let text;
|
|
74
|
+
try {
|
|
75
|
+
text = await readFile(report, 'utf8');
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return unavailable('gitleaks wrote no report');
|
|
79
|
+
}
|
|
80
|
+
const findings = parseFindings(text);
|
|
81
|
+
if (!findings)
|
|
82
|
+
return unavailable('gitleaks printed no parseable JSON report');
|
|
83
|
+
const relativeFindings = findings.map((f) => ({ ...f, File: typeof f.File === 'string' ? relative(f.File, copy.dir) : f.File }));
|
|
84
|
+
return Promise.all(relativeFindings.map(async (f) => ({ ...f, SupabaseRole: await supabaseRole(copy.dir, f) })));
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await copy.cleanup();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Report only: a failure is a warning and never makes the parameter not_measured. */
|
|
91
|
+
async function scanHistory(ctx, outDir, config) {
|
|
92
|
+
const tool = await ctx.tools.resolve('gitleaks');
|
|
93
|
+
if (!tool)
|
|
94
|
+
return null;
|
|
95
|
+
const report = path.join(outDir, 'history.json');
|
|
96
|
+
const result = await ctx.tools.run(tool, ['git', path.resolve(ctx.repoPath), '--config', config, '--report-format', 'json', '--report-path', report, '--exit-code', '0', '--redact', '--no-banner'], { cwd: outDir });
|
|
97
|
+
if (failureReason(tool, result, [0]) !== null)
|
|
98
|
+
return null;
|
|
99
|
+
let findings;
|
|
100
|
+
try {
|
|
101
|
+
findings = parseFindings(await readFile(report, 'utf8'));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (!findings)
|
|
107
|
+
return null;
|
|
108
|
+
return {
|
|
109
|
+
count: findings.length,
|
|
110
|
+
findings: findings.map((f) => ({ ruleId: f.RuleID, file: f.File, commit: f.Commit, line: f.StartLine })),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export const p54 = {
|
|
114
|
+
id: '5.4',
|
|
115
|
+
layer: 'static',
|
|
116
|
+
async measure(ctx) {
|
|
117
|
+
const outDir = await mkdtemp(path.join(tmpdir(), 'health-gitleaks-'));
|
|
118
|
+
try {
|
|
119
|
+
// An explicit config: the pinned default ruleset applies and no .gitleaks.toml is read.
|
|
120
|
+
const config = path.join(outDir, 'gitleaks.toml');
|
|
121
|
+
await writeFile(config, '[extend]\nuseDefault = true\n');
|
|
122
|
+
const findings = await scanWorkingTree(ctx, outDir, config);
|
|
123
|
+
if (!Array.isArray(findings))
|
|
124
|
+
return findings;
|
|
125
|
+
const units = scopeUnits(ctx);
|
|
126
|
+
const counts = new Map(units.map((u) => [`${u.package}\0${u.group}`, 0]));
|
|
127
|
+
for (const finding of findings) {
|
|
128
|
+
if (typeof finding.File !== 'string')
|
|
129
|
+
continue;
|
|
130
|
+
const owner = packageOf(finding.File, ctx.scope.packages);
|
|
131
|
+
const owned = units.filter((u) => u.package === owner);
|
|
132
|
+
for (const unit of owned.length > 0 ? owned : units) {
|
|
133
|
+
const key = `${unit.package}\0${unit.group}`;
|
|
134
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const warnings = [];
|
|
138
|
+
const history = await scanHistory(ctx, outDir, config);
|
|
139
|
+
if (history === null)
|
|
140
|
+
warnings.push('5.4: git history scan failed');
|
|
141
|
+
const result = units.map((u) => ({
|
|
142
|
+
package: u.package,
|
|
143
|
+
group: u.group,
|
|
144
|
+
weight: u.weight,
|
|
145
|
+
raw: { kind: 'number', value: counts.get(`${u.package}\0${u.group}`) ?? 0 },
|
|
146
|
+
}));
|
|
147
|
+
return {
|
|
148
|
+
status: 'measured',
|
|
149
|
+
units: result,
|
|
150
|
+
warnings,
|
|
151
|
+
reportOnly: {
|
|
152
|
+
secretsInTree: findings.map((f) => ({ ruleId: f.RuleID, file: f.File, line: f.StartLine, supabaseRole: f.SupabaseRole ?? null })),
|
|
153
|
+
...(history === null ? {} : { secretsInHistory: history }),
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
await rm(outDir, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile, readFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { stringify } from 'smol-toml';
|
|
5
|
+
import { packageOf } from '../scope/packages.js';
|
|
6
|
+
import { isOutcome, knipFailed, runKnip } from '../tools/knip.js';
|
|
7
|
+
import { failureReason, unavailable } from '../tools/outcome.js';
|
|
8
|
+
import { createWorkCopy } from '../tools/workcopy.js';
|
|
9
|
+
import { readToml } from '../util/toml.js';
|
|
10
|
+
import { requirementFiles } from './deps/declared.js';
|
|
11
|
+
import { joinRel, trackedSet } from './deps/files.js';
|
|
12
|
+
import { unitDeps } from './deps/resolved.js';
|
|
13
|
+
import { scopeUnits } from './deps/units.js';
|
|
14
|
+
function knipCounts(ctx, knip, pkg) {
|
|
15
|
+
const names = new Set();
|
|
16
|
+
const manifest = joinRel(pkg, 'package.json');
|
|
17
|
+
for (const [file, issues] of knip.files) {
|
|
18
|
+
if (file === manifest) {
|
|
19
|
+
for (const name of [...issues.dependencies, ...issues.devDependencies])
|
|
20
|
+
names.add(name);
|
|
21
|
+
}
|
|
22
|
+
if (packageOf(file, ctx.scope.packages) === pkg) {
|
|
23
|
+
for (const name of issues.unlisted)
|
|
24
|
+
names.add(`unlisted\0${name}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return names.size;
|
|
28
|
+
}
|
|
29
|
+
const DEPTRY_CODES = new Set(['DEP001', 'DEP002', 'DEP003']);
|
|
30
|
+
/** deptry found no section or file it reads dependencies from (a pyproject.toml without [project], poetry or pdm tables, and no requirements.txt). */
|
|
31
|
+
const NO_SPEC = 'no-spec';
|
|
32
|
+
/** deptry on a work copy of the unit with the repository's [tool.deptry] settings removed. */
|
|
33
|
+
async function deptryCount(ctx, tools, pkg) {
|
|
34
|
+
const tool = await tools.resolve('deptry');
|
|
35
|
+
if (!tool)
|
|
36
|
+
return unavailable(tools.notFoundReason('deptry'));
|
|
37
|
+
const dir = pkg === '.' ? '' : pkg;
|
|
38
|
+
const pythonFiles = [...trackedSet(ctx.repoPath)].filter((f) => f.endsWith('.py') && (dir === '' || f.startsWith(`${dir}/`)) && packageOf(f, ctx.scope.packages) === pkg);
|
|
39
|
+
const extra = [...requirementFiles(ctx.repoPath, pkg)];
|
|
40
|
+
const pipfile = joinRel(dir, 'Pipfile');
|
|
41
|
+
if (trackedSet(ctx.repoPath).has(pipfile))
|
|
42
|
+
extra.push(pipfile);
|
|
43
|
+
const copy = await createWorkCopy(ctx.repoPath, [...pythonFiles, ...extra]);
|
|
44
|
+
const outDir = await mkdtemp(path.join(tmpdir(), 'health-deptry-'));
|
|
45
|
+
try {
|
|
46
|
+
const pyprojectRel = joinRel(dir, 'pyproject.toml');
|
|
47
|
+
if (trackedSet(ctx.repoPath).has(pyprojectRel)) {
|
|
48
|
+
const doc = readToml(path.join(ctx.repoPath, pyprojectRel));
|
|
49
|
+
if (doc) {
|
|
50
|
+
const toolTable = doc.tool;
|
|
51
|
+
if (toolTable !== null && typeof toolTable === 'object' && !Array.isArray(toolTable)) {
|
|
52
|
+
delete toolTable.deptry;
|
|
53
|
+
}
|
|
54
|
+
await writeFile(path.join(copy.dir, pyprojectRel), stringify(doc));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const report = path.join(outDir, 'deptry.json');
|
|
58
|
+
const result = await tools.run(tool, ['.', '--json-output', report, '--no-ansi'], { cwd: path.join(copy.dir, dir) });
|
|
59
|
+
const failure = failureReason(tool, result, [0, 1]);
|
|
60
|
+
if (failure)
|
|
61
|
+
return unavailable(failure);
|
|
62
|
+
let text = '[]';
|
|
63
|
+
try {
|
|
64
|
+
text = await readFile(report, 'utf8');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
if (result.exitCode === 1 && `${result.stdout}${result.stderr}`.includes('DependencySpecificationNotFoundError'))
|
|
68
|
+
return NO_SPEC;
|
|
69
|
+
if (result.exitCode === 1)
|
|
70
|
+
return unavailable('deptry wrote no report');
|
|
71
|
+
}
|
|
72
|
+
let violations;
|
|
73
|
+
try {
|
|
74
|
+
violations = JSON.parse(text.trim() === '' ? '[]' : text);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return unavailable('deptry printed no parseable JSON report');
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(violations))
|
|
80
|
+
return unavailable('deptry report has an unexpected shape');
|
|
81
|
+
const pairs = new Set();
|
|
82
|
+
for (const entry of violations) {
|
|
83
|
+
const code = entry.error?.code;
|
|
84
|
+
if (typeof code === 'string' && DEPTRY_CODES.has(code))
|
|
85
|
+
pairs.add(`${code}\0${String(entry.module)}`);
|
|
86
|
+
}
|
|
87
|
+
return pairs.size;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
await copy.cleanup();
|
|
91
|
+
await rm(outDir, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export const p55 = {
|
|
95
|
+
id: '5.5',
|
|
96
|
+
layer: 'static',
|
|
97
|
+
async measure(ctx) {
|
|
98
|
+
const units = scopeUnits(ctx);
|
|
99
|
+
const warnings = [];
|
|
100
|
+
const counts = new Map();
|
|
101
|
+
const failed = new Set();
|
|
102
|
+
if (units.some((u) => u.group === 'typescript_javascript')) {
|
|
103
|
+
const knip = await runKnip(ctx);
|
|
104
|
+
if (!isOutcome(knip))
|
|
105
|
+
return knip;
|
|
106
|
+
warnings.push(...knip.warnings.map((w) => `5.5: ${w}`));
|
|
107
|
+
for (const unit of units.filter((u) => u.group === 'typescript_javascript')) {
|
|
108
|
+
const unitFiles = ctx.scope.files.filter((f) => f.role === 'logic' && f.group === unit.group && f.package === unit.package);
|
|
109
|
+
if (knipFailed(knip, unit.package) || unitFiles.some((f) => knipFailed(knip, f.path))) {
|
|
110
|
+
failed.add(`${unit.package}\0${unit.group}`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const deps = await unitDeps(ctx, unit.package, unit.group);
|
|
114
|
+
if (!deps.manifest) {
|
|
115
|
+
warnings.push(`5.5: ${unit.package} has no package.json`);
|
|
116
|
+
counts.set(`${unit.package}\0${unit.group}`, { numerator: 0, denominator: 0 });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const denominator = deps.manifest.deps.filter((d) => d.optional !== true).length;
|
|
120
|
+
counts.set(`${unit.package}\0${unit.group}`, { numerator: knipCounts(ctx, knip, unit.package), denominator });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const unit of units.filter((u) => u.group === 'python')) {
|
|
124
|
+
const deps = await unitDeps(ctx, unit.package, unit.group);
|
|
125
|
+
// deptry exits 1 with no report when there is nothing to read dependencies from, so it is not run.
|
|
126
|
+
if (!deps.manifest) {
|
|
127
|
+
warnings.push(`5.5: ${unit.package} has no Python dependency manifest`);
|
|
128
|
+
counts.set(`${unit.package}\0${unit.group}`, { numerator: 0, denominator: 0 });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const denominator = new Set((deps.manifest?.deps ?? []).map((d) => d.name)).size;
|
|
132
|
+
if (denominator === 0)
|
|
133
|
+
warnings.push(`5.5: ${unit.package} declares no dependencies`);
|
|
134
|
+
const numerator = await deptryCount(ctx, ctx.tools, unit.package);
|
|
135
|
+
if (numerator === NO_SPEC) {
|
|
136
|
+
// Nothing declared and nothing deptry can read: the same 0/0 as a unit with no manifest.
|
|
137
|
+
if (denominator > 0)
|
|
138
|
+
return unavailable(`deptry cannot read the dependencies declared in ${deps.manifest.file}`);
|
|
139
|
+
warnings.push(`5.5: deptry found no dependency specification in ${unit.package}`);
|
|
140
|
+
counts.set(`${unit.package}\0${unit.group}`, { numerator: 0, denominator: 0 });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (typeof numerator !== 'number')
|
|
144
|
+
return numerator;
|
|
145
|
+
counts.set(`${unit.package}\0${unit.group}`, { numerator, denominator });
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
status: 'measured',
|
|
149
|
+
units: units.map((u) => {
|
|
150
|
+
if (failed.has(`${u.package}\0${u.group}`))
|
|
151
|
+
return { package: u.package, group: u.group, weight: u.weight, raw: { kind: 'failed' } };
|
|
152
|
+
const c = counts.get(`${u.package}\0${u.group}`) ?? { numerator: 0, denominator: 0 };
|
|
153
|
+
return { package: u.package, group: u.group, weight: u.weight, raw: { kind: 'ratio', numerator: c.numerator, denominator: c.denominator, scale: 100 } };
|
|
154
|
+
}),
|
|
155
|
+
warnings,
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** install_fails when the install did not work; build_fails when it did and the build did not; works otherwise (no build step counts as works). */
|
|
2
|
+
function installLevel(root) {
|
|
3
|
+
if (root.install && root.install.status !== 'ok')
|
|
4
|
+
return 'install_fails';
|
|
5
|
+
if (root.build && root.build.status !== 'ok')
|
|
6
|
+
return 'build_fails';
|
|
7
|
+
return 'works';
|
|
8
|
+
}
|
|
9
|
+
export const p61 = {
|
|
10
|
+
id: '6.1',
|
|
11
|
+
layer: 'execution',
|
|
12
|
+
async measure({ exec }) {
|
|
13
|
+
if (!exec)
|
|
14
|
+
return { status: 'not_measured', reason: 'execution layer did not run' };
|
|
15
|
+
return {
|
|
16
|
+
status: 'measured',
|
|
17
|
+
units: exec.roots.map((root) => ({ package: root.dir, group: root.group, weight: root.weight, raw: { kind: 'level', level: installLevel(root) } })),
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { EntryPointChecks } from './ops/entry-points.js';
|
|
2
|
+
import { logicUnits } from './units.js';
|
|
3
|
+
export const p62 = {
|
|
4
|
+
id: '6.2',
|
|
5
|
+
layer: 'static',
|
|
6
|
+
async measure({ scope, repoPath, methodology }) {
|
|
7
|
+
const checks = new EntryPointChecks(repoPath, methodology.d6);
|
|
8
|
+
const findings = new Set();
|
|
9
|
+
const units = logicUnits(scope).map((unit) => {
|
|
10
|
+
const candidates = checks.forUnit(unit.dir);
|
|
11
|
+
for (const entry of candidates) {
|
|
12
|
+
if (entry.unrecognised.length > 0)
|
|
13
|
+
findings.add(`6.2: ${entry.name} calls ${entry.unrecognised.join(', ')}, which is not a recognised tool`);
|
|
14
|
+
}
|
|
15
|
+
const level = Math.max(0, ...candidates.map((c) => c.kinds.size));
|
|
16
|
+
return { package: unit.package, group: unit.group, weight: unit.weight, raw: { kind: 'level', level: String(level) } };
|
|
17
|
+
});
|
|
18
|
+
return { status: 'measured', units, ...(findings.size === 0 ? {} : { warnings: [...findings].sort() }) };
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { failure as checkFailure } from '../exec/findings.js';
|
|
2
|
+
import { withFailedUnits } from '../exec/roots.js';
|
|
3
|
+
import { testFailure, totalCases } from '../exec/tests.js';
|
|
4
|
+
import { emptyTallies } from './units.js';
|
|
5
|
+
const seconds = (outcomes) => outcomes.reduce((sum, outcome) => sum + outcome.durationMs, 0) / 1000;
|
|
6
|
+
/** The three step durations in seconds, full precision, and their sum. Steps that did not run count 0. The coverage provider install is not one of the three. */
|
|
7
|
+
export function feedbackLoop(exec) {
|
|
8
|
+
const typecheckSeconds = seconds(exec.typecheck.map((r) => r.outcome));
|
|
9
|
+
const lintSeconds = seconds(exec.lint.map((r) => r.outcome));
|
|
10
|
+
const testSeconds = seconds(exec.test.invocations.map((r) => r.outcome));
|
|
11
|
+
return { typecheckSeconds, lintSeconds, testSeconds, totalSeconds: typecheckSeconds + lintSeconds + testSeconds };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Summed wall-clock seconds of every typecheck, lint and test step. Every (package, group) unit carries the
|
|
15
|
+
* same sum, weighted by logic lines, so the pooled score is the repository's. Failed unless typecheck, lint
|
|
16
|
+
* and the test run all worked and some install root installed (`requires` in the YAML); units beneath a root
|
|
17
|
+
* whose install failed score 0.
|
|
18
|
+
*/
|
|
19
|
+
export const p63 = {
|
|
20
|
+
id: '6.3',
|
|
21
|
+
layer: 'execution',
|
|
22
|
+
async measure(ctx) {
|
|
23
|
+
const exec = ctx.exec;
|
|
24
|
+
if (!exec)
|
|
25
|
+
return { status: 'not_measured', reason: 'execution layer did not run' };
|
|
26
|
+
const stopped = testFailure(exec) ?? [...exec.typecheck, ...exec.lint].map(checkFailure).find((reason) => reason !== null) ?? null;
|
|
27
|
+
if (stopped !== null)
|
|
28
|
+
return { status: 'failed', reason: stopped };
|
|
29
|
+
if (exec.test.invocations.length === 0)
|
|
30
|
+
return { status: 'failed', reason: 'no supported test runner was found' };
|
|
31
|
+
if (totalCases(exec) === 0)
|
|
32
|
+
return { status: 'failed', reason: 'the test runner ran no test case' };
|
|
33
|
+
const value = feedbackLoop(exec).totalSeconds;
|
|
34
|
+
const logic = ctx.scope.files.filter((f) => f.role === 'logic');
|
|
35
|
+
const units = [...emptyTallies(logic).values()]
|
|
36
|
+
.sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group))
|
|
37
|
+
.map((t) => ({ package: t.package, group: t.group, weight: t.weight, raw: { kind: 'number', value } }));
|
|
38
|
+
return { status: 'measured', units: withFailedUnits(exec, ctx.scope, units) };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { checkKinds } from '../scripts/checks.js';
|
|
2
|
+
import { ciCommands } from '../scripts/ci.js';
|
|
3
|
+
import { expandCommand } from '../scripts/expand.js';
|
|
4
|
+
import { dirsUpward } from './ops/entry-points.js';
|
|
5
|
+
import { logicUnits } from './units.js';
|
|
6
|
+
export const p64 = {
|
|
7
|
+
id: '6.4',
|
|
8
|
+
layer: 'static',
|
|
9
|
+
async measure({ scope, repoPath, methodology }) {
|
|
10
|
+
const warnings = [];
|
|
11
|
+
const commands = ciCommands(repoPath, warnings).filter((c) => c.triggers?.qualifies === true);
|
|
12
|
+
// A command counts for the units at or below its working directory; expand each once.
|
|
13
|
+
const byDir = new Map();
|
|
14
|
+
for (const command of commands) {
|
|
15
|
+
const kinds = byDir.get(command.dir) ?? new Set();
|
|
16
|
+
for (const resolved of expandCommand(repoPath, command.command, { dir: command.dir, file: command.file }, { maxDepth: 3 })) {
|
|
17
|
+
for (const kind of checkKinds(resolved, methodology.d6))
|
|
18
|
+
kinds.add(kind);
|
|
19
|
+
}
|
|
20
|
+
byDir.set(command.dir, kinds);
|
|
21
|
+
}
|
|
22
|
+
const units = logicUnits(scope).map((unit) => {
|
|
23
|
+
const kinds = new Set();
|
|
24
|
+
for (const dir of dirsUpward(unit.dir))
|
|
25
|
+
for (const kind of byDir.get(dir) ?? [])
|
|
26
|
+
kinds.add(kind);
|
|
27
|
+
return { package: unit.package, group: unit.group, weight: unit.weight, raw: { kind: 'level', level: String(kinds.size) } };
|
|
28
|
+
});
|
|
29
|
+
return { status: 'measured', units, ...(warnings.length === 0 ? {} : { warnings: warnings.map((w) => `6.4: ${w}`) }) };
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pythonEnvReads } from '../python/env.js';
|
|
3
|
+
import { readTracked } from '../scope/tracked.js';
|
|
4
|
+
import { dirsUpward } from './ops/entry-points.js';
|
|
5
|
+
import { parseEnvExample } from './ops/env-example.js';
|
|
6
|
+
import { isExcludedEnvName, tsEnvReads } from './ops/env-reads.js';
|
|
7
|
+
import { runtimePinned } from './ops/runtime.js';
|
|
8
|
+
import { logicUnits } from './units.js';
|
|
9
|
+
export const p65 = {
|
|
10
|
+
id: '6.5',
|
|
11
|
+
layer: 'static',
|
|
12
|
+
async measure({ scope, repoPath, methodology }) {
|
|
13
|
+
const lists = methodology.d6;
|
|
14
|
+
const warnings = [];
|
|
15
|
+
const pyFiles = scope.files.filter((f) => f.group === 'python' && f.role === 'logic').map((f) => f.path);
|
|
16
|
+
const pyReads = pyFiles.length === 0 ? {} : pythonEnvReads(repoPath, pyFiles);
|
|
17
|
+
const units = logicUnits(scope).map((unit) => {
|
|
18
|
+
const read = new Set();
|
|
19
|
+
for (const file of unit.files) {
|
|
20
|
+
if (unit.group === 'python') {
|
|
21
|
+
const found = pyReads[file.path];
|
|
22
|
+
if (found?.ok)
|
|
23
|
+
for (const name of found.names)
|
|
24
|
+
read.add(name);
|
|
25
|
+
else
|
|
26
|
+
warnings.push(`6.5: ${file.path}: syntax error, its environment reads are not counted`);
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
for (const name of tsEnvReads(scope.getTsSourceFile(file.path)))
|
|
30
|
+
read.add(name);
|
|
31
|
+
}
|
|
32
|
+
const names = [...read].filter((name) => !isExcludedEnvName(name, lists));
|
|
33
|
+
let documented = new Set();
|
|
34
|
+
search: for (const dir of dirsUpward(unit.dir)) {
|
|
35
|
+
for (const name of lists.envExampleFiles) {
|
|
36
|
+
const text = readTracked(repoPath, path.posix.join(dir, name));
|
|
37
|
+
if (text !== null) {
|
|
38
|
+
documented = new Set(Object.keys(parseEnvExample(text)));
|
|
39
|
+
break search;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const numerator = names.filter((name) => documented.has(name)).length;
|
|
44
|
+
const pinned = runtimePinned(repoPath, unit.dir, unit.group, lists.runtimePinFiles, warnings);
|
|
45
|
+
return {
|
|
46
|
+
package: unit.package,
|
|
47
|
+
group: unit.group,
|
|
48
|
+
weight: unit.weight,
|
|
49
|
+
raw: {
|
|
50
|
+
kind: 'components',
|
|
51
|
+
components: {
|
|
52
|
+
runtime_pinned: { kind: 'level', level: pinned ? 'yes' : 'no' },
|
|
53
|
+
env_documented: { kind: 'ratio', numerator, denominator: names.length, scale: 100, ...(names.length === 0 ? { emptyScore: 100 } : {}) },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
return { status: 'measured', units, ...(warnings.length === 0 ? {} : { warnings: [...new Set(warnings)] }) };
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readTracked, trackedPaths } from '../scope/tracked.js';
|
|
3
|
+
import { extractCommands } from './ops/docs-commands.js';
|
|
4
|
+
import { DocResolver } from './ops/docs-resolve.js';
|
|
5
|
+
import { logicUnits } from './units.js';
|
|
6
|
+
const AGENT_FILES = ['agents.md', 'claude.md'];
|
|
7
|
+
const README_NAMES = ['readme.md', 'readme', 'readme.rst', 'readme.txt'];
|
|
8
|
+
/** The tracked file of `dir` whose name matches one of `names` case-insensitively, in the order of `names`. */
|
|
9
|
+
function findCi(tracked, dir, names) {
|
|
10
|
+
const inDir = tracked.filter((f) => path.posix.dirname(f) === dir);
|
|
11
|
+
for (const name of names) {
|
|
12
|
+
const found = inDir.find((f) => path.posix.basename(f).toLowerCase() === name);
|
|
13
|
+
if (found)
|
|
14
|
+
return found;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
export const p66 = {
|
|
19
|
+
id: '6.6',
|
|
20
|
+
layer: 'static',
|
|
21
|
+
async measure({ scope, repoPath, methodology }) {
|
|
22
|
+
const lists = methodology.d6;
|
|
23
|
+
const tracked = trackedPaths(repoPath);
|
|
24
|
+
const words = {
|
|
25
|
+
commandWords: new Set([
|
|
26
|
+
...lists.docPackageManagers,
|
|
27
|
+
...[...lists.typecheckers, ...lists.linters, ...lists.testRunners].map((t) => (t === 'node --test' ? 'node' : t.replace(/^python3? -m /, ''))),
|
|
28
|
+
]),
|
|
29
|
+
};
|
|
30
|
+
const resolver = new DocResolver(repoPath, lists);
|
|
31
|
+
const commandsOf = (file) => {
|
|
32
|
+
const text = file === undefined ? null : readTracked(repoPath, file);
|
|
33
|
+
return text === null ? [] : extractCommands(text, words);
|
|
34
|
+
};
|
|
35
|
+
const agentFiles = AGENT_FILES.flatMap((name) => findCi(tracked, '.', [name]) ?? []);
|
|
36
|
+
const agent = agentFiles.flatMap((file) => commandsOf(file).map((command) => ({ file, command })));
|
|
37
|
+
const unresolved = agent.filter(({ command }) => !resolver.resolves(command, '.'));
|
|
38
|
+
const warnings = unresolved.slice(0, 5).map(({ file, command }) => `6.6: ${file}:${command.line}: \`${command.text}\` does not resolve`);
|
|
39
|
+
const rootReadme = findCi(tracked, '.', README_NAMES);
|
|
40
|
+
const rootReadmeHasCommands = commandsOf(rootReadme).length > 0;
|
|
41
|
+
const units = logicUnits(scope).map((unit) => {
|
|
42
|
+
let level;
|
|
43
|
+
if (agent.length > 0)
|
|
44
|
+
level = unresolved.length === 0 ? 'agent_file' : 'readme';
|
|
45
|
+
else {
|
|
46
|
+
const own = unit.dir === '.' ? undefined : findCi(tracked, unit.dir, README_NAMES);
|
|
47
|
+
const hasCommands = own !== undefined && commandsOf(own).length > 0 ? true : rootReadmeHasCommands;
|
|
48
|
+
level = hasCommands ? 'readme' : 'none';
|
|
49
|
+
}
|
|
50
|
+
return { package: unit.package, group: unit.group, weight: unit.weight, raw: { kind: 'level', level } };
|
|
51
|
+
});
|
|
52
|
+
return { status: 'measured', units, ...(warnings.length === 0 ? {} : { warnings }) };
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { parseIni } from '../../util/ini.js';
|
|
5
|
+
import { readToml } from '../../util/toml.js';
|
|
6
|
+
function asRecord(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
function truthy(value) {
|
|
10
|
+
if (typeof value === 'boolean')
|
|
11
|
+
return value;
|
|
12
|
+
return typeof value === 'string' && ['true', '1', 'yes', 'on'].includes(value.trim().toLowerCase());
|
|
13
|
+
}
|
|
14
|
+
function pyrightLevel(mode, unsetMeans, basedpyright) {
|
|
15
|
+
if (typeof mode !== 'string')
|
|
16
|
+
return unsetMeans;
|
|
17
|
+
switch (mode) {
|
|
18
|
+
case 'off':
|
|
19
|
+
return 'none';
|
|
20
|
+
case 'basic':
|
|
21
|
+
return 'basic';
|
|
22
|
+
case 'standard':
|
|
23
|
+
return 'standard';
|
|
24
|
+
case 'strict':
|
|
25
|
+
return 'strict';
|
|
26
|
+
case 'recommended':
|
|
27
|
+
case 'all':
|
|
28
|
+
return basedpyright ? 'strict' : unsetMeans;
|
|
29
|
+
default:
|
|
30
|
+
return unsetMeans;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function mypyLevel(section) {
|
|
34
|
+
if (truthy(section.strict))
|
|
35
|
+
return 'strict';
|
|
36
|
+
if (truthy(section.disallow_untyped_defs) && truthy(section.check_untyped_defs))
|
|
37
|
+
return 'standard';
|
|
38
|
+
return 'basic';
|
|
39
|
+
}
|
|
40
|
+
function readText(file) {
|
|
41
|
+
try {
|
|
42
|
+
return readFileSync(file, 'utf8');
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** The level one directory declares, or undefined when it holds no pyright or mypy config. pyright wins over mypy. */
|
|
49
|
+
function levelInDirectory(dir, relDir, warnings) {
|
|
50
|
+
const rel = (name) => (relDir === '' ? name : `${relDir}/${name}`);
|
|
51
|
+
const pyproject = existsSync(path.join(dir, 'pyproject.toml')) ? readToml(path.join(dir, 'pyproject.toml')) : undefined;
|
|
52
|
+
if (existsSync(path.join(dir, 'pyproject.toml')) && pyproject === undefined) {
|
|
53
|
+
warnings.push(`2.2: ${rel('pyproject.toml')}: invalid TOML, ignored`);
|
|
54
|
+
}
|
|
55
|
+
const tool = asRecord(pyproject?.tool);
|
|
56
|
+
const pyrightJson = readText(path.join(dir, 'pyrightconfig.json'));
|
|
57
|
+
if (pyrightJson !== undefined) {
|
|
58
|
+
const parsed = ts.parseConfigFileTextToJson('pyrightconfig.json', pyrightJson);
|
|
59
|
+
if (parsed.error)
|
|
60
|
+
warnings.push(`2.2: ${rel('pyrightconfig.json')}: invalid JSON, ignored`);
|
|
61
|
+
else
|
|
62
|
+
return pyrightLevel(asRecord(parsed.config)?.typeCheckingMode, 'standard', false);
|
|
63
|
+
}
|
|
64
|
+
const pyright = asRecord(tool?.pyright);
|
|
65
|
+
if (pyright)
|
|
66
|
+
return pyrightLevel(pyright.typeCheckingMode, 'standard', false);
|
|
67
|
+
const basedpyright = asRecord(tool?.basedpyright);
|
|
68
|
+
if (basedpyright)
|
|
69
|
+
return pyrightLevel(basedpyright.typeCheckingMode, 'strict', true);
|
|
70
|
+
for (const name of ['mypy.ini', '.mypy.ini']) {
|
|
71
|
+
const text = readText(path.join(dir, name));
|
|
72
|
+
const section = text === undefined ? undefined : parseIni(text).mypy;
|
|
73
|
+
if (section)
|
|
74
|
+
return mypyLevel(section);
|
|
75
|
+
}
|
|
76
|
+
const mypyToml = asRecord(tool?.mypy);
|
|
77
|
+
if (mypyToml)
|
|
78
|
+
return mypyLevel(mypyToml);
|
|
79
|
+
const setupCfg = readText(path.join(dir, 'setup.cfg'));
|
|
80
|
+
const cfgSection = setupCfg === undefined ? undefined : parseIni(setupCfg).mypy;
|
|
81
|
+
if (cfgSection)
|
|
82
|
+
return mypyLevel(cfgSection);
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/** Search from the unit's package directory up to the repository root; the first directory with a config decides. */
|
|
86
|
+
export function pythonLevel(repoPath, packageDir, warnings) {
|
|
87
|
+
const parts = packageDir === '.' ? [] : packageDir.split('/');
|
|
88
|
+
for (let depth = parts.length; depth >= 0; depth -= 1) {
|
|
89
|
+
const relDir = parts.slice(0, depth).join('/');
|
|
90
|
+
const level = levelInDirectory(path.join(repoPath, relDir), relDir, warnings);
|
|
91
|
+
if (level !== undefined)
|
|
92
|
+
return level;
|
|
93
|
+
}
|
|
94
|
+
return 'none';
|
|
95
|
+
}
|