vibecodedshit 0.0.0-stage → 0.2.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 +64 -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 +8 -0
- package/dist/card/previous.js +38 -0
- package/dist/card/render.js +71 -0
- package/dist/cli.js +121 -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 +25 -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/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 +9 -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,5 @@
|
|
|
1
|
+
import { emptyTallies, logicFiles } from '../units.js';
|
|
2
|
+
/** Scope units: every (package, language group) with logic files, weight = its logic lines. */
|
|
3
|
+
export function scopeUnits(ctx) {
|
|
4
|
+
return [...emptyTallies([...logicFiles(ctx.scope, 'typescript_javascript'), ...logicFiles(ctx.scope, 'python')]).values()].sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group));
|
|
5
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports once for every function node (declarations, expressions, arrows, methods).
|
|
3
|
+
* `sonarjs/cognitive-complexity` registers on the same `:function` selector, so this is the
|
|
4
|
+
* denominator for 4.1: all functions as enumerated by the tool.
|
|
5
|
+
*/
|
|
6
|
+
const functionRule = {
|
|
7
|
+
meta: { type: 'problem', schema: [], messages: { fn: 'function' } },
|
|
8
|
+
create(context) {
|
|
9
|
+
return {
|
|
10
|
+
':function'(node) {
|
|
11
|
+
context.report({ node, messageId: 'fn' });
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
export const healthPlugin = {
|
|
17
|
+
meta: { name: 'health' },
|
|
18
|
+
rules: { function: functionRule },
|
|
19
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { splitSimple, tokenize } from '../../scripts/expand.js';
|
|
2
|
+
const ALIASES = { bunx: ['npx'], uvx: ['uv', 'run'], pip3: ['pip'], python3: ['python'] };
|
|
3
|
+
const SKIPPED = new Set(['sudo']);
|
|
4
|
+
/** First word of a simple command after `VAR=value` words and `sudo`; aliases (`bunx`, `uvx`, `pip3`, `python3`) map to their base word. */
|
|
5
|
+
function leadWord(tokens) {
|
|
6
|
+
let rest = tokens;
|
|
7
|
+
while (rest[0] !== undefined && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(rest[0]) || SKIPPED.has(rest[0])))
|
|
8
|
+
rest = rest.slice(1);
|
|
9
|
+
const first = rest[0];
|
|
10
|
+
if (first === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
const alias = ALIASES[first];
|
|
13
|
+
if (alias)
|
|
14
|
+
rest = [...alias, ...rest.slice(1)];
|
|
15
|
+
return { word: rest[0], rest };
|
|
16
|
+
}
|
|
17
|
+
/** Candidates from fenced blocks (every line) and inline code spans (one per span), before splitting into simple commands. */
|
|
18
|
+
function candidates(text) {
|
|
19
|
+
const out = [];
|
|
20
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
21
|
+
let fence;
|
|
22
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
23
|
+
const line = lines[i] ?? '';
|
|
24
|
+
const marker = /^\s*(```+|~~~+)/.exec(line)?.[1];
|
|
25
|
+
if (marker !== undefined) {
|
|
26
|
+
fence = fence === undefined ? marker[0] : fence === marker[0] ? undefined : fence;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (fence === undefined) {
|
|
30
|
+
for (const span of line.matchAll(/`+([^`\n]+)`+/g))
|
|
31
|
+
out.push({ line: i + 1, text: (span[1] ?? '').trim(), inline: true });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
let candidate = line.trim();
|
|
35
|
+
const start = i + 1;
|
|
36
|
+
while (candidate.endsWith('\\') && i + 1 < lines.length) {
|
|
37
|
+
i += 1;
|
|
38
|
+
candidate = `${candidate.slice(0, -1).trimEnd()} ${(lines[i] ?? '').trim()}`;
|
|
39
|
+
}
|
|
40
|
+
candidate = candidate.replace(/^[$>]\s+/, '');
|
|
41
|
+
if (candidate === '' || candidate.startsWith('#'))
|
|
42
|
+
continue;
|
|
43
|
+
out.push({ line: start, text: candidate, inline: false });
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The documented commands of a file. A command is a simple command whose first word is a package manager or recognised tool.
|
|
49
|
+
* An inline span is a candidate only when it has a space (`npm` alone is not a command; `package.json` and `src/index.ts` are not).
|
|
50
|
+
*/
|
|
51
|
+
export function extractCommands(text, words) {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const candidate of candidates(text)) {
|
|
54
|
+
if (candidate.inline && !/\s/.test(candidate.text))
|
|
55
|
+
continue;
|
|
56
|
+
let cd = '.';
|
|
57
|
+
for (const simple of splitSimple(candidate.text)) {
|
|
58
|
+
const tokens = tokenize(simple.text);
|
|
59
|
+
const lead = leadWord(tokens);
|
|
60
|
+
if (!lead)
|
|
61
|
+
continue;
|
|
62
|
+
if (lead.word === 'cd' && lead.rest[1] !== undefined && !/[$`*?]/.test(lead.rest[1])) {
|
|
63
|
+
if (simple.sep === '&&')
|
|
64
|
+
cd = cd === '.' ? lead.rest[1].replace(/^\.\//, '') : `${cd}/${lead.rest[1]}`;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!words.commandWords.has(lead.word))
|
|
68
|
+
continue;
|
|
69
|
+
out.push({ line: candidate.line, text: simple.text.trim(), cd, tokens: [lead.word, ...lead.rest.slice(1)] });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readPackageJson } from '../../manifest/package-json.js';
|
|
3
|
+
import { pythonDeclaredDeps } from '../../manifest/python.js';
|
|
4
|
+
import { trackedPaths } from '../../scope/tracked.js';
|
|
5
|
+
import { findFile } from '../../scripts/entry-points/find.js';
|
|
6
|
+
import { justfileEntryPoints } from '../../scripts/entry-points/justfile.js';
|
|
7
|
+
import { makefileEntryPoints } from '../../scripts/entry-points/makefile.js';
|
|
8
|
+
import { noxEntryPoints } from '../../scripts/entry-points/nox.js';
|
|
9
|
+
import { packageJsonEntryPoints } from '../../scripts/entry-points/package-json.js';
|
|
10
|
+
import { pyprojectEntryPoints } from '../../scripts/entry-points/pyproject.js';
|
|
11
|
+
import { toxEntryPoints } from '../../scripts/entry-points/tox.js';
|
|
12
|
+
import { dirsUpward } from './entry-points.js';
|
|
13
|
+
const NODE_BUILTINS = new Set([
|
|
14
|
+
'install', 'i', 'ci', 'add', 'remove', 'update', 'up', 'link', 'init', 'dlx', 'audit', 'outdated', 'publish', 'pack', 'version', 'config',
|
|
15
|
+
'cache', 'rebuild', 'prune', 'dedupe', 'why', 'ls', 'list', 'x',
|
|
16
|
+
]);
|
|
17
|
+
const UV_BUILTINS = new Set(['sync', 'lock', 'add', 'remove', 'venv', 'pip', 'python', 'tool', 'init', 'build', 'publish']);
|
|
18
|
+
const POETRY_BUILTINS = new Set(['install', 'add', 'remove', 'lock', 'shell', 'build', 'publish', 'init', 'update', 'show', 'env', 'config']);
|
|
19
|
+
const PDM_BUILTINS = new Set(['install', 'add', 'remove', 'lock', 'sync', 'build', 'publish', 'init']);
|
|
20
|
+
const PYTHON_STDLIB = new Set(['venv', 'pip', 'http.server', 'json.tool', 'unittest', 'pytest', 'ensurepip', 'build', 'compileall', 'timeit', 'zipapp', 'http', 'site', 'sysconfig']);
|
|
21
|
+
const WORKSPACE_FLAGS = new Set(['-w', '--workspace', '--workspaces', '--filter', '-F', '-r', '--recursive', '--cwd']);
|
|
22
|
+
const VALUE_FLAGS = new Set(['--prefix', '-C', '--cwd', '--filter', '-F', '--workspace', '--directory']);
|
|
23
|
+
const MAKEFILES = ['Makefile', 'makefile', 'GNUmakefile'];
|
|
24
|
+
const join = (dir, next) => (next === undefined ? dir : path.posix.normalize(path.posix.join(dir, next)));
|
|
25
|
+
export class DocResolver {
|
|
26
|
+
repoPath;
|
|
27
|
+
tracked;
|
|
28
|
+
manifests;
|
|
29
|
+
toolNames;
|
|
30
|
+
constructor(repoPath, lists) {
|
|
31
|
+
this.repoPath = repoPath;
|
|
32
|
+
this.tracked = new Set(trackedPaths(repoPath));
|
|
33
|
+
this.toolNames = new Set([...lists.typecheckers, ...lists.linters, ...lists.testRunners].map((t) => (t === 'node --test' ? 'node' : t.replace(/^python3? -m /, ''))));
|
|
34
|
+
}
|
|
35
|
+
allManifests() {
|
|
36
|
+
this.manifests ??= [...this.tracked]
|
|
37
|
+
.filter((f) => path.posix.basename(f) === 'package.json')
|
|
38
|
+
.map((f) => {
|
|
39
|
+
const dir = path.posix.dirname(f);
|
|
40
|
+
const pkg = readPackageJson(this.repoPath, dir);
|
|
41
|
+
return { dir, deps: new Set(Object.keys(pkg?.dependencies ?? {})), scripts: new Set(Object.keys(pkg?.scripts ?? {})) };
|
|
42
|
+
});
|
|
43
|
+
return this.manifests;
|
|
44
|
+
}
|
|
45
|
+
hasScript(dir, name) {
|
|
46
|
+
return packageJsonEntryPoints.lookup(this.repoPath, dir, name) !== undefined;
|
|
47
|
+
}
|
|
48
|
+
declaredPython(dir, name) {
|
|
49
|
+
const wanted = name.toLowerCase().replace(/[-_.]+/g, '-');
|
|
50
|
+
return dirsUpward(dir).some((d) => pythonDeclaredDeps(this.repoPath, d).has(wanted));
|
|
51
|
+
}
|
|
52
|
+
firstPartyModule(dir, name) {
|
|
53
|
+
const top = name.split('.').join('/');
|
|
54
|
+
return [dir, 'src'].some((base) => [`${top}.py`, `${top}/__init__.py`, `${top}/__main__.py`].some((f) => this.tracked.has(path.posix.normalize(path.posix.join(base, f)))));
|
|
55
|
+
}
|
|
56
|
+
/** `npx <t>`, `uv run <t>`, `python -m <t>` and a bare recognised tool: resolves for a script, a recognised tool, a declared dependency, a stdlib or first-party module. */
|
|
57
|
+
toolRef(tool, dir, runner) {
|
|
58
|
+
if (tool === undefined)
|
|
59
|
+
return true;
|
|
60
|
+
if (runner === 'python -m' && (PYTHON_STDLIB.has(tool) || this.firstPartyModule(dir, tool)))
|
|
61
|
+
return true;
|
|
62
|
+
if ((runner === 'uv run' || runner === 'poetry run') && pyprojectEntryPoints.lookup(this.repoPath, dir, `scripts:${tool}`) !== undefined)
|
|
63
|
+
return true;
|
|
64
|
+
if (runner === 'pdm run' && pyprojectEntryPoints.lookup(this.repoPath, dir, `pdm:${tool}`) !== undefined)
|
|
65
|
+
return true;
|
|
66
|
+
if (this.toolNames.has(tool))
|
|
67
|
+
return true;
|
|
68
|
+
if (this.allManifests().some((m) => m.deps.has(tool)))
|
|
69
|
+
return true;
|
|
70
|
+
return this.declaredPython(dir, tool);
|
|
71
|
+
}
|
|
72
|
+
nodeCommand(runner, tokens, baseDir) {
|
|
73
|
+
let dir = baseDir;
|
|
74
|
+
let workspace = false;
|
|
75
|
+
const args = [];
|
|
76
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
77
|
+
const token = tokens[i];
|
|
78
|
+
if (token === '--')
|
|
79
|
+
break;
|
|
80
|
+
if (WORKSPACE_FLAGS.has(token))
|
|
81
|
+
workspace = true;
|
|
82
|
+
if (token === '--prefix' || token === '-C' || token === '--cwd') {
|
|
83
|
+
dir = join(baseDir, tokens[i + 1]);
|
|
84
|
+
i += 1;
|
|
85
|
+
}
|
|
86
|
+
else if (VALUE_FLAGS.has(token))
|
|
87
|
+
i += 1;
|
|
88
|
+
else if (token.startsWith('--prefix='))
|
|
89
|
+
dir = join(baseDir, token.slice(9));
|
|
90
|
+
else if (!token.startsWith('-'))
|
|
91
|
+
args.push(token);
|
|
92
|
+
}
|
|
93
|
+
const sub = args[0];
|
|
94
|
+
if (sub === undefined || NODE_BUILTINS.has(sub))
|
|
95
|
+
return true;
|
|
96
|
+
if (sub === 'exec' || (runner === 'bun' && sub === 'x'))
|
|
97
|
+
return this.toolRef(args[1], dir, runner === 'pnpm' ? 'pnpm exec' : runner);
|
|
98
|
+
let name;
|
|
99
|
+
let toolFallback = false;
|
|
100
|
+
if (sub === 'workspace')
|
|
101
|
+
name = args[2];
|
|
102
|
+
else if (sub === 'run' || sub === 'run-script')
|
|
103
|
+
name = args[1];
|
|
104
|
+
else if (['test', 'start', 'stop', 'restart'].includes(sub))
|
|
105
|
+
name = sub;
|
|
106
|
+
else if (runner === 'yarn' || runner === 'pnpm') {
|
|
107
|
+
name = sub;
|
|
108
|
+
toolFallback = true;
|
|
109
|
+
}
|
|
110
|
+
else
|
|
111
|
+
return true;
|
|
112
|
+
if (name === undefined)
|
|
113
|
+
return true;
|
|
114
|
+
if (workspace || sub === 'workspace' ? this.allManifests().some((m) => m.scripts.has(name)) : this.hasScript(dir, name))
|
|
115
|
+
return true;
|
|
116
|
+
if (name === 'start' && runner === 'npm' && this.tracked.has(path.posix.join(dir, 'server.js')))
|
|
117
|
+
return true;
|
|
118
|
+
return toolFallback && this.toolRef(name, dir, runner);
|
|
119
|
+
}
|
|
120
|
+
makeCommand(tokens, baseDir) {
|
|
121
|
+
let dir = baseDir;
|
|
122
|
+
const targets = [];
|
|
123
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
124
|
+
const token = tokens[i];
|
|
125
|
+
if (token === '-C' || token === '--directory') {
|
|
126
|
+
dir = join(baseDir, tokens[i + 1]);
|
|
127
|
+
i += 1;
|
|
128
|
+
}
|
|
129
|
+
else if (token === '-f' || token === '--file')
|
|
130
|
+
i += 1;
|
|
131
|
+
else if (!token.startsWith('-') && !token.includes('='))
|
|
132
|
+
targets.push(token);
|
|
133
|
+
}
|
|
134
|
+
const names = new Set(makefileEntryPoints.list(this.repoPath, dir).map((e) => e.name));
|
|
135
|
+
const exists = MAKEFILES.some((m) => this.tracked.has(path.posix.join(dir, m)));
|
|
136
|
+
return targets.length === 0 ? exists : targets.every((t) => names.has(t));
|
|
137
|
+
}
|
|
138
|
+
justCommand(tokens, baseDir) {
|
|
139
|
+
let dir = baseDir;
|
|
140
|
+
const recipes = [];
|
|
141
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
142
|
+
const token = tokens[i];
|
|
143
|
+
if (token === '-d' || token === '--working-directory') {
|
|
144
|
+
dir = join(baseDir, tokens[i + 1]);
|
|
145
|
+
i += 1;
|
|
146
|
+
}
|
|
147
|
+
else if (token === '-f' || token === '--justfile')
|
|
148
|
+
i += 1;
|
|
149
|
+
else if (!token.startsWith('-') && !token.includes('='))
|
|
150
|
+
recipes.push(token);
|
|
151
|
+
}
|
|
152
|
+
if (recipes.length === 0)
|
|
153
|
+
return findFile(this.repoPath, dir, ['justfile', 'Justfile', '.justfile'], true) !== undefined;
|
|
154
|
+
return recipes.every((r) => justfileEntryPoints.lookup(this.repoPath, dir, r) !== undefined);
|
|
155
|
+
}
|
|
156
|
+
/** Whether one documented command resolves. `dir` is the document's directory with the command's `cd` already applied. */
|
|
157
|
+
resolves(command, docDir) {
|
|
158
|
+
const dir = join(docDir, command.cd === '.' ? undefined : command.cd);
|
|
159
|
+
const tokens = command.tokens;
|
|
160
|
+
const first = tokens[0];
|
|
161
|
+
switch (first) {
|
|
162
|
+
case 'npm':
|
|
163
|
+
case 'pnpm':
|
|
164
|
+
case 'yarn':
|
|
165
|
+
case 'bun':
|
|
166
|
+
return this.nodeCommand(first, tokens, dir);
|
|
167
|
+
case 'npx': {
|
|
168
|
+
const tool = tokens.slice(1).find((t) => !t.startsWith('-'));
|
|
169
|
+
return this.toolRef(tool, dir, 'npx');
|
|
170
|
+
}
|
|
171
|
+
case 'make':
|
|
172
|
+
return this.makeCommand(tokens, dir);
|
|
173
|
+
case 'just':
|
|
174
|
+
return this.justCommand(tokens, dir);
|
|
175
|
+
case 'pip':
|
|
176
|
+
case 'pipenv':
|
|
177
|
+
return true;
|
|
178
|
+
case 'uv':
|
|
179
|
+
case 'poetry':
|
|
180
|
+
case 'pdm': {
|
|
181
|
+
const builtins = first === 'uv' ? UV_BUILTINS : first === 'poetry' ? POETRY_BUILTINS : PDM_BUILTINS;
|
|
182
|
+
const sub = tokens[1];
|
|
183
|
+
if (sub === undefined || builtins.has(sub))
|
|
184
|
+
return true;
|
|
185
|
+
if (sub !== 'run')
|
|
186
|
+
return first === 'uv';
|
|
187
|
+
const tool = tokens.slice(2).find((t) => !t.startsWith('-'));
|
|
188
|
+
if (tool === 'python' || tool === 'python3')
|
|
189
|
+
return this.pythonCommand(tokens.slice(tokens.indexOf(tool)), dir);
|
|
190
|
+
return this.toolRef(tool, dir, `${first} run`);
|
|
191
|
+
}
|
|
192
|
+
case 'python':
|
|
193
|
+
return this.pythonCommand(tokens, dir);
|
|
194
|
+
case 'tox': {
|
|
195
|
+
const envs = tokens.flatMap((t, i) => (tokens[i - 1] === '-e' ? t.split(',') : []));
|
|
196
|
+
return envs.length === 0 ? toxEntryPoints.fileFor?.(this.repoPath, dir) !== undefined : envs.every((e) => toxEntryPoints.lookup(this.repoPath, dir, e) !== undefined);
|
|
197
|
+
}
|
|
198
|
+
case 'nox': {
|
|
199
|
+
const sessions = tokens.flatMap((t, i) => (['-s', '--session', '-e', '--sessions'].includes(tokens[i - 1] ?? '') ? t.split(',') : []));
|
|
200
|
+
return sessions.length === 0 ? noxEntryPoints.fileFor?.(this.repoPath, dir) !== undefined : sessions.every((s) => noxEntryPoints.lookup(this.repoPath, dir, s) !== undefined);
|
|
201
|
+
}
|
|
202
|
+
default:
|
|
203
|
+
return this.toolRef(first, dir, 'bare');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
pythonCommand(tokens, dir) {
|
|
207
|
+
const arg = tokens[1];
|
|
208
|
+
if (arg === undefined || arg === '-c' || arg === '-' || arg === '-m') {
|
|
209
|
+
if (arg === '-m')
|
|
210
|
+
return this.toolRef(tokens[2], dir, 'python -m');
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
if (arg.startsWith('-'))
|
|
214
|
+
return true;
|
|
215
|
+
return this.tracked.has(path.posix.normalize(path.posix.join(dir, arg)));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { checkKinds, UNRECOGNISED_RUNNERS } from '../../scripts/checks.js';
|
|
3
|
+
import { entryPointKinds } from '../../scripts/entry-points/index.js';
|
|
4
|
+
import { expandCommand } from '../../scripts/expand.js';
|
|
5
|
+
/** The directory itself and each ancestor up to the repository root ("."). */
|
|
6
|
+
export function dirsUpward(dir) {
|
|
7
|
+
const out = [];
|
|
8
|
+
for (let current = path.posix.normalize(dir === '' ? '.' : dir);; current = path.posix.dirname(current)) {
|
|
9
|
+
out.push(current);
|
|
10
|
+
if (current === '.' || current === '' || current.startsWith('..'))
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Entry points defined in exactly `dir`, expanded up to 3 levels, with the checks each one runs. Memoized per scan. */
|
|
15
|
+
export class EntryPointChecks {
|
|
16
|
+
repoPath;
|
|
17
|
+
rec;
|
|
18
|
+
cache = new Map();
|
|
19
|
+
constructor(repoPath, rec) {
|
|
20
|
+
this.repoPath = repoPath;
|
|
21
|
+
this.rec = rec;
|
|
22
|
+
}
|
|
23
|
+
inDir(dir) {
|
|
24
|
+
const cached = this.cache.get(dir);
|
|
25
|
+
if (cached)
|
|
26
|
+
return cached;
|
|
27
|
+
const results = [];
|
|
28
|
+
for (const kind of entryPointKinds) {
|
|
29
|
+
for (const entry of kind.list(this.repoPath, dir)) {
|
|
30
|
+
const commands = expandCommand(this.repoPath, entry.invoke, { dir, file: dir }, { maxDepth: 3 });
|
|
31
|
+
const kinds = new Set();
|
|
32
|
+
const unrecognised = new Set();
|
|
33
|
+
for (const command of commands) {
|
|
34
|
+
for (const found of checkKinds(command, this.rec))
|
|
35
|
+
kinds.add(found);
|
|
36
|
+
if (command.tool !== null && UNRECOGNISED_RUNNERS.has(command.tool))
|
|
37
|
+
unrecognised.add(command.tool);
|
|
38
|
+
}
|
|
39
|
+
results.push({ name: `${path.posix.join(dir, kind.id)}:${entry.name}`, kinds, unrecognised: [...unrecognised] });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
this.cache.set(dir, results);
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
/** Candidate entry points for a unit: those of its package directory and of every ancestor (a root `verify` covers every package). */
|
|
46
|
+
forUnit(dir) {
|
|
47
|
+
return dirsUpward(dir).flatMap((d) => this.inDir(d));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** `KEY=value`, `export KEY=value`, blank lines, `#` comment lines and quoted values. A commented-out `# KEY=` line does not count. */
|
|
2
|
+
export function parseEnvExample(text) {
|
|
3
|
+
const out = {};
|
|
4
|
+
for (const raw of text.replace(/\r\n?/g, '\n').split('\n')) {
|
|
5
|
+
const line = raw.trim();
|
|
6
|
+
if (line === '' || line.startsWith('#'))
|
|
7
|
+
continue;
|
|
8
|
+
const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_.]*)\s*=\s*(.*)$/.exec(line);
|
|
9
|
+
if (!match?.[1])
|
|
10
|
+
continue;
|
|
11
|
+
let value = (match[2] ?? '').trim();
|
|
12
|
+
const quote = value[0];
|
|
13
|
+
if ((quote === '"' || quote === "'") && value.lastIndexOf(quote) > 0)
|
|
14
|
+
value = value.slice(1, value.lastIndexOf(quote));
|
|
15
|
+
else
|
|
16
|
+
value = value.replace(/\s+#.*$/, '');
|
|
17
|
+
out[match[1]] = value;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
const isName = (node, text) => ts.isIdentifier(node) && node.text === text;
|
|
3
|
+
/** `process.env` */
|
|
4
|
+
function isProcessEnv(node) {
|
|
5
|
+
return ts.isPropertyAccessExpression(node) && isName(node.expression, 'process') && node.name.text === 'env';
|
|
6
|
+
}
|
|
7
|
+
/** `import.meta.env` */
|
|
8
|
+
function isImportMetaEnv(node) {
|
|
9
|
+
return (ts.isPropertyAccessExpression(node) &&
|
|
10
|
+
node.name.text === 'env' &&
|
|
11
|
+
ts.isMetaProperty(node.expression) &&
|
|
12
|
+
node.expression.keywordToken === ts.SyntaxKind.ImportKeyword &&
|
|
13
|
+
node.expression.name.text === 'meta');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Names read through `process.env.X`, `process.env["X"]`, destructuring from `process.env` (a rest element or computed key is skipped),
|
|
17
|
+
* `import.meta.env.X` and `import.meta.env["X"]`. An alias (`const env = process.env`) and a non-literal key are not counted.
|
|
18
|
+
*/
|
|
19
|
+
export function tsEnvReads(sf) {
|
|
20
|
+
const names = new Set();
|
|
21
|
+
const visit = (node) => {
|
|
22
|
+
if (ts.isPropertyAccessExpression(node) && (isProcessEnv(node.expression) || isImportMetaEnv(node.expression)))
|
|
23
|
+
names.add(node.name.text);
|
|
24
|
+
else if (ts.isElementAccessExpression(node) && (isProcessEnv(node.expression) || isImportMetaEnv(node.expression)) && ts.isStringLiteralLike(node.argumentExpression)) {
|
|
25
|
+
names.add(node.argumentExpression.text);
|
|
26
|
+
}
|
|
27
|
+
else if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && node.initializer && isProcessEnv(node.initializer)) {
|
|
28
|
+
for (const element of node.name.elements) {
|
|
29
|
+
if (element.dotDotDotToken)
|
|
30
|
+
continue;
|
|
31
|
+
const key = element.propertyName ?? element.name;
|
|
32
|
+
if (ts.isIdentifier(key))
|
|
33
|
+
names.add(key.text);
|
|
34
|
+
else if (ts.isStringLiteralLike(key))
|
|
35
|
+
names.add(key.text);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
ts.forEachChild(node, visit);
|
|
39
|
+
};
|
|
40
|
+
visit(sf);
|
|
41
|
+
return names;
|
|
42
|
+
}
|
|
43
|
+
/** True when a name is excluded from 6.5: one of the YAML names, or starting with one of the YAML prefixes. */
|
|
44
|
+
export function isExcludedEnvName(name, lists) {
|
|
45
|
+
return lists.envExcludedNames.includes(name) || lists.envExcludedPrefixes.some((prefix) => name.startsWith(prefix));
|
|
46
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readPackageJson } from '../../manifest/package-json.js';
|
|
3
|
+
import { readTracked } from '../../scope/tracked.js';
|
|
4
|
+
import { parseTomlText } from '../../util/toml.js';
|
|
5
|
+
import { dirsUpward } from './entry-points.js';
|
|
6
|
+
const isObj = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
const nonEmpty = (value) => (typeof value === 'string' && value.trim() !== '') || (Array.isArray(value) && value.some(nonEmpty));
|
|
8
|
+
const fileHasContent = (name, group) => ({
|
|
9
|
+
group,
|
|
10
|
+
check: (repoPath, dir) => (readTracked(repoPath, path.posix.join(dir, name))?.trim() ?? '') !== '',
|
|
11
|
+
});
|
|
12
|
+
function toml(repoPath, file, warnings) {
|
|
13
|
+
const text = readTracked(repoPath, file);
|
|
14
|
+
if (text === null)
|
|
15
|
+
return undefined;
|
|
16
|
+
const doc = parseTomlText(text);
|
|
17
|
+
if (!doc)
|
|
18
|
+
warnings.push(`6.5: ${file} is not valid TOML, so it does not pin a runtime`);
|
|
19
|
+
return doc;
|
|
20
|
+
}
|
|
21
|
+
const toolVersions = (tools, group) => ({
|
|
22
|
+
group,
|
|
23
|
+
check: (repoPath, dir) => (readTracked(repoPath, path.posix.join(dir, '.tool-versions')) ?? '')
|
|
24
|
+
.split('\n')
|
|
25
|
+
.some((line) => {
|
|
26
|
+
const [tool, version] = line.trim().split(/\s+/);
|
|
27
|
+
return tool !== undefined && tools.includes(tool) && version !== undefined && !version.startsWith('#');
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
const mise = (tools, group) => ({
|
|
31
|
+
group,
|
|
32
|
+
check(repoPath, dir, warnings) {
|
|
33
|
+
const doc = toml(repoPath, path.posix.join(dir, 'mise.toml'), warnings);
|
|
34
|
+
const table = doc && isObj(doc.tools) ? doc.tools : {};
|
|
35
|
+
return tools.some((tool) => nonEmpty(table[tool]));
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
/** The pin files of the 6.5 `runtime_pinned` measure, by the words the YAML uses; a word the YAML adds that is not here pins nothing. */
|
|
39
|
+
const PROBES = {
|
|
40
|
+
'.nvmrc': [fileHasContent('.nvmrc', 'typescript_javascript')],
|
|
41
|
+
'.node-version': [fileHasContent('.node-version', 'typescript_javascript')],
|
|
42
|
+
'.tool-versions': [toolVersions(['node', 'nodejs'], 'typescript_javascript'), toolVersions(['python'], 'python')],
|
|
43
|
+
'mise.toml': [mise(['node', 'nodejs'], 'typescript_javascript'), mise(['python'], 'python')],
|
|
44
|
+
'engines.node in package.json': [
|
|
45
|
+
{ group: 'typescript_javascript', check: (repoPath, dir) => nonEmpty(readPackageJson(repoPath, dir)?.engines.node) },
|
|
46
|
+
],
|
|
47
|
+
'volta in package.json': [
|
|
48
|
+
{ group: 'typescript_javascript', check: (repoPath, dir) => nonEmpty(readPackageJson(repoPath, dir)?.volta?.node) },
|
|
49
|
+
],
|
|
50
|
+
'.python-version': [fileHasContent('.python-version', 'python')],
|
|
51
|
+
'requires-python in pyproject.toml': [
|
|
52
|
+
{
|
|
53
|
+
group: 'python',
|
|
54
|
+
check(repoPath, dir, warnings) {
|
|
55
|
+
const doc = toml(repoPath, path.posix.join(dir, 'pyproject.toml'), warnings);
|
|
56
|
+
return doc !== undefined && isObj(doc.project) && nonEmpty(doc.project['requires-python']);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
'runtime.txt': [fileHasContent('runtime.txt', 'python')],
|
|
61
|
+
};
|
|
62
|
+
/** Looked up at the unit's package directory, then each ancestor up to the repository root. Poetry's `python = "^3.12"` dependency does not count. */
|
|
63
|
+
export function runtimePinned(repoPath, dir, group, pinFiles, warnings) {
|
|
64
|
+
for (const d of dirsUpward(dir)) {
|
|
65
|
+
for (const word of pinFiles) {
|
|
66
|
+
for (const probe of PROBES[word] ?? []) {
|
|
67
|
+
if (probe.group === group && probe.check(repoPath, d, warnings))
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { PyImportGraph } from './safety/py-tests.js';
|
|
2
|
+
import { TsCredits } from './safety/ts-credits.js';
|
|
3
|
+
import { createTsResolver } from './safety/ts-resolve.js';
|
|
4
|
+
import { logicUnits } from './units.js';
|
|
5
|
+
export const p14 = {
|
|
6
|
+
id: '1.4',
|
|
7
|
+
layer: 'static',
|
|
8
|
+
async measure({ scope, repoPath }) {
|
|
9
|
+
const units = logicUnits(scope);
|
|
10
|
+
const warnings = [];
|
|
11
|
+
const credited = new Set();
|
|
12
|
+
const tsTests = scope.files.filter((f) => f.role === 'test' && f.group === 'typescript_javascript');
|
|
13
|
+
if (tsTests.length > 0 && units.some((u) => u.group === 'typescript_javascript')) {
|
|
14
|
+
const credits = new TsCredits(repoPath, scope, createTsResolver(repoPath));
|
|
15
|
+
for (const test of tsTests)
|
|
16
|
+
for (const file of credits.credits(test.path))
|
|
17
|
+
credited.add(file);
|
|
18
|
+
}
|
|
19
|
+
const pyFiles = scope.files.filter((f) => f.group === 'python');
|
|
20
|
+
const pyTests = pyFiles.filter((f) => f.role === 'test');
|
|
21
|
+
if (pyTests.length > 0 && units.some((u) => u.group === 'python')) {
|
|
22
|
+
const graph = new PyImportGraph(repoPath, pyFiles);
|
|
23
|
+
for (const test of pyTests) {
|
|
24
|
+
if (graph.factsOf(test.path)?.ok === false)
|
|
25
|
+
warnings.push(`1.4: ${test.path}: syntax error, its imports are not counted`);
|
|
26
|
+
for (const file of graph.credits(test.path))
|
|
27
|
+
credited.add(file);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
status: 'measured',
|
|
32
|
+
units: units.map((unit) => ({
|
|
33
|
+
package: unit.package,
|
|
34
|
+
group: unit.group,
|
|
35
|
+
weight: unit.weight,
|
|
36
|
+
raw: { kind: 'ratio', numerator: unit.files.filter((f) => credited.has(f.path)).length, denominator: unit.files.length, scale: 100 },
|
|
37
|
+
})),
|
|
38
|
+
...(warnings.length === 0 ? {} : { warnings }),
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { pythonTestFacts } from '../python/tests.js';
|
|
2
|
+
import { testCases } from './safety/ts-cases.js';
|
|
3
|
+
import { TsCredits } from './safety/ts-credits.js';
|
|
4
|
+
import { createTsResolver } from './safety/ts-resolve.js';
|
|
5
|
+
import { logicUnits, unitId } from './units.js';
|
|
6
|
+
export const p15 = {
|
|
7
|
+
id: '1.5',
|
|
8
|
+
layer: 'static',
|
|
9
|
+
async measure({ scope, repoPath }) {
|
|
10
|
+
const units = logicUnits(scope);
|
|
11
|
+
const tallies = new Map(units.map((u) => [unitId(u), { numerator: 0, denominator: 0 }]));
|
|
12
|
+
const warnings = [];
|
|
13
|
+
const tsTests = scope.files.filter((f) => f.role === 'test' && f.group === 'typescript_javascript' && tallies.has(unitId(f)));
|
|
14
|
+
if (tsTests.length > 0) {
|
|
15
|
+
const credits = new TsCredits(repoPath, scope, createTsResolver(repoPath));
|
|
16
|
+
for (const test of tsTests) {
|
|
17
|
+
const sf = credits.source(test.path);
|
|
18
|
+
const tally = tallies.get(unitId(test));
|
|
19
|
+
if (!sf || !tally)
|
|
20
|
+
continue;
|
|
21
|
+
for (const found of testCases(sf)) {
|
|
22
|
+
tally.denominator += 1;
|
|
23
|
+
if (found.asserts)
|
|
24
|
+
tally.numerator += 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const pyFiles = scope.files.filter((f) => f.group === 'python');
|
|
29
|
+
const pyTests = pyFiles.filter((f) => f.role === 'test' && tallies.has(unitId(f)));
|
|
30
|
+
if (pyTests.length > 0) {
|
|
31
|
+
const facts = pythonTestFacts(repoPath);
|
|
32
|
+
for (const test of pyTests) {
|
|
33
|
+
const info = facts[test.path];
|
|
34
|
+
const tally = tallies.get(unitId(test));
|
|
35
|
+
if (!tally)
|
|
36
|
+
continue;
|
|
37
|
+
if (!info?.ok) {
|
|
38
|
+
warnings.push(`1.5: ${test.path}: syntax error, its test cases are not counted`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const found of info.cases) {
|
|
42
|
+
tally.denominator += 1;
|
|
43
|
+
if (found.asserts)
|
|
44
|
+
tally.numerator += 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
status: 'measured',
|
|
50
|
+
units: units.map((unit) => ({
|
|
51
|
+
package: unit.package,
|
|
52
|
+
group: unit.group,
|
|
53
|
+
weight: unit.weight,
|
|
54
|
+
raw: { kind: 'ratio', ...tallies.get(unitId(unit)), scale: 100 },
|
|
55
|
+
})),
|
|
56
|
+
...(warnings.length === 0 ? {} : { warnings }),
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|