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,44 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pythonLevel } from './py/typecheck-config.js';
|
|
3
|
+
import { levelOf, lowestLevel, unitConfigs } from './ts/tsconfig.js';
|
|
4
|
+
import { emptyTallies, logicFiles } from './units.js';
|
|
5
|
+
const TS_SOURCE = /\.(?:ts|tsx|mts|cts)$/i;
|
|
6
|
+
export const p22 = {
|
|
7
|
+
id: '2.2',
|
|
8
|
+
layer: 'static',
|
|
9
|
+
async measure({ scope, repoPath }) {
|
|
10
|
+
const warnings = [];
|
|
11
|
+
const cache = new Map();
|
|
12
|
+
const warned = new Set();
|
|
13
|
+
const units = [];
|
|
14
|
+
const tsFiles = logicFiles(scope, 'typescript_javascript');
|
|
15
|
+
for (const tally of emptyTallies(tsFiles).values()) {
|
|
16
|
+
const unitFiles = tsFiles.filter((f) => f.package === tally.package).map((f) => f.path);
|
|
17
|
+
const { configs, read } = unitConfigs(repoPath, tally.package, unitFiles, cache);
|
|
18
|
+
for (const config of read) {
|
|
19
|
+
for (const missing of config.missingExtends) {
|
|
20
|
+
const message = `2.2: ${path.posix.relative(repoPath.split(path.sep).join('/'), config.file)} extends ${missing}, which could not be resolved; level computed without it`;
|
|
21
|
+
if (!warned.has(message)) {
|
|
22
|
+
warned.add(message);
|
|
23
|
+
warnings.push(message);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const hasTypeScript = unitFiles.some((f) => TS_SOURCE.test(f));
|
|
28
|
+
const levels = configs.map((c) => (!hasTypeScript && c.options.checkJs !== true ? 'none' : levelOf(c.options)));
|
|
29
|
+
const level = levels.length === 0 ? 'none' : lowestLevel(levels);
|
|
30
|
+
units.push({ package: tally.package, group: tally.group, weight: tally.weight, raw: { kind: 'level', level } });
|
|
31
|
+
}
|
|
32
|
+
const pyFiles = logicFiles(scope, 'python');
|
|
33
|
+
for (const tally of emptyTallies(pyFiles).values()) {
|
|
34
|
+
units.push({
|
|
35
|
+
package: tally.package,
|
|
36
|
+
group: tally.group,
|
|
37
|
+
weight: tally.weight,
|
|
38
|
+
raw: { kind: 'level', level: pythonLevel(repoPath, tally.package, warnings) },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
units.sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group));
|
|
42
|
+
return { status: 'measured', units, warnings: [...new Set(warnings)] };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { analyzePythonTyping } from '../python/typing.js';
|
|
3
|
+
import { countSuppressionComments, hasLeadingDirective } from './ts/directives.js';
|
|
4
|
+
import { emptyTallies, logicFiles, unitId } from './units.js';
|
|
5
|
+
const FILE_LEVEL_OPT_OUT = 10;
|
|
6
|
+
function isConstType(type) {
|
|
7
|
+
return ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && type.typeName.text === 'const';
|
|
8
|
+
}
|
|
9
|
+
/** Escape-hatch points of one TypeScript or JavaScript file (methodology 2.3). */
|
|
10
|
+
function countTsEscapeHatches(sourceFile) {
|
|
11
|
+
let points = 0;
|
|
12
|
+
const visit = (node) => {
|
|
13
|
+
switch (node.kind) {
|
|
14
|
+
case ts.SyntaxKind.AnyKeyword:
|
|
15
|
+
case ts.SyntaxKind.NonNullExpression:
|
|
16
|
+
points += 1;
|
|
17
|
+
break;
|
|
18
|
+
case ts.SyntaxKind.AsExpression:
|
|
19
|
+
case ts.SyntaxKind.TypeAssertionExpression:
|
|
20
|
+
if (!isConstType(node.type))
|
|
21
|
+
points += 1;
|
|
22
|
+
break;
|
|
23
|
+
case ts.SyntaxKind.PropertyDeclaration:
|
|
24
|
+
case ts.SyntaxKind.VariableDeclaration:
|
|
25
|
+
if (node.exclamationToken)
|
|
26
|
+
points += 1;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
ts.forEachChild(node, visit);
|
|
30
|
+
};
|
|
31
|
+
ts.forEachChild(sourceFile, visit);
|
|
32
|
+
points += countSuppressionComments(sourceFile);
|
|
33
|
+
if (hasLeadingDirective(sourceFile, 'ts-nocheck'))
|
|
34
|
+
points += FILE_LEVEL_OPT_OUT;
|
|
35
|
+
return points;
|
|
36
|
+
}
|
|
37
|
+
export const p23 = {
|
|
38
|
+
id: '2.3',
|
|
39
|
+
layer: 'static',
|
|
40
|
+
async measure({ scope, repoPath }) {
|
|
41
|
+
const tsFiles = logicFiles(scope, 'typescript_javascript');
|
|
42
|
+
const pyFiles = logicFiles(scope, 'python');
|
|
43
|
+
const tallies = emptyTallies([...tsFiles, ...pyFiles]);
|
|
44
|
+
for (const file of tsFiles) {
|
|
45
|
+
const tally = tallies.get(unitId(file));
|
|
46
|
+
if (!tally)
|
|
47
|
+
continue;
|
|
48
|
+
tally.numerator += countTsEscapeHatches(scope.getTsSourceFile(file.path));
|
|
49
|
+
tally.denominator += file.logicLines;
|
|
50
|
+
}
|
|
51
|
+
const warnings = [];
|
|
52
|
+
const info = analyzePythonTyping(repoPath, pyFiles.map((f) => f.path));
|
|
53
|
+
let unparsed = 0;
|
|
54
|
+
for (const file of pyFiles) {
|
|
55
|
+
const tally = tallies.get(unitId(file));
|
|
56
|
+
const facts = info[file.path];
|
|
57
|
+
if (!tally)
|
|
58
|
+
continue;
|
|
59
|
+
if (!facts?.ok) {
|
|
60
|
+
unparsed += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const p = facts.points;
|
|
64
|
+
tally.numerator +=
|
|
65
|
+
p.anyCount + p.castCount + p.typeIgnoreLine + p.pyrightIgnore +
|
|
66
|
+
FILE_LEVEL_OPT_OUT * (p.typeIgnoreHeader + (p.mypyIgnoreErrors ? 1 : 0));
|
|
67
|
+
tally.denominator += file.logicLines;
|
|
68
|
+
}
|
|
69
|
+
if (unparsed > 0)
|
|
70
|
+
warnings.push(`2.3/2.4: ${unparsed} Python files could not be parsed`);
|
|
71
|
+
const units = [...tallies.values()]
|
|
72
|
+
.sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group))
|
|
73
|
+
.map((t) => ({
|
|
74
|
+
package: t.package,
|
|
75
|
+
group: t.group,
|
|
76
|
+
weight: t.weight,
|
|
77
|
+
raw: { kind: 'ratio', numerator: t.numerator, denominator: t.denominator, scale: 1000 },
|
|
78
|
+
}));
|
|
79
|
+
return { status: 'measured', units, warnings };
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { analyzePythonTyping } from '../python/typing.js';
|
|
4
|
+
import { hasLeadingDirective } from './ts/directives.js';
|
|
5
|
+
import { unitConfigs } from './ts/tsconfig.js';
|
|
6
|
+
import { emptyTallies, logicFiles, unitId } from './units.js';
|
|
7
|
+
function hasModifier(node, kind) {
|
|
8
|
+
return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((m) => m.kind === kind) ?? false);
|
|
9
|
+
}
|
|
10
|
+
/** A function or arrow function behind parentheses, `satisfies T` and `as T`. */
|
|
11
|
+
function functionOperand(expression) {
|
|
12
|
+
let contextual = false;
|
|
13
|
+
let current = expression;
|
|
14
|
+
for (;;) {
|
|
15
|
+
if (ts.isParenthesizedExpression(current))
|
|
16
|
+
current = current.expression;
|
|
17
|
+
else if (ts.isSatisfiesExpression(current)) {
|
|
18
|
+
contextual = true;
|
|
19
|
+
current = current.expression;
|
|
20
|
+
}
|
|
21
|
+
else if (ts.isAsExpression(current)) {
|
|
22
|
+
if (!(ts.isTypeReferenceNode(current.type) && current.type.getText() === 'const'))
|
|
23
|
+
contextual = true;
|
|
24
|
+
current = current.expression;
|
|
25
|
+
}
|
|
26
|
+
else
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
return ts.isArrowFunction(current) || ts.isFunctionExpression(current) ? { node: current, contextual } : undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Overload signatures with an implementation count as the implementation; without one, every signature counts. */
|
|
32
|
+
function groupOverloads(nodes, keyOf) {
|
|
33
|
+
const byKey = new Map();
|
|
34
|
+
for (const node of nodes) {
|
|
35
|
+
const key = keyOf(node);
|
|
36
|
+
byKey.set(key, [...(byKey.get(key) ?? []), node]);
|
|
37
|
+
}
|
|
38
|
+
return [...byKey.values()].flatMap((group) => {
|
|
39
|
+
const impls = group.filter((n) => n.body !== undefined);
|
|
40
|
+
return impls.length > 0 ? impls : group;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function classSurface(node) {
|
|
44
|
+
const candidates = [];
|
|
45
|
+
const setters = [];
|
|
46
|
+
for (const member of node.members) {
|
|
47
|
+
if (ts.isMethodDeclaration(member) || ts.isConstructorDeclaration(member))
|
|
48
|
+
candidates.push(member);
|
|
49
|
+
else if (ts.isSetAccessorDeclaration(member))
|
|
50
|
+
setters.push(member);
|
|
51
|
+
else
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const isPublic = (member) => !hasModifier(member, ts.SyntaxKind.PrivateKeyword) &&
|
|
55
|
+
!hasModifier(member, ts.SyntaxKind.ProtectedKeyword) &&
|
|
56
|
+
!(member.name !== undefined && ts.isPrivateIdentifier(member.name));
|
|
57
|
+
const methods = groupOverloads(candidates.filter(isPublic), (m) => `${hasModifier(m, ts.SyntaxKind.StaticKeyword) ? 'static ' : ''}${ts.isConstructorDeclaration(m) ? 'constructor' : m.name.getText()}`);
|
|
58
|
+
return [...methods, ...setters.filter(isPublic)].map((fn) => ({ node: fn, contextual: false }));
|
|
59
|
+
}
|
|
60
|
+
function isLocalFunctionValue(expression) {
|
|
61
|
+
return expression ? functionOperand(expression) : undefined;
|
|
62
|
+
}
|
|
63
|
+
/** Functions and function-valued variables declared at the top level of a file, by name. */
|
|
64
|
+
function localFunctions(sourceFile) {
|
|
65
|
+
const byName = new Map();
|
|
66
|
+
const declarations = [];
|
|
67
|
+
for (const statement of sourceFile.statements) {
|
|
68
|
+
if (ts.isFunctionDeclaration(statement) && statement.name)
|
|
69
|
+
declarations.push(statement);
|
|
70
|
+
else if (ts.isVariableStatement(statement)) {
|
|
71
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
72
|
+
if (!ts.isIdentifier(declaration.name))
|
|
73
|
+
continue;
|
|
74
|
+
const surface = isLocalFunctionValue(declaration.initializer);
|
|
75
|
+
if (surface)
|
|
76
|
+
byName.set(declaration.name.text, [{ ...surface, contextual: surface.contextual || declaration.type !== undefined }]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const fn of groupOverloads(declarations, (d) => d.name?.text ?? '')) {
|
|
81
|
+
const name = fn.name?.text ?? '';
|
|
82
|
+
byName.set(name, [...(byName.get(name) ?? []), { node: fn, contextual: false }]);
|
|
83
|
+
}
|
|
84
|
+
return byName;
|
|
85
|
+
}
|
|
86
|
+
function publicSurface(sourceFile) {
|
|
87
|
+
const surface = new Map();
|
|
88
|
+
const add = (items) => {
|
|
89
|
+
for (const item of items)
|
|
90
|
+
surface.set(item.node, item);
|
|
91
|
+
};
|
|
92
|
+
const locals = localFunctions(sourceFile);
|
|
93
|
+
const exportedDeclarations = [];
|
|
94
|
+
for (const statement of sourceFile.statements) {
|
|
95
|
+
const exported = hasModifier(statement, ts.SyntaxKind.ExportKeyword);
|
|
96
|
+
if (ts.isFunctionDeclaration(statement) && exported)
|
|
97
|
+
exportedDeclarations.push(statement);
|
|
98
|
+
else if (ts.isClassDeclaration(statement) && exported)
|
|
99
|
+
add(classSurface(statement));
|
|
100
|
+
else if (ts.isVariableStatement(statement) && exported) {
|
|
101
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
102
|
+
const fn = isLocalFunctionValue(declaration.initializer);
|
|
103
|
+
if (fn)
|
|
104
|
+
add([{ ...fn, contextual: fn.contextual || declaration.type !== undefined }]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
108
|
+
const fn = functionOperand(statement.expression);
|
|
109
|
+
if (fn)
|
|
110
|
+
add([fn]);
|
|
111
|
+
else if (ts.isIdentifier(statement.expression))
|
|
112
|
+
add(locals.get(statement.expression.text) ?? []);
|
|
113
|
+
}
|
|
114
|
+
else if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
115
|
+
for (const specifier of statement.exportClause.elements)
|
|
116
|
+
add(locals.get((specifier.propertyName ?? specifier.name).text) ?? []);
|
|
117
|
+
}
|
|
118
|
+
else if (ts.isExpressionStatement(statement)) {
|
|
119
|
+
add(commonJsSurface(statement.expression, locals));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
add(groupOverloads(exportedDeclarations, (d) => d.name?.text ?? 'default').map((node) => ({ node, contextual: false })));
|
|
123
|
+
return [...surface.values()];
|
|
124
|
+
}
|
|
125
|
+
function commonJsSurface(expression, locals) {
|
|
126
|
+
if (!ts.isBinaryExpression(expression) || expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
|
127
|
+
return [];
|
|
128
|
+
const left = expression.left;
|
|
129
|
+
const right = expression.right;
|
|
130
|
+
const isModuleExports = (e) => ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && e.expression.text === 'module' && e.name.text === 'exports';
|
|
131
|
+
if (isModuleExports(left)) {
|
|
132
|
+
const fn = functionOperand(right);
|
|
133
|
+
if (fn)
|
|
134
|
+
return [fn];
|
|
135
|
+
if (ts.isClassExpression(right))
|
|
136
|
+
return classSurface(right);
|
|
137
|
+
if (ts.isObjectLiteralExpression(right)) {
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const property of right.properties) {
|
|
140
|
+
if (ts.isMethodDeclaration(property))
|
|
141
|
+
out.push({ node: property, contextual: false });
|
|
142
|
+
else if (ts.isPropertyAssignment(property)) {
|
|
143
|
+
const value = functionOperand(property.initializer);
|
|
144
|
+
if (value)
|
|
145
|
+
out.push(value);
|
|
146
|
+
}
|
|
147
|
+
else if (ts.isShorthandPropertyAssignment(property))
|
|
148
|
+
out.push(...(locals.get(property.name.text) ?? []));
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
if (ts.isPropertyAccessExpression(left) &&
|
|
155
|
+
((ts.isIdentifier(left.expression) && left.expression.text === 'exports') || isModuleExports(left.expression))) {
|
|
156
|
+
const fn = functionOperand(right);
|
|
157
|
+
return fn ? [fn] : [];
|
|
158
|
+
}
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
function countTsSlots(sourceFile, checkedAsJs) {
|
|
162
|
+
const slots = { typed: 0, total: 0 };
|
|
163
|
+
for (const { node, contextual } of publicSurface(sourceFile)) {
|
|
164
|
+
for (const parameter of node.parameters) {
|
|
165
|
+
if (ts.isIdentifier(parameter.name) && parameter.name.text === 'this')
|
|
166
|
+
continue;
|
|
167
|
+
slots.total += 1;
|
|
168
|
+
const jsdoc = checkedAsJs && ts.getJSDocParameterTags(parameter).some((tag) => tag.typeExpression !== undefined);
|
|
169
|
+
if (parameter.type !== undefined || parameter.initializer !== undefined || contextual || jsdoc)
|
|
170
|
+
slots.typed += 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return slots;
|
|
174
|
+
}
|
|
175
|
+
// ---- Python -------------------------------------------------------------------------------
|
|
176
|
+
/** Dotted import path parts of a module, with a leading `src` directory ignored. */
|
|
177
|
+
function moduleParts(file) {
|
|
178
|
+
const relative = file.package === '.' ? file.path : file.path.slice(file.package.length + 1);
|
|
179
|
+
const parts = relative.split('/');
|
|
180
|
+
if (parts[0] === 'src' && parts.length > 1)
|
|
181
|
+
parts.shift();
|
|
182
|
+
const base = path.posix.basename(relative);
|
|
183
|
+
const stem = base.replace(/\.[^.]+$/, '');
|
|
184
|
+
parts.pop();
|
|
185
|
+
if (stem !== '__init__')
|
|
186
|
+
parts.push(stem);
|
|
187
|
+
return parts;
|
|
188
|
+
}
|
|
189
|
+
const isPublicName = (name) => !name.startsWith('_');
|
|
190
|
+
const isPublicMethod = (name) => !name.startsWith('_') || (name.startsWith('__') && name.endsWith('__') && name.length > 4);
|
|
191
|
+
const isOverload = (fn) => fn.decorators.some((d) => d === 'overload' || d.endsWith('.overload'));
|
|
192
|
+
/** Overload stubs are skipped when a non-overload definition of the same name exists in the same scope. */
|
|
193
|
+
function withoutOverloads(functions) {
|
|
194
|
+
const implemented = new Set(functions.filter((f) => !isOverload(f)).map((f) => f.name));
|
|
195
|
+
return functions.filter((f) => !(isOverload(f) && implemented.has(f.name)));
|
|
196
|
+
}
|
|
197
|
+
function countFunctionSlots(fn, isMethod, slots) {
|
|
198
|
+
let params = fn.params;
|
|
199
|
+
const isStatic = fn.decorators.includes('staticmethod');
|
|
200
|
+
const first = params[0];
|
|
201
|
+
if (isMethod && (!isStatic || fn.name === '__new__') && first?.kind === 'positional')
|
|
202
|
+
params = params.slice(1);
|
|
203
|
+
for (const param of params) {
|
|
204
|
+
slots.total += 1;
|
|
205
|
+
if (param.typed)
|
|
206
|
+
slots.typed += 1;
|
|
207
|
+
}
|
|
208
|
+
if (fn.name !== '__init__') {
|
|
209
|
+
slots.total += 1;
|
|
210
|
+
if (fn.returns)
|
|
211
|
+
slots.typed += 1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function countPythonSlots(defs, allNames) {
|
|
215
|
+
const slots = { typed: 0, total: 0 };
|
|
216
|
+
const listed = allNames === null ? null : new Set(allNames);
|
|
217
|
+
const isPublicModuleName = (name) => (listed === null ? isPublicName(name) : listed.has(name));
|
|
218
|
+
const functions = withoutOverloads(defs.filter((d) => d.kind === 'function').map((d) => d.info));
|
|
219
|
+
for (const fn of functions)
|
|
220
|
+
if (isPublicModuleName(fn.name))
|
|
221
|
+
countFunctionSlots(fn, false, slots);
|
|
222
|
+
for (const def of defs) {
|
|
223
|
+
if (def.kind !== 'class' || !isPublicModuleName(def.name))
|
|
224
|
+
continue;
|
|
225
|
+
for (const method of withoutOverloads(def.methods))
|
|
226
|
+
if (isPublicMethod(method.name))
|
|
227
|
+
countFunctionSlots(method, true, slots);
|
|
228
|
+
}
|
|
229
|
+
return slots;
|
|
230
|
+
}
|
|
231
|
+
// ---- Measurer -------------------------------------------------------------------------------
|
|
232
|
+
export const p24 = {
|
|
233
|
+
id: '2.4',
|
|
234
|
+
layer: 'static',
|
|
235
|
+
async measure({ scope, repoPath }) {
|
|
236
|
+
const tsFiles = logicFiles(scope, 'typescript_javascript');
|
|
237
|
+
const pyFiles = logicFiles(scope, 'python');
|
|
238
|
+
const tallies = emptyTallies([...tsFiles, ...pyFiles]);
|
|
239
|
+
const cache = new Map();
|
|
240
|
+
const unitConfigsByPackage = new Map();
|
|
241
|
+
const configsFor = (pkg) => {
|
|
242
|
+
const existing = unitConfigsByPackage.get(pkg);
|
|
243
|
+
if (existing)
|
|
244
|
+
return existing;
|
|
245
|
+
const made = unitConfigs(repoPath, pkg, tsFiles.filter((f) => f.package === pkg).map((f) => f.path), cache);
|
|
246
|
+
unitConfigsByPackage.set(pkg, made);
|
|
247
|
+
return made;
|
|
248
|
+
};
|
|
249
|
+
for (const file of tsFiles) {
|
|
250
|
+
const tally = tallies.get(unitId(file));
|
|
251
|
+
if (!tally)
|
|
252
|
+
continue;
|
|
253
|
+
const sourceFile = scope.getTsSourceFile(file.path);
|
|
254
|
+
let checkedAsJs = false;
|
|
255
|
+
if (file.language === 'javascript') {
|
|
256
|
+
const absolute = path.resolve(repoPath, file.path).split(path.sep).join('/');
|
|
257
|
+
const byConfig = configsFor(file.package).configs.some((c) => c.options.checkJs === true && c.fileNames.has(absolute));
|
|
258
|
+
const byComment = hasLeadingDirective(sourceFile, 'ts-check') && !hasLeadingDirective(sourceFile, 'ts-nocheck');
|
|
259
|
+
checkedAsJs = byConfig || byComment;
|
|
260
|
+
}
|
|
261
|
+
const slots = countTsSlots(sourceFile, checkedAsJs);
|
|
262
|
+
tally.numerator += slots.typed;
|
|
263
|
+
tally.denominator += slots.total;
|
|
264
|
+
}
|
|
265
|
+
// Files that cannot be parsed are reported once, by 2.3.
|
|
266
|
+
const info = analyzePythonTyping(repoPath, pyFiles.map((f) => f.path));
|
|
267
|
+
for (const file of pyFiles) {
|
|
268
|
+
const tally = tallies.get(unitId(file));
|
|
269
|
+
const facts = info[file.path];
|
|
270
|
+
if (!tally || !facts?.ok)
|
|
271
|
+
continue;
|
|
272
|
+
if (moduleParts(file).some((part) => part.startsWith('_')))
|
|
273
|
+
continue;
|
|
274
|
+
const slots = countPythonSlots(facts.defs, facts.allNames);
|
|
275
|
+
tally.numerator += slots.typed;
|
|
276
|
+
tally.denominator += slots.total;
|
|
277
|
+
}
|
|
278
|
+
const units = [...tallies.values()]
|
|
279
|
+
.sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group))
|
|
280
|
+
.map((t) => ({
|
|
281
|
+
package: t.package,
|
|
282
|
+
group: t.group,
|
|
283
|
+
weight: t.weight,
|
|
284
|
+
raw: { kind: 'ratio', numerator: t.numerator, denominator: t.denominator, scale: 100 },
|
|
285
|
+
}));
|
|
286
|
+
return { status: 'measured', units };
|
|
287
|
+
},
|
|
288
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { d3Gap, getD3Rules } from './d3/index.js';
|
|
2
|
+
export const p31 = {
|
|
3
|
+
id: '3.1',
|
|
4
|
+
layer: 'static',
|
|
5
|
+
async measure(ctx) {
|
|
6
|
+
const gap = d3Gap(ctx);
|
|
7
|
+
if (gap !== undefined)
|
|
8
|
+
return { status: 'not_measured', reason: gap };
|
|
9
|
+
const { units, warnings } = await getD3Rules(ctx);
|
|
10
|
+
return {
|
|
11
|
+
status: 'measured',
|
|
12
|
+
units: units.map((unit) => ({ package: unit.package, group: unit.group, weight: unit.weight, raw: { kind: 'level', level: unit.level31 } })),
|
|
13
|
+
...(warnings.length === 0 ? {} : { warnings: warnings.map((w) => `3.1: ${w}`) }),
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { d3Gap, getD3Graph } from './d3/index.js';
|
|
2
|
+
import { internalEdges } from './d3/types.js';
|
|
3
|
+
import { unavailable } from '../tools/outcome.js';
|
|
4
|
+
/** Violations per 1,000 internal import edges. */
|
|
5
|
+
export const p32 = {
|
|
6
|
+
id: '3.2',
|
|
7
|
+
layer: 'static',
|
|
8
|
+
async measure(ctx) {
|
|
9
|
+
const gap = d3Gap(ctx);
|
|
10
|
+
if (gap !== undefined)
|
|
11
|
+
return { status: 'not_measured', reason: gap };
|
|
12
|
+
const graph = await getD3Graph(ctx);
|
|
13
|
+
if ('unavailable' in graph)
|
|
14
|
+
return unavailable(graph.unavailable);
|
|
15
|
+
return {
|
|
16
|
+
status: 'measured',
|
|
17
|
+
units: graph.units.map((unit) => ({
|
|
18
|
+
package: unit.package,
|
|
19
|
+
group: unit.group,
|
|
20
|
+
weight: unit.weight,
|
|
21
|
+
raw: { kind: 'ratio', numerator: unit.violating.length, denominator: internalEdges(unit).length, scale: 1000 },
|
|
22
|
+
})),
|
|
23
|
+
...(graph.warnings.length === 0 ? {} : { warnings: graph.warnings.map((w) => `3.2: ${w}`) }),
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { d3Gap, getD3Graph } from './d3/index.js';
|
|
2
|
+
import { cyclicNodes } from './d3/scc.js';
|
|
3
|
+
import { unavailable } from '../tools/outcome.js';
|
|
4
|
+
/** Logic files in a strongly connected component of two or more files of the runtime import graph, per language group. */
|
|
5
|
+
export const p33 = {
|
|
6
|
+
id: '3.3',
|
|
7
|
+
layer: 'static',
|
|
8
|
+
async measure(ctx) {
|
|
9
|
+
const gap = d3Gap(ctx);
|
|
10
|
+
if (gap !== undefined)
|
|
11
|
+
return { status: 'not_measured', reason: gap };
|
|
12
|
+
const graph = await getD3Graph(ctx);
|
|
13
|
+
if ('unavailable' in graph)
|
|
14
|
+
return unavailable(graph.unavailable);
|
|
15
|
+
const cyclic = new Map();
|
|
16
|
+
for (const group of new Set(graph.units.map((u) => u.group))) {
|
|
17
|
+
const members = graph.units.filter((u) => u.group === group);
|
|
18
|
+
const nodes = members.flatMap((u) => u.logicFiles);
|
|
19
|
+
const edges = members.flatMap((u) => u.edges.filter((e) => e.runtime && !e.fromTest).map((e) => [e.from, e.to]));
|
|
20
|
+
cyclic.set(group, cyclicNodes(nodes, edges));
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
status: 'measured',
|
|
24
|
+
units: graph.units.map((unit) => ({
|
|
25
|
+
package: unit.package,
|
|
26
|
+
group: unit.group,
|
|
27
|
+
weight: unit.weight,
|
|
28
|
+
raw: {
|
|
29
|
+
kind: 'ratio',
|
|
30
|
+
numerator: unit.logicFiles.filter((f) => cyclic.get(unit.group)?.has(f)).length,
|
|
31
|
+
denominator: unit.logicFiles.length,
|
|
32
|
+
scale: 100,
|
|
33
|
+
},
|
|
34
|
+
})),
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { d3Gap, getD3Graph } from './d3/index.js';
|
|
2
|
+
import { unavailable } from '../tools/outcome.js';
|
|
3
|
+
/** Cross-module imports that land on the module's public entry. Not applicable without two modules in a unit. */
|
|
4
|
+
export const p34 = {
|
|
5
|
+
id: '3.4',
|
|
6
|
+
layer: 'static',
|
|
7
|
+
async measure(ctx) {
|
|
8
|
+
const gap = d3Gap(ctx);
|
|
9
|
+
if (gap !== undefined)
|
|
10
|
+
return { status: 'not_measured', reason: gap };
|
|
11
|
+
const graph = await getD3Graph(ctx);
|
|
12
|
+
if ('unavailable' in graph)
|
|
13
|
+
return unavailable(graph.unavailable);
|
|
14
|
+
const units = graph.units
|
|
15
|
+
.filter((unit) => unit.moduleCount >= 2)
|
|
16
|
+
.map((unit) => ({
|
|
17
|
+
package: unit.package,
|
|
18
|
+
group: unit.group,
|
|
19
|
+
weight: unit.weight,
|
|
20
|
+
raw: { kind: 'ratio', numerator: unit.crossings.filter((c) => c.toEntry).length, denominator: unit.crossings.length, scale: 100 },
|
|
21
|
+
}));
|
|
22
|
+
if (units.length === 0)
|
|
23
|
+
return { status: 'not_applicable', reason: ctx.param.not_applicable_when ?? 'fewer than two modules' };
|
|
24
|
+
return { status: 'measured', units };
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { d3Gap, getD3Graph } from './d3/index.js';
|
|
2
|
+
import { unavailable } from '../tools/outcome.js';
|
|
3
|
+
/** Domain files that import infrastructure. Not applicable without a domain layer. */
|
|
4
|
+
export const p35 = {
|
|
5
|
+
id: '3.5',
|
|
6
|
+
layer: 'static',
|
|
7
|
+
async measure(ctx) {
|
|
8
|
+
const gap = d3Gap(ctx);
|
|
9
|
+
if (gap !== undefined)
|
|
10
|
+
return { status: 'not_measured', reason: gap };
|
|
11
|
+
const graph = await getD3Graph(ctx);
|
|
12
|
+
if ('unavailable' in graph)
|
|
13
|
+
return unavailable(graph.unavailable);
|
|
14
|
+
const units = graph.units
|
|
15
|
+
.filter((unit) => unit.domain.files.length > 0)
|
|
16
|
+
.map((unit) => ({
|
|
17
|
+
package: unit.package,
|
|
18
|
+
group: unit.group,
|
|
19
|
+
weight: unit.weight,
|
|
20
|
+
raw: { kind: 'ratio', numerator: unit.domain.impure.length, denominator: unit.domain.files.length, scale: 100 },
|
|
21
|
+
}));
|
|
22
|
+
if (units.length === 0)
|
|
23
|
+
return { status: 'not_applicable', reason: ctx.param.not_applicable_when ?? 'no domain layer' };
|
|
24
|
+
return { status: 'measured', units };
|
|
25
|
+
},
|
|
26
|
+
};
|