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,184 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { ESLint } from 'eslint';
|
|
7
|
+
import sonarjs from 'eslint-plugin-sonarjs';
|
|
8
|
+
import tseslint from 'typescript-eslint';
|
|
9
|
+
import { ToolError } from '../scope/types.js';
|
|
10
|
+
import { failureReason, unavailable } from '../tools/outcome.js';
|
|
11
|
+
import { createWorkCopy } from '../tools/workcopy.js';
|
|
12
|
+
import { healthPlugin } from './eslint/health-plugin.js';
|
|
13
|
+
import { emptyTallies, logicFiles, tallyUnits, unitId } from './units.js';
|
|
14
|
+
/** Functions with cognitive complexity strictly greater than this count (methodology 4.1: "strictly greater than 15"). */
|
|
15
|
+
const COMPLEXITY_THRESHOLD = 15;
|
|
16
|
+
const MESSAGE = /from (\d+) to the 0 allowed/;
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
function installedVersion(pkg) {
|
|
19
|
+
const json = JSON.parse(readFileSync(require.resolve(`${pkg}/package.json`), 'utf8'));
|
|
20
|
+
return json.version;
|
|
21
|
+
}
|
|
22
|
+
/** TypeScript and JavaScript: the ESLint Node API with an inline config. No repository code runs, no repository config is read. */
|
|
23
|
+
async function lintTypeScript(repoPath, files) {
|
|
24
|
+
const eslint = new ESLint({
|
|
25
|
+
overrideConfigFile: true,
|
|
26
|
+
cwd: repoPath,
|
|
27
|
+
ignore: false,
|
|
28
|
+
overrideConfig: [
|
|
29
|
+
{
|
|
30
|
+
files: ['**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}'],
|
|
31
|
+
languageOptions: {
|
|
32
|
+
parser: tseslint.parser,
|
|
33
|
+
sourceType: 'module',
|
|
34
|
+
parserOptions: { ecmaFeatures: { jsx: true } },
|
|
35
|
+
},
|
|
36
|
+
plugins: { sonarjs, health: healthPlugin },
|
|
37
|
+
rules: { 'sonarjs/cognitive-complexity': ['error', 0], 'health/function': 'error' },
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
const byPath = new Map(files.map((f) => [path.resolve(repoPath, f.path), f]));
|
|
42
|
+
const lint = await eslint.lintFiles([...byPath.keys()]);
|
|
43
|
+
const results = [];
|
|
44
|
+
let unparsed = 0;
|
|
45
|
+
for (const result of lint) {
|
|
46
|
+
const file = byPath.get(path.resolve(result.filePath));
|
|
47
|
+
if (!file)
|
|
48
|
+
continue;
|
|
49
|
+
if (result.messages.some((m) => m.fatal && m.ruleId === null)) {
|
|
50
|
+
unparsed += 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const entry = { file: file.path, group: file.group, functions: 0, complexities: [] };
|
|
54
|
+
for (const message of result.messages) {
|
|
55
|
+
if (message.ruleId === 'health/function') {
|
|
56
|
+
entry.functions += 1;
|
|
57
|
+
}
|
|
58
|
+
else if (message.ruleId === 'sonarjs/cognitive-complexity') {
|
|
59
|
+
const match = MESSAGE.exec(message.message);
|
|
60
|
+
if (!match?.[1])
|
|
61
|
+
throw new ToolError(`eslint-plugin-sonarjs message not understood: ${message.message}`);
|
|
62
|
+
entry.complexities.push(Number(match[1]));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
results.push(entry);
|
|
66
|
+
}
|
|
67
|
+
return { results, unparsed };
|
|
68
|
+
}
|
|
69
|
+
/** complexipy's line for a file it could not parse; the report still holds every other file. */
|
|
70
|
+
const UNPARSED = /Failed to process /g;
|
|
71
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
72
|
+
/**
|
|
73
|
+
* Python: complexipy in a work copy, so no repository config is read. complexipy exits 1 when any function is over
|
|
74
|
+
* --max-complexity-allowed (--ignore-complexity does not change that, 7.x and 8.x) or a file fails to parse; it still
|
|
75
|
+
* writes the report, so exit 1 is accepted and the report decides.
|
|
76
|
+
*/
|
|
77
|
+
async function runComplexipy(ctx, files) {
|
|
78
|
+
const tool = await ctx.tools.resolve('complexipy');
|
|
79
|
+
if (!tool)
|
|
80
|
+
return unavailable(ctx.tools.notFoundReason('complexipy'));
|
|
81
|
+
const copy = await createWorkCopy(ctx.repoPath, files.map((f) => f.path));
|
|
82
|
+
const outDir = await mkdtemp(path.join(tmpdir(), 'health-complexipy-'));
|
|
83
|
+
try {
|
|
84
|
+
const outFile = path.join(outDir, 'complexipy.json');
|
|
85
|
+
const result = await ctx.tools.run(tool, ['.', '--max-complexity-allowed', String(COMPLEXITY_THRESHOLD), '--no-ignore', '--ignore-complexity', '--output-format', 'json', '--output', outFile, '--quiet'], { cwd: copy.dir });
|
|
86
|
+
const failure = failureReason(tool, result, [0, 1]);
|
|
87
|
+
if (failure)
|
|
88
|
+
return unavailable(failure);
|
|
89
|
+
let entries;
|
|
90
|
+
try {
|
|
91
|
+
entries = JSON.parse(readFileSync(outFile, 'utf8'));
|
|
92
|
+
if (!Array.isArray(entries))
|
|
93
|
+
throw new Error('not an array');
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return unavailable('complexipy printed no parseable JSON report');
|
|
97
|
+
}
|
|
98
|
+
const byRel = new Map(files.map((f) => [f.path, f]));
|
|
99
|
+
const perFile = new Map();
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (typeof entry.path !== 'string' || typeof entry.complexity !== 'number') {
|
|
102
|
+
return unavailable('complexipy report has an unexpected shape');
|
|
103
|
+
}
|
|
104
|
+
const toRel = (p) => path.relative(copy.dir, path.resolve(copy.dir, p)).split(path.sep).join('/');
|
|
105
|
+
// `path` is the file; if a complexipy release makes it the directory, `file_name` completes it.
|
|
106
|
+
let rel = toRel(entry.path);
|
|
107
|
+
if (!byRel.has(rel) && typeof entry.file_name === 'string')
|
|
108
|
+
rel = toRel(path.join(entry.path, entry.file_name));
|
|
109
|
+
const file = byRel.get(rel);
|
|
110
|
+
if (!file)
|
|
111
|
+
continue;
|
|
112
|
+
const item = perFile.get(rel) ?? { file: rel, group: file.group, functions: 0, complexities: [] };
|
|
113
|
+
item.functions += 1;
|
|
114
|
+
if (entry.complexity > 0)
|
|
115
|
+
item.complexities.push(entry.complexity);
|
|
116
|
+
perFile.set(rel, item);
|
|
117
|
+
}
|
|
118
|
+
const unparsed = `${result.stdout}\n${result.stderr}`.replace(ANSI, '').match(UNPARSED)?.length ?? 0;
|
|
119
|
+
return { results: [...perFile.values()], unparsed };
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await copy.cleanup();
|
|
123
|
+
await rm(outDir, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export const p41 = {
|
|
127
|
+
id: '4.1',
|
|
128
|
+
layer: 'static',
|
|
129
|
+
async measure(ctx) {
|
|
130
|
+
const tsFiles = logicFiles(ctx.scope, 'typescript_javascript');
|
|
131
|
+
const pyFiles = logicFiles(ctx.scope, 'python');
|
|
132
|
+
const warnings = [];
|
|
133
|
+
const perFile = [];
|
|
134
|
+
let unparsed = 0;
|
|
135
|
+
if (tsFiles.length > 0) {
|
|
136
|
+
const ts = await lintTypeScript(ctx.repoPath, tsFiles);
|
|
137
|
+
perFile.push(...ts.results);
|
|
138
|
+
unparsed += ts.unparsed;
|
|
139
|
+
ctx.tools.recordVersion('eslint', installedVersion('eslint'));
|
|
140
|
+
ctx.tools.recordVersion('eslint-plugin-sonarjs', installedVersion('eslint-plugin-sonarjs'));
|
|
141
|
+
ctx.tools.recordVersion('@typescript-eslint/parser', installedVersion('@typescript-eslint/parser'));
|
|
142
|
+
}
|
|
143
|
+
if (pyFiles.length > 0) {
|
|
144
|
+
const py = await runComplexipy(ctx, pyFiles);
|
|
145
|
+
if (!('results' in py))
|
|
146
|
+
return py;
|
|
147
|
+
perFile.push(...py.results);
|
|
148
|
+
unparsed += py.unparsed;
|
|
149
|
+
}
|
|
150
|
+
if (unparsed > 0)
|
|
151
|
+
warnings.push(`4.1: ${unparsed} files could not be parsed`);
|
|
152
|
+
const tallies = emptyTallies([...tsFiles, ...pyFiles]);
|
|
153
|
+
const byPath = new Map([...tsFiles, ...pyFiles].map((f) => [f.path, f]));
|
|
154
|
+
const complexityByFile = [];
|
|
155
|
+
for (const item of perFile.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0))) {
|
|
156
|
+
const file = byPath.get(item.file);
|
|
157
|
+
if (!file)
|
|
158
|
+
continue;
|
|
159
|
+
const tally = tallies.get(unitId(file));
|
|
160
|
+
if (!tally)
|
|
161
|
+
continue;
|
|
162
|
+
const over = item.complexities.filter((c) => c > COMPLEXITY_THRESHOLD).length;
|
|
163
|
+
tally.denominator += item.functions;
|
|
164
|
+
tally.numerator += over;
|
|
165
|
+
const sum = item.complexities.reduce((a, b) => a + b, 0);
|
|
166
|
+
if (sum > 0) {
|
|
167
|
+
complexityByFile.push({
|
|
168
|
+
file: item.file,
|
|
169
|
+
group: item.group,
|
|
170
|
+
functions: item.functions,
|
|
171
|
+
overThreshold: over,
|
|
172
|
+
complexitySum: sum,
|
|
173
|
+
complexityMax: Math.max(...item.complexities),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
status: 'measured',
|
|
179
|
+
units: tallyUnits(tallies),
|
|
180
|
+
reportOnly: { complexityByFile },
|
|
181
|
+
warnings,
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Files with more than this many logic lines are large (methodology 4.2). */
|
|
2
|
+
const LARGE_FILE_LOGIC_LINES = 500;
|
|
3
|
+
export const p42 = {
|
|
4
|
+
id: '4.2',
|
|
5
|
+
layer: 'static',
|
|
6
|
+
async measure({ scope }) {
|
|
7
|
+
const units = new Map();
|
|
8
|
+
for (const file of scope.files) {
|
|
9
|
+
if (file.role !== 'logic')
|
|
10
|
+
continue;
|
|
11
|
+
const key = `${file.package}\0${file.group}`;
|
|
12
|
+
const unit = units.get(key) ?? { package: file.package, group: file.group, large: 0, total: 0 };
|
|
13
|
+
unit.total += file.logicLines;
|
|
14
|
+
if (file.logicLines > LARGE_FILE_LOGIC_LINES)
|
|
15
|
+
unit.large += file.logicLines;
|
|
16
|
+
units.set(key, unit);
|
|
17
|
+
}
|
|
18
|
+
const ordered = [...units.values()].sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group));
|
|
19
|
+
const result = ordered.map((unit) => ({
|
|
20
|
+
package: unit.package,
|
|
21
|
+
group: unit.group,
|
|
22
|
+
weight: unit.total,
|
|
23
|
+
raw: { kind: 'ratio', numerator: unit.large, denominator: unit.total, scale: 100 },
|
|
24
|
+
}));
|
|
25
|
+
return { status: 'measured', units: result };
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { failureReason, unavailable } from '../tools/outcome.js';
|
|
6
|
+
import { createWorkCopy } from '../tools/workcopy.js';
|
|
7
|
+
import { emptyTallies, logicFiles, tallyUnits, unitId } from './units.js';
|
|
8
|
+
/** Methodology 4.3: min-tokens 50, min-lines 5. */
|
|
9
|
+
const MIN_TOKENS = '50';
|
|
10
|
+
const MIN_LINES = '5';
|
|
11
|
+
/** Physical lines the way jscpd counts them: a trailing newline does not start another line. */
|
|
12
|
+
function physicalLines(text) {
|
|
13
|
+
if (text === '')
|
|
14
|
+
return 0;
|
|
15
|
+
const lines = text.split(/\r\n|\r|\n/);
|
|
16
|
+
return text.endsWith('\n') || text.endsWith('\r') ? lines.length - 1 : lines.length;
|
|
17
|
+
}
|
|
18
|
+
function isRange(fragment) {
|
|
19
|
+
return !!fragment && typeof fragment.name === 'string' && typeof fragment.start === 'number' && typeof fragment.end === 'number';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Duplicated lines per file, the way jscpd's `duplicatedLines` counts them: each clone once, with the
|
|
23
|
+
* lines of the fragment it is attributed to. A clone belongs to its firstFile, or to its secondFile when
|
|
24
|
+
* firstFile is out of scope; a clone with neither file in scope is dropped. Returns null on a malformed clone.
|
|
25
|
+
*/
|
|
26
|
+
export function attributeClones(clones, inScope, rel) {
|
|
27
|
+
const byFile = new Map();
|
|
28
|
+
for (const clone of clones) {
|
|
29
|
+
if (!isRange(clone.firstFile) || !isRange(clone.secondFile))
|
|
30
|
+
return null;
|
|
31
|
+
const target = [clone.firstFile, clone.secondFile].find((f) => inScope(rel(f.name)));
|
|
32
|
+
if (!target)
|
|
33
|
+
continue;
|
|
34
|
+
const file = rel(target.name);
|
|
35
|
+
byFile.set(file, (byFile.get(file) ?? 0) + target.end - target.start + 1);
|
|
36
|
+
}
|
|
37
|
+
return byFile;
|
|
38
|
+
}
|
|
39
|
+
export const p43 = {
|
|
40
|
+
id: '4.3',
|
|
41
|
+
layer: 'static',
|
|
42
|
+
async measure(ctx) {
|
|
43
|
+
const files = [...logicFiles(ctx.scope, 'typescript_javascript'), ...logicFiles(ctx.scope, 'python')];
|
|
44
|
+
const tallies = emptyTallies(files);
|
|
45
|
+
if (files.length === 0)
|
|
46
|
+
return { status: 'measured', units: [] };
|
|
47
|
+
const tool = await ctx.tools.resolve('jscpd');
|
|
48
|
+
if (!tool)
|
|
49
|
+
return unavailable(ctx.tools.notFoundReason('jscpd'));
|
|
50
|
+
const copy = await createWorkCopy(ctx.repoPath, files.map((f) => f.path));
|
|
51
|
+
const outDir = await mkdtemp(path.join(tmpdir(), 'health-jscpd-'));
|
|
52
|
+
try {
|
|
53
|
+
const result = await ctx.tools.run(tool, [
|
|
54
|
+
'.',
|
|
55
|
+
'--min-tokens', MIN_TOKENS,
|
|
56
|
+
'--min-lines', MIN_LINES,
|
|
57
|
+
'--reporters', 'json',
|
|
58
|
+
'--output', outDir,
|
|
59
|
+
'--format', 'typescript,javascript,jsx,tsx,python',
|
|
60
|
+
'--max-lines', '1000000',
|
|
61
|
+
'--max-size', '100mb',
|
|
62
|
+
'--no-gitignore',
|
|
63
|
+
'--no-colors',
|
|
64
|
+
], { cwd: copy.dir });
|
|
65
|
+
const failure = failureReason(tool, result, [0]);
|
|
66
|
+
if (failure)
|
|
67
|
+
return unavailable(failure);
|
|
68
|
+
let duplicates;
|
|
69
|
+
try {
|
|
70
|
+
const report = JSON.parse(readFileSync(path.join(outDir, 'jscpd-report.json'), 'utf8'));
|
|
71
|
+
if (!Array.isArray(report.duplicates))
|
|
72
|
+
throw new Error('no duplicates array');
|
|
73
|
+
duplicates = report.duplicates;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return unavailable('jscpd report is missing or has an unexpected shape');
|
|
77
|
+
}
|
|
78
|
+
// jscpd 5 reports line totals per format, not per file, so the per-unit numbers are rebuilt here:
|
|
79
|
+
// lines are the file's physical lines (jscpd skips files shorter than min-lines), duplicated
|
|
80
|
+
// lines are the clones attributed to the file, each clone counted once.
|
|
81
|
+
const counted = new Map();
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
const lines = physicalLines(readFileSync(path.join(copy.dir, file.path), 'utf8'));
|
|
84
|
+
if (lines >= Number(MIN_LINES))
|
|
85
|
+
counted.set(file.path, lines);
|
|
86
|
+
}
|
|
87
|
+
const rel = (name) => path.relative(copy.dir, path.resolve(copy.dir, name)).split(path.sep).join('/');
|
|
88
|
+
const duplicated = attributeClones(duplicates, (file) => counted.has(file), rel);
|
|
89
|
+
if (!duplicated)
|
|
90
|
+
return unavailable('jscpd report has an unexpected shape');
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const tally = tallies.get(unitId(file));
|
|
93
|
+
const lines = counted.get(file.path);
|
|
94
|
+
if (!tally || lines === undefined)
|
|
95
|
+
continue;
|
|
96
|
+
tally.denominator += lines;
|
|
97
|
+
tally.numerator += duplicated.get(file.path) ?? 0;
|
|
98
|
+
}
|
|
99
|
+
return { status: 'measured', units: tallyUnits(tallies) };
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
await copy.cleanup();
|
|
103
|
+
await rm(outDir, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { analyzePythonDeadCode } from '../python/deadcode.js';
|
|
4
|
+
import { isOutcome, knipFailed, runKnip } from '../tools/knip.js';
|
|
5
|
+
import { failureReason, unavailable } from '../tools/outcome.js';
|
|
6
|
+
import { createWorkCopy } from '../tools/workcopy.js';
|
|
7
|
+
import { readToml } from '../util/toml.js';
|
|
8
|
+
import { countExportedSymbols } from './ts/exports.js';
|
|
9
|
+
import { emptyTallies, logicFiles, tallyUnits, unitId } from './units.js';
|
|
10
|
+
/** methodology 4.4: vulture min-confidence 60. */
|
|
11
|
+
const VULTURE_MIN_CONFIDENCE = '60';
|
|
12
|
+
const VULTURE_LINE = /^(.+?):(\d+): unused (function|class|method) '(.+)' \((\d+)% confidence\)$/;
|
|
13
|
+
/** Module basenames that are entry points: never reported as unused modules (methodology 4.4). */
|
|
14
|
+
const ENTRY_NAMES = new Set([
|
|
15
|
+
'__main__', 'manage', 'wsgi', 'asgi', 'conftest', 'settings',
|
|
16
|
+
'models', 'admin', 'apps', 'urls', 'views', // Django conventions
|
|
17
|
+
'main', // FastAPI
|
|
18
|
+
]);
|
|
19
|
+
function asRecord(value) {
|
|
20
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
function pkgDirOf(pkg) {
|
|
23
|
+
return pkg === '.' ? '' : pkg;
|
|
24
|
+
}
|
|
25
|
+
function joinRel(...parts) {
|
|
26
|
+
return parts.filter((p) => p !== '').join('/');
|
|
27
|
+
}
|
|
28
|
+
// ---- TypeScript and JavaScript ----------------------------------------------------------
|
|
29
|
+
/** Fills the TS/JS tallies. `failed` collects the units beneath a knip root whose install and knip both failed. */
|
|
30
|
+
async function measureTypeScript(ctx, tallies, warnings, failed) {
|
|
31
|
+
const files = logicFiles(ctx.scope, 'typescript_javascript');
|
|
32
|
+
if (files.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
const knip = await runKnip(ctx);
|
|
35
|
+
if (!isOutcome(knip))
|
|
36
|
+
return knip;
|
|
37
|
+
warnings.push(...knip.warnings.map((w) => `4.4: ${w}`));
|
|
38
|
+
const perUnit = new Map();
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
if (knipFailed(knip, file.path)) {
|
|
41
|
+
failed.add(unitId(file));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const exported = countExportedSymbols(ctx.scope.getTsSourceFile(file.path));
|
|
45
|
+
const issues = knip.files.get(file.path);
|
|
46
|
+
const unused = issues?.unusedFile ? exported : (issues?.exports ?? 0);
|
|
47
|
+
const key = unitId(file);
|
|
48
|
+
const unit = perUnit.get(key) ?? { numerator: 0, denominator: 0 };
|
|
49
|
+
unit.denominator += exported;
|
|
50
|
+
unit.numerator += unused;
|
|
51
|
+
perUnit.set(key, unit);
|
|
52
|
+
}
|
|
53
|
+
for (const [key, unit] of perUnit) {
|
|
54
|
+
const tally = tallies.get(key);
|
|
55
|
+
if (!tally)
|
|
56
|
+
continue;
|
|
57
|
+
if (unit.numerator > unit.denominator) {
|
|
58
|
+
warnings.push(`4.4: ${tally.package} (${tally.group}): knip reported ${unit.numerator} unused exports for ${unit.denominator} exported symbols, clamped`);
|
|
59
|
+
unit.numerator = unit.denominator;
|
|
60
|
+
}
|
|
61
|
+
tally.numerator += unit.numerator;
|
|
62
|
+
tally.denominator += unit.denominator;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function importRoots(pkg) {
|
|
67
|
+
const dir = pkgDirOf(pkg);
|
|
68
|
+
return [...new Set([dir, joinRel(dir, 'src'), ''])];
|
|
69
|
+
}
|
|
70
|
+
/** Module parts of `file` under import root `root`, or undefined when the file is not under it. */
|
|
71
|
+
function modulePartsUnder(file, root) {
|
|
72
|
+
if (root !== '' && !file.startsWith(`${root}/`))
|
|
73
|
+
return undefined;
|
|
74
|
+
const rel = root === '' ? file : file.slice(root.length + 1);
|
|
75
|
+
const parts = rel.replace(/\.py$/, '').split('/');
|
|
76
|
+
if (parts[parts.length - 1] === '__init__')
|
|
77
|
+
parts.pop();
|
|
78
|
+
return parts;
|
|
79
|
+
}
|
|
80
|
+
function isDistributedPackage(repoPath, pkg, pyproject) {
|
|
81
|
+
if (pyproject && (asRecord(pyproject.project) || asRecord(asRecord(pyproject.tool)?.poetry)))
|
|
82
|
+
return true;
|
|
83
|
+
const dir = path.join(repoPath, pkgDirOf(pkg));
|
|
84
|
+
return existsSync(path.join(dir, 'setup.py')) || existsSync(path.join(dir, 'setup.cfg'));
|
|
85
|
+
}
|
|
86
|
+
function scriptTargets(pyproject) {
|
|
87
|
+
const targets = { modules: new Set(), objects: new Set() };
|
|
88
|
+
const add = (scripts) => {
|
|
89
|
+
for (const value of Object.values(asRecord(scripts) ?? {})) {
|
|
90
|
+
const text = typeof value === 'string' ? value : typeof asRecord(value)?.reference === 'string' ? String(asRecord(value)?.reference) : '';
|
|
91
|
+
const [modulePart, objectPart] = text.split(':');
|
|
92
|
+
const module = (modulePart ?? '').trim();
|
|
93
|
+
if (module === '')
|
|
94
|
+
continue;
|
|
95
|
+
targets.modules.add(module);
|
|
96
|
+
const object = (objectPart ?? '').trim();
|
|
97
|
+
if (object !== '')
|
|
98
|
+
targets.objects.add(`${module}:${object}`);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
add(asRecord(pyproject?.project)?.scripts);
|
|
102
|
+
add(asRecord(asRecord(pyproject?.tool)?.poetry)?.scripts);
|
|
103
|
+
return targets;
|
|
104
|
+
}
|
|
105
|
+
function isEntryModule(mod, scripts) {
|
|
106
|
+
const segments = mod.file.path.replace(/\.py$/, '').split('/');
|
|
107
|
+
const base = segments[segments.length - 1] ?? '';
|
|
108
|
+
if (ENTRY_NAMES.has(base))
|
|
109
|
+
return true;
|
|
110
|
+
if (segments.slice(0, -1).includes('settings'))
|
|
111
|
+
return true;
|
|
112
|
+
return [...mod.names].some((name) => scripts.modules.has(name));
|
|
113
|
+
}
|
|
114
|
+
/** Imported dotted name to the files that import it. */
|
|
115
|
+
function importGraph(modules, tests, pkg) {
|
|
116
|
+
const importedBy = new Map();
|
|
117
|
+
const mark = (name, importer) => {
|
|
118
|
+
if (name === '')
|
|
119
|
+
return;
|
|
120
|
+
const set = importedBy.get(name) ?? new Set();
|
|
121
|
+
set.add(importer);
|
|
122
|
+
importedBy.set(name, set);
|
|
123
|
+
};
|
|
124
|
+
const markWithParents = (parts, importer) => {
|
|
125
|
+
for (let i = 1; i <= parts.length; i += 1)
|
|
126
|
+
mark(parts.slice(0, i).join('.'), importer);
|
|
127
|
+
};
|
|
128
|
+
for (const mod of [...modules, ...tests]) {
|
|
129
|
+
for (const root of importRoots(pkg)) {
|
|
130
|
+
const own = modulePartsUnder(mod.file.path, root);
|
|
131
|
+
if (!own)
|
|
132
|
+
continue;
|
|
133
|
+
const isInit = mod.file.path.endsWith('/__init__.py') || mod.file.path === '__init__.py';
|
|
134
|
+
const ownPackage = isInit ? own : own.slice(0, -1);
|
|
135
|
+
for (const imp of mod.info.imports) {
|
|
136
|
+
const moduleParts = imp.module === '' ? [] : imp.module.split('.');
|
|
137
|
+
let base;
|
|
138
|
+
if (imp.level === 0) {
|
|
139
|
+
base = moduleParts;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const keep = ownPackage.length - (imp.level - 1);
|
|
143
|
+
if (keep < 0)
|
|
144
|
+
continue;
|
|
145
|
+
base = [...ownPackage.slice(0, keep), ...moduleParts];
|
|
146
|
+
}
|
|
147
|
+
markWithParents(base, mod.file.path);
|
|
148
|
+
if (imp.names.length > 0 && base.length > 0) {
|
|
149
|
+
for (const name of imp.names)
|
|
150
|
+
mark([...base, name].join('.'), mod.file.path);
|
|
151
|
+
}
|
|
152
|
+
else if (imp.names.length > 0) {
|
|
153
|
+
for (const name of imp.names)
|
|
154
|
+
mark(name, mod.file.path);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return importedBy;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* A method vulture flags is used when it may be called through a base class (methodology 4.4): its class,
|
|
163
|
+
* directly or through base classes defined in the repository, has a base defined outside the scanned files
|
|
164
|
+
* (a framework or standard-library hook), or a base class defined in the repository has a method of the same name.
|
|
165
|
+
*/
|
|
166
|
+
export function isOverride(infos, file, qualname) {
|
|
167
|
+
const dot = qualname.indexOf('.');
|
|
168
|
+
if (dot === -1)
|
|
169
|
+
return false;
|
|
170
|
+
const method = qualname.slice(dot + 1);
|
|
171
|
+
const seen = new Set([`${file}\0${qualname.slice(0, dot)}`]);
|
|
172
|
+
const queue = [[file, qualname.slice(0, dot)]];
|
|
173
|
+
for (let next = queue.shift(); next; next = queue.shift()) {
|
|
174
|
+
const [classFile, className] = next;
|
|
175
|
+
const info = infos[classFile];
|
|
176
|
+
const cls = info?.ok ? info.classes[className] : undefined;
|
|
177
|
+
for (const base of cls?.bases ?? []) {
|
|
178
|
+
if (base.file === null || base.class === null)
|
|
179
|
+
return true;
|
|
180
|
+
const key = `${base.file}\0${base.class}`;
|
|
181
|
+
if (seen.has(key))
|
|
182
|
+
continue;
|
|
183
|
+
seen.add(key);
|
|
184
|
+
const baseInfo = infos[base.file];
|
|
185
|
+
if (baseInfo?.ok && baseInfo.symbols.some((s) => s.kind === 'method' && s.qualname === `${base.class}.${method}`))
|
|
186
|
+
return true;
|
|
187
|
+
queue.push([base.file, base.class]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
function matchSymbol(symbols, name, line) {
|
|
193
|
+
return symbols.find((s) => s.name === name && s.startLine <= line && line <= s.line);
|
|
194
|
+
}
|
|
195
|
+
async function measurePython(ctx, tallies, warnings) {
|
|
196
|
+
const logic = logicFiles(ctx.scope, 'python');
|
|
197
|
+
if (logic.length === 0)
|
|
198
|
+
return null;
|
|
199
|
+
const tests = ctx.scope.files.filter((f) => f.role === 'test' && f.group === 'python');
|
|
200
|
+
const vulture = await ctx.tools.resolve('vulture');
|
|
201
|
+
if (!vulture)
|
|
202
|
+
return unavailable(ctx.tools.notFoundReason('vulture'));
|
|
203
|
+
const infos = analyzePythonDeadCode(ctx.repoPath, [...logic, ...tests].map((f) => f.path));
|
|
204
|
+
let unparsed = 0;
|
|
205
|
+
for (const file of [...logic, ...tests])
|
|
206
|
+
if (infos[file.path]?.ok !== true)
|
|
207
|
+
unparsed += 1;
|
|
208
|
+
if (unparsed > 0)
|
|
209
|
+
warnings.push(`4.4: ${unparsed} Python files could not be parsed`);
|
|
210
|
+
const toModule = (file) => {
|
|
211
|
+
const info = infos[file.path];
|
|
212
|
+
if (!info || !info.ok)
|
|
213
|
+
return undefined;
|
|
214
|
+
const names = new Set();
|
|
215
|
+
for (const root of importRoots(file.package)) {
|
|
216
|
+
const parts = modulePartsUnder(file.path, root);
|
|
217
|
+
if (parts && parts.length > 0)
|
|
218
|
+
names.add(parts.join('.'));
|
|
219
|
+
}
|
|
220
|
+
return { file, info, names };
|
|
221
|
+
};
|
|
222
|
+
const packages = [...new Set(logic.map((f) => f.package))];
|
|
223
|
+
for (const pkg of packages) {
|
|
224
|
+
const unitLogic = logic.filter((f) => f.package === pkg).map(toModule).filter((m) => m !== undefined);
|
|
225
|
+
const unitTests = tests.filter((f) => f.package === pkg).map(toModule).filter((m) => m !== undefined);
|
|
226
|
+
const tally = tallies.get(unitId({ package: pkg, group: 'python' }));
|
|
227
|
+
if (!tally || unitLogic.length === 0)
|
|
228
|
+
continue;
|
|
229
|
+
const pyprojectPath = path.join(ctx.repoPath, joinRel(pkgDirOf(pkg), 'pyproject.toml'));
|
|
230
|
+
const pyproject = readToml(pyprojectPath);
|
|
231
|
+
if (pyproject === undefined && existsSync(pyprojectPath)) {
|
|
232
|
+
warnings.push(`${joinRel(pkgDirOf(pkg), 'pyproject.toml')}: invalid TOML, ignored`);
|
|
233
|
+
}
|
|
234
|
+
const distributed = isDistributedPackage(ctx.repoPath, pkg, pyproject);
|
|
235
|
+
const scripts = scriptTargets(pyproject);
|
|
236
|
+
const importedBy = importGraph(unitLogic, unitTests, pkg);
|
|
237
|
+
const unusedModules = new Set();
|
|
238
|
+
for (const mod of unitLogic) {
|
|
239
|
+
if (isEntryModule(mod, scripts))
|
|
240
|
+
continue;
|
|
241
|
+
const used = [...mod.names].some((name) => [...(importedBy.get(name) ?? [])].some((importer) => importer !== mod.file.path));
|
|
242
|
+
if (!used)
|
|
243
|
+
unusedModules.add(mod.file.path);
|
|
244
|
+
}
|
|
245
|
+
const copy = await createWorkCopy(ctx.repoPath, [...unitLogic, ...unitTests].map((m) => m.file.path));
|
|
246
|
+
let hits;
|
|
247
|
+
try {
|
|
248
|
+
const result = await ctx.tools.run(vulture, ['.', '--min-confidence', VULTURE_MIN_CONFIDENCE], { cwd: copy.dir });
|
|
249
|
+
const failure = failureReason(vulture, result, [0, 3]);
|
|
250
|
+
if (failure)
|
|
251
|
+
return unavailable(failure);
|
|
252
|
+
hits = [];
|
|
253
|
+
for (const line of result.stdout.split('\n')) {
|
|
254
|
+
const match = VULTURE_LINE.exec(line.trim());
|
|
255
|
+
if (!match)
|
|
256
|
+
continue;
|
|
257
|
+
hits.push({
|
|
258
|
+
file: path.posix.normalize((match[1] ?? '').split(path.sep).join('/')).replace(/^\.\//, ''),
|
|
259
|
+
line: Number(match[2]),
|
|
260
|
+
name: match[4] ?? '',
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
await copy.cleanup();
|
|
266
|
+
}
|
|
267
|
+
const hitsByFile = new Map();
|
|
268
|
+
for (const hit of hits)
|
|
269
|
+
hitsByFile.set(hit.file, [...(hitsByFile.get(hit.file) ?? []), hit]);
|
|
270
|
+
for (const mod of unitLogic) {
|
|
271
|
+
tally.denominator += mod.info.symbols.length;
|
|
272
|
+
if (unusedModules.has(mod.file.path)) {
|
|
273
|
+
tally.numerator += mod.info.symbols.length;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const counted = new Set();
|
|
277
|
+
const allNames = new Set(distributed ? (mod.info.allNames ?? []) : []);
|
|
278
|
+
for (const hit of hitsByFile.get(mod.file.path) ?? []) {
|
|
279
|
+
const symbol = matchSymbol(mod.info.symbols, hit.name, hit.line);
|
|
280
|
+
if (!symbol || symbol.decorated || allNames.has(symbol.name))
|
|
281
|
+
continue;
|
|
282
|
+
if (symbol.kind === 'method' && isOverride(infos, mod.file.path, symbol.qualname))
|
|
283
|
+
continue;
|
|
284
|
+
if ([...mod.names].some((name) => scripts.objects.has(`${name}:${symbol.qualname}`)))
|
|
285
|
+
continue;
|
|
286
|
+
counted.add(symbol);
|
|
287
|
+
}
|
|
288
|
+
tally.numerator += counted.size;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
export const p44 = {
|
|
294
|
+
id: '4.4',
|
|
295
|
+
layer: 'static',
|
|
296
|
+
async measure(ctx) {
|
|
297
|
+
const tallies = emptyTallies([...logicFiles(ctx.scope, 'typescript_javascript'), ...logicFiles(ctx.scope, 'python')]);
|
|
298
|
+
const warnings = [];
|
|
299
|
+
const failed = new Set();
|
|
300
|
+
const ts = await measureTypeScript(ctx, tallies, warnings, failed);
|
|
301
|
+
if (ts)
|
|
302
|
+
return ts;
|
|
303
|
+
const py = await measurePython(ctx, tallies, warnings);
|
|
304
|
+
if (py)
|
|
305
|
+
return py;
|
|
306
|
+
const units = tallyUnits(tallies).map((unit) => (failed.has(`${unit.package}\0${unit.group}`) ? { ...unit, raw: { kind: 'failed' } } : unit));
|
|
307
|
+
return { status: 'measured', units, warnings };
|
|
308
|
+
},
|
|
309
|
+
};
|