vibecodedshit 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +95 -0
- package/README.md +73 -2
- package/assets/fonts/NotoEmoji-OFL.txt +93 -0
- package/assets/fonts/NotoEmoji-Poop.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-OFL.txt +93 -0
- package/dist/bin.js +11 -0
- package/dist/card/previous.js +38 -0
- package/dist/card/render.js +71 -0
- package/dist/cli.js +138 -0
- package/dist/commands/card.js +69 -0
- package/dist/commands/common.js +1 -0
- package/dist/commands/diff.js +54 -0
- package/dist/commands/footer.js +28 -0
- package/dist/commands/report.js +49 -0
- package/dist/commands/route.js +62 -0
- package/dist/commands/submit.js +125 -0
- package/dist/commands/summary.js +77 -0
- package/dist/diff/compare.js +73 -0
- package/dist/diff/conditions.js +87 -0
- package/dist/diff/format.js +23 -0
- package/dist/exec/docker.js +158 -0
- package/dist/exec/findings.js +73 -0
- package/dist/exec/plan.js +50 -0
- package/dist/exec/reports.js +90 -0
- package/dist/exec/roots.js +49 -0
- package/dist/exec/run.js +182 -0
- package/dist/exec/steps/build.js +45 -0
- package/dist/exec/steps/coverage-install.js +12 -0
- package/dist/exec/steps/install.js +41 -0
- package/dist/exec/steps/knip.js +28 -0
- package/dist/exec/steps/lint.js +25 -0
- package/dist/exec/steps/lockfiles.js +25 -0
- package/dist/exec/steps/test.js +17 -0
- package/dist/exec/steps/typecheck.js +40 -0
- package/dist/exec/test-reports/coverage-py.js +23 -0
- package/dist/exec/test-reports/coverage-ranges.js +65 -0
- package/dist/exec/test-reports/coverage-summary.js +23 -0
- package/dist/exec/test-reports/jest-json.js +31 -0
- package/dist/exec/test-reports/junit.js +24 -0
- package/dist/exec/test-reports/mocha-json.js +18 -0
- package/dist/exec/test-reports/vitest-json.js +5 -0
- package/dist/exec/test-runners/detect.js +99 -0
- package/dist/exec/test-runners/index.js +6 -0
- package/dist/exec/test-runners/jest.js +27 -0
- package/dist/exec/test-runners/mocha.js +28 -0
- package/dist/exec/test-runners/node-test.js +24 -0
- package/dist/exec/test-runners/pytest.js +18 -0
- package/dist/exec/test-runners/script.js +48 -0
- package/dist/exec/test-runners/spec.js +1 -0
- package/dist/exec/test-runners/vitest.js +28 -0
- package/dist/exec/tests.js +120 -0
- package/dist/exec/types.js +1 -0
- package/dist/interactive/after-scan.js +121 -0
- package/dist/interactive/docker-setup.js +97 -0
- package/dist/interactive/prompt.js +17 -0
- package/dist/interactive/system.js +10 -0
- package/dist/manifest/package-json.js +36 -0
- package/dist/manifest/python.js +106 -0
- package/dist/measure/d3/index.js +45 -0
- package/dist/measure/d3/presets.js +100 -0
- package/dist/measure/d3/py/adapter.js +67 -0
- package/dist/measure/d3/py/config.js +80 -0
- package/dist/measure/d3/py/contracts.js +57 -0
- package/dist/measure/d3/py/domain.js +57 -0
- package/dist/measure/d3/py/graph.js +214 -0
- package/dist/measure/d3/py/interpreter.js +31 -0
- package/dist/measure/d3/py/layers.js +120 -0
- package/dist/measure/d3/py/roots.js +58 -0
- package/dist/measure/d3/py/types.js +1 -0
- package/dist/measure/d3/scc.js +64 -0
- package/dist/measure/d3/ts/adapter.js +371 -0
- package/dist/measure/d3/ts/classify.js +107 -0
- package/dist/measure/d3/ts/config.js +318 -0
- package/dist/measure/d3/ts/domain.js +23 -0
- package/dist/measure/d3/ts/rules.js +65 -0
- package/dist/measure/d3/types.js +5 -0
- package/dist/measure/deps/cvss.js +66 -0
- package/dist/measure/deps/declared.js +195 -0
- package/dist/measure/deps/files.js +40 -0
- package/dist/measure/deps/lockfiles.js +308 -0
- package/dist/measure/deps/osv.js +122 -0
- package/dist/measure/deps/registry.js +142 -0
- package/dist/measure/deps/resolved.js +78 -0
- package/dist/measure/deps/units.js +5 -0
- package/dist/measure/eslint/health-plugin.js +19 -0
- package/dist/measure/ops/docs-commands.js +73 -0
- package/dist/measure/ops/docs-resolve.js +217 -0
- package/dist/measure/ops/entry-points.js +49 -0
- package/dist/measure/ops/env-example.js +20 -0
- package/dist/measure/ops/env-reads.js +46 -0
- package/dist/measure/ops/runtime.js +73 -0
- package/dist/measure/p1-1.js +8 -0
- package/dist/measure/p1-2.js +8 -0
- package/dist/measure/p1-3.js +8 -0
- package/dist/measure/p1-4.js +41 -0
- package/dist/measure/p1-5.js +59 -0
- package/dist/measure/p2-1.js +8 -0
- package/dist/measure/p2-2.js +44 -0
- package/dist/measure/p2-3.js +81 -0
- package/dist/measure/p2-4.js +288 -0
- package/dist/measure/p2-5.js +8 -0
- package/dist/measure/p3-1.js +16 -0
- package/dist/measure/p3-2.js +26 -0
- package/dist/measure/p3-3.js +37 -0
- package/dist/measure/p3-4.js +26 -0
- package/dist/measure/p3-5.js +26 -0
- package/dist/measure/p4-1.js +184 -0
- package/dist/measure/p4-2.js +27 -0
- package/dist/measure/p4-3.js +106 -0
- package/dist/measure/p4-4.js +309 -0
- package/dist/measure/p5-1.js +117 -0
- package/dist/measure/p5-2.js +80 -0
- package/dist/measure/p5-3.js +21 -0
- package/dist/measure/p5-4.js +161 -0
- package/dist/measure/p5-5.js +158 -0
- package/dist/measure/p6-1.js +20 -0
- package/dist/measure/p6-2.js +20 -0
- package/dist/measure/p6-3.js +40 -0
- package/dist/measure/p6-4.js +31 -0
- package/dist/measure/p6-5.js +60 -0
- package/dist/measure/p6-6.js +54 -0
- package/dist/measure/py/typecheck-config.js +95 -0
- package/dist/measure/registry.js +31 -0
- package/dist/measure/safety/py-tests.js +124 -0
- package/dist/measure/safety/ts-barrels.js +68 -0
- package/dist/measure/safety/ts-cases.js +111 -0
- package/dist/measure/safety/ts-credits.js +101 -0
- package/dist/measure/safety/ts-imports.js +63 -0
- package/dist/measure/safety/ts-resolve.js +43 -0
- package/dist/measure/ts/directives.js +37 -0
- package/dist/measure/ts/exports.js +60 -0
- package/dist/measure/ts/tsconfig.js +179 -0
- package/dist/measure/types.js +1 -0
- package/dist/measure/units.js +41 -0
- package/dist/methodology/load.js +159 -0
- package/dist/methodology/schema.js +105 -0
- package/dist/python/deadcode.js +27 -0
- package/dist/python/env.js +13 -0
- package/dist/python/helper.js +24 -0
- package/dist/python/imports.js +40 -0
- package/dist/python/nox.js +16 -0
- package/dist/python/scope.js +35 -0
- package/dist/python/tests.js +19 -0
- package/dist/python/typing.js +27 -0
- package/dist/report/escape.js +5 -0
- package/dist/report/format.js +75 -0
- package/dist/report/render.js +213 -0
- package/dist/report/roadmap.js +58 -0
- package/dist/report/scares.js +83 -0
- package/dist/report/style.js +27 -0
- package/dist/report-only/hotspots.js +48 -0
- package/dist/route/check.js +37 -0
- package/dist/route/files.js +74 -0
- package/dist/route/order.js +22 -0
- package/dist/route/select.js +114 -0
- package/dist/route/write.js +13 -0
- package/dist/run/read.js +89 -0
- package/dist/run/recompute.js +19 -0
- package/dist/run/scan.js +196 -0
- package/dist/run/schema.js +1 -0
- package/dist/run/score-run.js +42 -0
- package/dist/run/write.js +4 -0
- package/dist/scope/build.js +173 -0
- package/dist/scope/packages.js +87 -0
- package/dist/scope/rules.js +21 -0
- package/dist/scope/tracked.js +34 -0
- package/dist/scope/ts-lines.js +63 -0
- package/dist/scope/types.js +7 -0
- package/dist/scoring/aggregate.js +38 -0
- package/dist/scoring/pool.js +65 -0
- package/dist/scoring/score.js +81 -0
- package/dist/scoring/what-if.js +38 -0
- package/dist/scripts/checks.js +53 -0
- package/dist/scripts/ci.js +139 -0
- package/dist/scripts/entry-points/find.js +31 -0
- package/dist/scripts/entry-points/index.js +18 -0
- package/dist/scripts/entry-points/justfile.js +62 -0
- package/dist/scripts/entry-points/makefile.js +66 -0
- package/dist/scripts/entry-points/nox.js +29 -0
- package/dist/scripts/entry-points/package-json.js +21 -0
- package/dist/scripts/entry-points/pyproject.js +73 -0
- package/dist/scripts/entry-points/tox.js +130 -0
- package/dist/scripts/expand.js +300 -0
- package/dist/scripts/triggers.js +151 -0
- package/dist/scripts/types.js +1 -0
- package/dist/submit/constants.js +10 -0
- package/dist/submit/payload.js +42 -0
- package/dist/submit/repo-identity.js +46 -0
- package/dist/submit/send.js +29 -0
- package/dist/tools/catalog.js +46 -0
- package/dist/tools/image.js +180 -0
- package/dist/tools/kit.js +71 -0
- package/dist/tools/knip.js +169 -0
- package/dist/tools/outcome.js +21 -0
- package/dist/tools/resolve.js +66 -0
- package/dist/tools/workcopy.js +27 -0
- package/dist/util/ini.js +37 -0
- package/dist/util/pep440.js +163 -0
- package/dist/util/toml.js +27 -0
- package/docker/Dockerfile +142 -0
- package/docker/bin/health-capture +5 -0
- package/docker/bin/health-coverage-install +5 -0
- package/docker/bin/health-eslint +66 -0
- package/docker/bin/health-pytest +10 -0
- package/docker/bin/health-tsc +10 -0
- package/docker/bin/health-venv +11 -0
- package/docker/config/eslint.config.mjs +4 -0
- package/docker/config/eslint.rules.mjs +65 -0
- package/docker/config/eslint.untyped.config.mjs +4 -0
- package/docker/config/knip.json +1 -0
- package/docker/config/ruff.toml +7 -0
- package/methodology/LICENSE +396 -0
- package/methodology/v1.0/METHODOLOGY.md +1176 -0
- package/methodology/v1.0/dimensions/1-safety-net.yaml +240 -0
- package/methodology/v1.0/dimensions/2-type-static-rigor.yaml +289 -0
- package/methodology/v1.0/dimensions/3-architecture-boundaries.yaml +266 -0
- package/methodology/v1.0/dimensions/4-complexity-maintainability.yaml +192 -0
- package/methodology/v1.0/dimensions/5-dependency-security-hygiene.yaml +223 -0
- package/methodology/v1.0/dimensions/6-operability-feedback-loop.yaml +279 -0
- package/methodology/v1.0/methodology.yaml +242 -0
- package/methodology/v1.0/presets/django.yaml +43 -0
- package/methodology/v1.0/presets/express.yaml +47 -0
- package/methodology/v1.0/presets/fastapi.yaml +52 -0
- package/methodology/v1.0/presets/generic.yaml +41 -0
- package/methodology/v1.0/presets/nextjs.yaml +50 -0
- package/methodology/v1.0/scope.yaml +115 -0
- package/package.json +61 -4
- package/python/dead_code_helper.py +265 -0
- package/python/env_helper.py +71 -0
- package/python/graph_helper.py +277 -0
- package/python/imports_helper.py +132 -0
- package/python/nox_helper.py +79 -0
- package/python/scope_helper.py +96 -0
- package/python/tests_helper.py +171 -0
- package/python/typing_helper.py +235 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { trackedPaths } from '../../scope/tracked.js';
|
|
5
|
+
const STRICT_FAMILY = [
|
|
6
|
+
'noImplicitAny',
|
|
7
|
+
'strictNullChecks',
|
|
8
|
+
'strictFunctionTypes',
|
|
9
|
+
'strictBindCallApply',
|
|
10
|
+
'strictPropertyInitialization',
|
|
11
|
+
'noImplicitThis',
|
|
12
|
+
'useUnknownInCatchVariables',
|
|
13
|
+
'alwaysStrict',
|
|
14
|
+
];
|
|
15
|
+
const LEVEL_ORDER = ['none', 'partial', 'strict', 'strict_plus'];
|
|
16
|
+
const posix = (p) => p.split(path.sep).join('/');
|
|
17
|
+
const host = {
|
|
18
|
+
...ts.sys,
|
|
19
|
+
onUnRecoverableConfigFileDiagnostic: () => undefined,
|
|
20
|
+
};
|
|
21
|
+
/** Parse one tsconfig or jsconfig with `extends` resolved by the TypeScript API. JSON only, nothing is executed. */
|
|
22
|
+
function loadTsConfig(file, cache) {
|
|
23
|
+
const key = posix(file);
|
|
24
|
+
if (cache.has(key))
|
|
25
|
+
return cache.get(key);
|
|
26
|
+
let info;
|
|
27
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(file, {}, host);
|
|
28
|
+
if (parsed) {
|
|
29
|
+
const raw = parsed.raw;
|
|
30
|
+
const references = (parsed.projectReferences ?? []).map((ref) => {
|
|
31
|
+
const resolved = ts.resolveProjectReferencePath(ref);
|
|
32
|
+
return posix(resolved);
|
|
33
|
+
});
|
|
34
|
+
const missingExtends = [];
|
|
35
|
+
for (const error of parsed.errors) {
|
|
36
|
+
if (error.code !== 6053)
|
|
37
|
+
continue; // File '{0}' not found.
|
|
38
|
+
const match = /'([^']+)'/.exec(ts.flattenDiagnosticMessageText(error.messageText, '\n'));
|
|
39
|
+
if (match?.[1])
|
|
40
|
+
missingExtends.push(match[1]);
|
|
41
|
+
}
|
|
42
|
+
info = {
|
|
43
|
+
file: key,
|
|
44
|
+
options: parsed.options,
|
|
45
|
+
fileNames: new Set(parsed.fileNames.map(posix)),
|
|
46
|
+
references,
|
|
47
|
+
solution: Array.isArray(raw?.files) && raw.files.length === 0 && references.length > 0,
|
|
48
|
+
missingExtends,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
cache.set(key, info);
|
|
52
|
+
return info;
|
|
53
|
+
}
|
|
54
|
+
/** The level a set of compiler options declares: every flag is its explicit value, else `strict`, else false. */
|
|
55
|
+
export function levelOf(options) {
|
|
56
|
+
const bag = options;
|
|
57
|
+
const strict = bag.strict === true;
|
|
58
|
+
const effective = (flag) => (typeof bag[flag] === 'boolean' ? bag[flag] === true : strict);
|
|
59
|
+
const turnedOff = STRICT_FAMILY.some((flag) => bag[flag] === false);
|
|
60
|
+
if (strict && !turnedOff)
|
|
61
|
+
return options.noUncheckedIndexedAccess === true ? 'strict_plus' : 'strict';
|
|
62
|
+
return STRICT_FAMILY.some(effective) ? 'partial' : 'none';
|
|
63
|
+
}
|
|
64
|
+
export function lowestLevel(levels) {
|
|
65
|
+
return levels.reduce((low, level) => (LEVEL_ORDER.indexOf(level) < LEVEL_ORDER.indexOf(low) ? level : low), 'strict_plus');
|
|
66
|
+
}
|
|
67
|
+
function reachable(root, cache) {
|
|
68
|
+
const seen = new Set([root.file]);
|
|
69
|
+
const out = [];
|
|
70
|
+
const queue = [...root.references];
|
|
71
|
+
while (queue.length > 0) {
|
|
72
|
+
const next = queue.shift();
|
|
73
|
+
if (next === undefined || seen.has(next))
|
|
74
|
+
continue;
|
|
75
|
+
seen.add(next);
|
|
76
|
+
const info = loadTsConfig(next, cache);
|
|
77
|
+
if (!info)
|
|
78
|
+
continue;
|
|
79
|
+
out.push(info);
|
|
80
|
+
queue.push(...info.references);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function covers(config, files) {
|
|
85
|
+
for (const file of config.fileNames)
|
|
86
|
+
if (files.has(file))
|
|
87
|
+
return true;
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
function configIn(dir) {
|
|
91
|
+
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
|
92
|
+
const candidate = path.join(dir, name);
|
|
93
|
+
if (existsSync(candidate))
|
|
94
|
+
return candidate;
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The configs that apply to one unit: the package's own `tsconfig.json`, else its `jsconfig.json`,
|
|
100
|
+
* else the nearest ancestor config up to the repository root, kept only if it (or a config it
|
|
101
|
+
* references) covers a logic file of the unit. A solution-style config is replaced by the configs
|
|
102
|
+
* reachable through its `references` that cover a logic file of the unit (the config itself if none do).
|
|
103
|
+
*/
|
|
104
|
+
export function unitConfigs(repoPath, packageDir, logicFiles, cache) {
|
|
105
|
+
const files = new Set(logicFiles.map((f) => posix(path.resolve(repoPath, f))));
|
|
106
|
+
const root = path.resolve(repoPath);
|
|
107
|
+
const start = path.resolve(repoPath, packageDir);
|
|
108
|
+
let chosen = configIn(start);
|
|
109
|
+
let ancestor = false;
|
|
110
|
+
if (!chosen) {
|
|
111
|
+
ancestor = true;
|
|
112
|
+
for (let dir = path.dirname(start);; dir = path.dirname(dir)) {
|
|
113
|
+
if (dir.length < root.length)
|
|
114
|
+
break;
|
|
115
|
+
chosen = configIn(dir);
|
|
116
|
+
if (chosen || dir === root)
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!chosen)
|
|
121
|
+
return { configs: [], read: [] };
|
|
122
|
+
const top = loadTsConfig(chosen, cache);
|
|
123
|
+
if (!top)
|
|
124
|
+
return { configs: [], read: [] };
|
|
125
|
+
const nested = reachable(top, cache);
|
|
126
|
+
const read = [top, ...nested];
|
|
127
|
+
if (ancestor && ![top, ...nested].some((c) => covers(c, files)))
|
|
128
|
+
return { configs: [], read: [] };
|
|
129
|
+
if (!top.solution)
|
|
130
|
+
return { configs: [top], read };
|
|
131
|
+
const covering = nested.filter((c) => !c.solution && covers(c, files));
|
|
132
|
+
return { configs: covering.length > 0 ? covering : [top], read };
|
|
133
|
+
}
|
|
134
|
+
/** The nearest `tsconfig.json`, else `jsconfig.json`, from `dir` up to the repository root. Absolute path. */
|
|
135
|
+
export function nearestTsConfig(repoPath, dir) {
|
|
136
|
+
const root = path.resolve(repoPath);
|
|
137
|
+
for (let current = path.resolve(root, dir); current.length >= root.length; current = path.dirname(current)) {
|
|
138
|
+
const found = configIn(current);
|
|
139
|
+
if (found)
|
|
140
|
+
return found;
|
|
141
|
+
if (current === root)
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const optionsCache = new Map();
|
|
147
|
+
/**
|
|
148
|
+
* The compiler options of the nearest `tsconfig.json`, else `jsconfig.json`, with `extends` chains resolved through a host that only
|
|
149
|
+
* sees tracked files (so `node_modules` is never read). Without a config: `Bundler` resolution, ESNext modules, `allowJs`, JSON modules.
|
|
150
|
+
*/
|
|
151
|
+
export function compilerOptionsFor(repoPath, dir) {
|
|
152
|
+
const fallback = {
|
|
153
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
154
|
+
module: ts.ModuleKind.ESNext,
|
|
155
|
+
allowJs: true,
|
|
156
|
+
resolveJsonModule: true,
|
|
157
|
+
};
|
|
158
|
+
const configFile = nearestTsConfig(repoPath, dir);
|
|
159
|
+
if (!configFile)
|
|
160
|
+
return fallback;
|
|
161
|
+
const cached = optionsCache.get(configFile);
|
|
162
|
+
if (cached)
|
|
163
|
+
return cached;
|
|
164
|
+
const root = path.resolve(repoPath);
|
|
165
|
+
const tracked = new Set(trackedPaths(repoPath));
|
|
166
|
+
const rel = (abs) => path.posix.relative(posix(root), posix(abs));
|
|
167
|
+
const visible = (abs) => tracked.has(rel(abs));
|
|
168
|
+
const configHost = {
|
|
169
|
+
useCaseSensitiveFileNames: true,
|
|
170
|
+
readDirectory: () => [],
|
|
171
|
+
fileExists: visible,
|
|
172
|
+
readFile: (abs) => (visible(abs) ? ts.sys.readFile(abs) : undefined),
|
|
173
|
+
};
|
|
174
|
+
const read = ts.readConfigFile(configFile, (abs) => configHost.readFile(abs));
|
|
175
|
+
const parsed = read.error ? undefined : ts.parseJsonConfigFileContent(read.config, configHost, path.dirname(configFile));
|
|
176
|
+
const options = { ...fallback, ...(parsed?.options ?? {}), allowJs: true };
|
|
177
|
+
optionsCache.set(configFile, options);
|
|
178
|
+
return options;
|
|
179
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function logicFiles(scope, group) {
|
|
2
|
+
return scope.files.filter((f) => f.role === 'logic' && f.group === group);
|
|
3
|
+
}
|
|
4
|
+
export function unitId(file) {
|
|
5
|
+
return `${file.package}\0${file.group}`;
|
|
6
|
+
}
|
|
7
|
+
/** One tally per (package, group) that has logic files, weight = logic lines of the unit. */
|
|
8
|
+
export function emptyTallies(files) {
|
|
9
|
+
const tallies = new Map();
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
const key = unitId(file);
|
|
12
|
+
const tally = tallies.get(key) ?? { package: file.package, group: file.group, weight: 0, numerator: 0, denominator: 0 };
|
|
13
|
+
tally.weight += file.logicLines;
|
|
14
|
+
tallies.set(key, tally);
|
|
15
|
+
}
|
|
16
|
+
return tallies;
|
|
17
|
+
}
|
|
18
|
+
export function tallyUnits(tallies) {
|
|
19
|
+
return [...tallies.values()]
|
|
20
|
+
.sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group))
|
|
21
|
+
.map((t) => ({
|
|
22
|
+
package: t.package,
|
|
23
|
+
group: t.group,
|
|
24
|
+
weight: t.weight,
|
|
25
|
+
raw: { kind: 'ratio', numerator: t.numerator, denominator: t.denominator, scale: 100 },
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
/** One unit per (workspace package, language group) that has at least one logic-role file, sorted by package then group. */
|
|
29
|
+
export function logicUnits(scope) {
|
|
30
|
+
const units = new Map();
|
|
31
|
+
for (const file of scope.files) {
|
|
32
|
+
if (file.role !== 'logic')
|
|
33
|
+
continue;
|
|
34
|
+
const key = unitId(file);
|
|
35
|
+
const unit = units.get(key) ?? { package: file.package, group: file.group, dir: file.package, weight: 0, files: [] };
|
|
36
|
+
unit.weight += file.logicLines;
|
|
37
|
+
unit.files.push(file);
|
|
38
|
+
units.set(key, unit);
|
|
39
|
+
}
|
|
40
|
+
return [...units.values()].sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group));
|
|
41
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
import { dimensionFileSchema, methodologyFileSchema, presetFileSchema, scopeFileSchema, } from './schema.js';
|
|
6
|
+
export class MethodologyError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'MethodologyError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Package root: the directory holding `methodology/`, both from `src/` and from `dist/`. */
|
|
13
|
+
/** The methodology version a scan uses when `--methodology` is not given. */
|
|
14
|
+
export const CURRENT_METHODOLOGY_VERSION = '1.0';
|
|
15
|
+
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
16
|
+
function readYaml(file, schema) {
|
|
17
|
+
let doc;
|
|
18
|
+
try {
|
|
19
|
+
doc = parse(readFileSync(file, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
throw new MethodologyError(`${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
23
|
+
}
|
|
24
|
+
const result = schema.safeParse(doc);
|
|
25
|
+
if (!result.success) {
|
|
26
|
+
const issue = result.error.issues[0];
|
|
27
|
+
const where = issue && issue.path.length > 0 ? issue.path.join('.') : '(root)';
|
|
28
|
+
throw new MethodologyError(`${file}: ${where}: ${issue?.message ?? 'invalid'}`);
|
|
29
|
+
}
|
|
30
|
+
return result.data;
|
|
31
|
+
}
|
|
32
|
+
function commaList(text) {
|
|
33
|
+
return text
|
|
34
|
+
.split(',')
|
|
35
|
+
.map((entry) => entry.trim().replace(/^node:/, ''))
|
|
36
|
+
.filter((entry) => entry !== '');
|
|
37
|
+
}
|
|
38
|
+
function parseD3Lists(def, file = 'dimension 3') {
|
|
39
|
+
const lists = { forbiddenImports: { typescript_javascript: [], python: [] }, infrastructureDirectories: [] };
|
|
40
|
+
if (!def)
|
|
41
|
+
return lists;
|
|
42
|
+
const measure = def.measure ?? {};
|
|
43
|
+
for (const group of ['typescript_javascript', 'python']) {
|
|
44
|
+
const match = /Forbidden imports:\s*(.+?)\.(?:\s|$)/s.exec(measure[group] ?? '');
|
|
45
|
+
if (!match?.[1])
|
|
46
|
+
throw new MethodologyError(`${file}: parameter ${def.id} measure.${group} has no "Forbidden imports:" list`);
|
|
47
|
+
lists.forbiddenImports[group] = [...new Set(commaList(match[1]))];
|
|
48
|
+
}
|
|
49
|
+
const infra = (measure.infrastructure_directories ?? '').replace(/\.\s*$/, '');
|
|
50
|
+
lists.infrastructureDirectories = commaList(infra);
|
|
51
|
+
if (lists.infrastructureDirectories.length === 0) {
|
|
52
|
+
throw new MethodologyError(`${file}: parameter ${def.id} measure.infrastructure_directories is empty`);
|
|
53
|
+
}
|
|
54
|
+
return lists;
|
|
55
|
+
}
|
|
56
|
+
function words(text) {
|
|
57
|
+
return text
|
|
58
|
+
.replace(/\s+/g, ' ')
|
|
59
|
+
.replace(/,?\s+(?:and|or)\s+/g, ', ')
|
|
60
|
+
.split(/[,;]/)
|
|
61
|
+
.map((entry) => entry.trim())
|
|
62
|
+
.filter((entry) => entry !== '');
|
|
63
|
+
}
|
|
64
|
+
function parseD6Lists(scriptResolution, defs, defs6x, file = 'dimension 6') {
|
|
65
|
+
const text = (scriptResolution ?? '').replace(/\s+/g, ' ');
|
|
66
|
+
const pick = (re, what) => {
|
|
67
|
+
const match = re.exec(text);
|
|
68
|
+
if (!match?.[1])
|
|
69
|
+
throw new MethodologyError(`${file}: script_resolution has no ${what} list`);
|
|
70
|
+
return words(match[1]);
|
|
71
|
+
};
|
|
72
|
+
const typecheckers = pick(/typecheckers (.+?);/, 'typecheckers');
|
|
73
|
+
const linters = pick(/linters (.+?);/, 'linters');
|
|
74
|
+
const testRunners = pick(/test runners (.+?)\. The prefixes/, 'test runners');
|
|
75
|
+
const prefixes = pick(/The prefixes (.+?) are removed/, 'prefixes');
|
|
76
|
+
const env = defs?.scoring.method === 'mean_of_components' ? defs.scoring.components.find((c) => c.key === 'env_documented') : undefined;
|
|
77
|
+
const runtime = defs?.scoring.method === 'mean_of_components' ? defs.scoring.components.find((c) => c.key === 'runtime_pinned') : undefined;
|
|
78
|
+
const envMeasure = (env?.measure ?? '').replace(/\s+/g, ' ');
|
|
79
|
+
const examples = /Example files: (.+?)\. Reads:/.exec(envMeasure)?.[1];
|
|
80
|
+
const excluded = /Excluded names: (.+?), and names starting with (.+?)\.?$/.exec(envMeasure);
|
|
81
|
+
if (!examples || !excluded?.[1] || !excluded[2])
|
|
82
|
+
throw new MethodologyError(`${file}: parameter 6.5 env_documented measure has no example files or excluded names`);
|
|
83
|
+
const pins = /^Any of (.+?)\.?$/.exec((runtime?.measure ?? '').replace(/\s+/g, ' '))?.[1];
|
|
84
|
+
if (!pins)
|
|
85
|
+
throw new MethodologyError(`${file}: parameter 6.5 runtime_pinned measure has no file list`);
|
|
86
|
+
const docMeasure = (defs6x?.measure?.all_languages ?? '').replace(/\s+/g, ' ');
|
|
87
|
+
const managers = /starts with a package manager \((.+?)\)/.exec(docMeasure)?.[1];
|
|
88
|
+
if (!managers)
|
|
89
|
+
throw new MethodologyError(`${file}: parameter 6.6 measure has no package manager list`);
|
|
90
|
+
return {
|
|
91
|
+
typecheckers,
|
|
92
|
+
linters,
|
|
93
|
+
testRunners,
|
|
94
|
+
prefixes,
|
|
95
|
+
envExcludedNames: words(excluded[1]),
|
|
96
|
+
envExcludedPrefixes: words(excluded[2]),
|
|
97
|
+
envExampleFiles: words(examples),
|
|
98
|
+
runtimePinFiles: words(pins),
|
|
99
|
+
docPackageManagers: words(managers),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function loadMethodology(version, rootDir = packageRoot) {
|
|
103
|
+
if (!/^\d+\.\d+$/.test(version)) {
|
|
104
|
+
throw new MethodologyError(`Unknown methodology version "${version}"`);
|
|
105
|
+
}
|
|
106
|
+
const dir = path.join(rootDir, 'methodology', `v${version}`);
|
|
107
|
+
if (!existsSync(dir)) {
|
|
108
|
+
throw new MethodologyError(`Unknown methodology version "${version}" (no ${dir})`);
|
|
109
|
+
}
|
|
110
|
+
const main = readYaml(path.join(dir, 'methodology.yaml'), methodologyFileSchema);
|
|
111
|
+
if (main.version !== version) {
|
|
112
|
+
throw new MethodologyError(`${path.join(dir, 'methodology.yaml')}: version "${main.version}" does not match directory v${version}`);
|
|
113
|
+
}
|
|
114
|
+
const scope = readYaml(path.join(dir, 'scope.yaml'), scopeFileSchema);
|
|
115
|
+
const presets = {};
|
|
116
|
+
const dimensions = main.dimensions.map((entry) => {
|
|
117
|
+
const file = path.join(dir, entry.file);
|
|
118
|
+
const parsed = readYaml(file, dimensionFileSchema);
|
|
119
|
+
if (parsed.dimension.id !== entry.id || parsed.dimension.key !== entry.key) {
|
|
120
|
+
throw new MethodologyError(`${file}: dimension id/key do not match methodology.yaml`);
|
|
121
|
+
}
|
|
122
|
+
const total = parsed.parameters.reduce((sum, p) => sum + p.weight, 0);
|
|
123
|
+
if (Math.abs(total - 100) > 1e-9) {
|
|
124
|
+
throw new MethodologyError(`${file}: parameter weights sum to ${total}, expected 100`);
|
|
125
|
+
}
|
|
126
|
+
for (const presetFile of parsed.presets?.files ?? []) {
|
|
127
|
+
const loaded = readYaml(path.join(dir, presetFile), presetFileSchema);
|
|
128
|
+
presets[loaded.preset.key] = {
|
|
129
|
+
preset: loaded.preset.key,
|
|
130
|
+
name: loaded.preset.name,
|
|
131
|
+
...(loaded.conventions === undefined ? {} : { conventions: loaded.conventions }),
|
|
132
|
+
languages: loaded.preset.languages,
|
|
133
|
+
layers: loaded.layers,
|
|
134
|
+
modules: loaded.modules,
|
|
135
|
+
rules: loaded.rules,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
id: parsed.dimension.id,
|
|
140
|
+
key: parsed.dimension.key,
|
|
141
|
+
name: parsed.dimension.name,
|
|
142
|
+
...(parsed.script_resolution === undefined ? {} : { scriptResolution: parsed.script_resolution }),
|
|
143
|
+
parameters: parsed.parameters,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
const d3Def = dimensions.find((d) => d.id === 3)?.parameters.find((p) => p.id === '3.5');
|
|
147
|
+
const d6Dim = dimensions.find((d) => d.id === 6);
|
|
148
|
+
return {
|
|
149
|
+
version,
|
|
150
|
+
presets,
|
|
151
|
+
d3: parseD3Lists(d3Def, path.join(dir, 'dimensions')),
|
|
152
|
+
d6: parseD6Lists(d6Dim?.scriptResolution, d6Dim?.parameters.find((p) => p.id === '6.5'), d6Dim?.parameters.find((p) => p.id === '6.6'), path.join(dir, 'dimensions')),
|
|
153
|
+
dimensions,
|
|
154
|
+
capMargin: main.aggregation.dimension_to_health.cap_margin,
|
|
155
|
+
bands: main.bands,
|
|
156
|
+
scope,
|
|
157
|
+
execution: main.execution,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const anchor = z.object({ raw: z.number(), score: z.number(), descriptor: z.string().optional() });
|
|
3
|
+
const levelEntry = z.object({ level: z.string(), score: z.number(), descriptor: z.string().optional() });
|
|
4
|
+
const component = z.object({
|
|
5
|
+
key: z.string(),
|
|
6
|
+
method: z.enum(['levels', 'ratio']),
|
|
7
|
+
levels: z.array(levelEntry).optional(),
|
|
8
|
+
definition: z.string().optional(),
|
|
9
|
+
measure: z.string().optional(),
|
|
10
|
+
});
|
|
11
|
+
const scoring = z.discriminatedUnion('method', [
|
|
12
|
+
z.object({ method: z.literal('piecewise_linear'), anchors: z.array(anchor).min(2) }),
|
|
13
|
+
z.object({
|
|
14
|
+
method: z.literal('levels'),
|
|
15
|
+
levels: z.union([z.array(levelEntry), z.record(z.string(), z.array(levelEntry))]),
|
|
16
|
+
}),
|
|
17
|
+
z.object({ method: z.literal('mean_of_components'), components: z.array(component).min(1) }),
|
|
18
|
+
]);
|
|
19
|
+
const parameterSchema = z.object({
|
|
20
|
+
id: z.string(),
|
|
21
|
+
key: z.string(),
|
|
22
|
+
name: z.string(),
|
|
23
|
+
layer: z.enum(['static', 'execution']),
|
|
24
|
+
weight: z.number(),
|
|
25
|
+
pooling: z.enum(['ratio', 'unit_weighted']),
|
|
26
|
+
raw: z.object({ unit: z.string(), when_empty: z.number() }),
|
|
27
|
+
scoring,
|
|
28
|
+
/** Prose per language or topic, for example the 3.5 forbidden-import lists. */
|
|
29
|
+
measure: z.record(z.string(), z.string()).optional(),
|
|
30
|
+
not_applicable_when: z.string().nullable().optional(),
|
|
31
|
+
});
|
|
32
|
+
export const dimensionFileSchema = z.object({
|
|
33
|
+
dimension: z.object({ id: z.number(), key: z.string(), name: z.string() }),
|
|
34
|
+
/** Dimension 6 prose naming the recognised tools and the removed prefixes. */
|
|
35
|
+
script_resolution: z.string().optional(),
|
|
36
|
+
parameters: z.array(parameterSchema).min(1),
|
|
37
|
+
presets: z.object({ files: z.array(z.string()) }).optional(),
|
|
38
|
+
});
|
|
39
|
+
const stringList = z.array(z.string());
|
|
40
|
+
const presetRule = z.object({
|
|
41
|
+
name: z.string(),
|
|
42
|
+
description: z.string(),
|
|
43
|
+
from: z.object({ in: stringList.optional(), not_in: stringList.optional() }),
|
|
44
|
+
to: z.object({ in: stringList.optional(), packages: stringList.optional() }),
|
|
45
|
+
severity: z.enum(['error', 'warn', 'info']),
|
|
46
|
+
type_only_imports: z.enum(['included', 'excluded']),
|
|
47
|
+
});
|
|
48
|
+
export const presetFileSchema = z.object({
|
|
49
|
+
preset: z.object({
|
|
50
|
+
key: z.enum(['nextjs', 'express', 'django', 'fastapi', 'generic']),
|
|
51
|
+
name: z.string(),
|
|
52
|
+
languages: stringList,
|
|
53
|
+
}),
|
|
54
|
+
conventions: z.string().optional(),
|
|
55
|
+
/** Glob lists, or prose for content layers (nextjs `server_only` and `client`). */
|
|
56
|
+
layers: z.record(z.string(), z.union([stringList, z.string()])),
|
|
57
|
+
modules: z.object({
|
|
58
|
+
roots: z.union([stringList, z.string()]),
|
|
59
|
+
public_entry: z.union([z.string(), stringList, z.record(z.string(), z.string())]),
|
|
60
|
+
}),
|
|
61
|
+
rules: z.array(presetRule),
|
|
62
|
+
});
|
|
63
|
+
export const methodologyFileSchema = z.object({
|
|
64
|
+
version: z.string(),
|
|
65
|
+
dimensions: z.array(z.object({ id: z.number(), key: z.string(), file: z.string() })).min(1),
|
|
66
|
+
aggregation: z.object({
|
|
67
|
+
dimension_to_health: z.object({ cap_margin: z.number() }),
|
|
68
|
+
}),
|
|
69
|
+
bands: z
|
|
70
|
+
.array(z.object({ key: z.string(), name: z.string(), min: z.number(), max: z.number(), meaning: z.string().optional() }))
|
|
71
|
+
.min(1),
|
|
72
|
+
execution: z.object({
|
|
73
|
+
runner: z.object({ image: z.string(), cpus: z.number(), memory_gb: z.number() }),
|
|
74
|
+
timeouts_seconds: z.object({
|
|
75
|
+
install: z.number(),
|
|
76
|
+
build: z.number(),
|
|
77
|
+
typecheck: z.number(),
|
|
78
|
+
lint: z.number(),
|
|
79
|
+
tests: z.number(),
|
|
80
|
+
}),
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
const globList = z.array(z.string());
|
|
84
|
+
export const scopeFileSchema = z.object({
|
|
85
|
+
source_files: z.object({
|
|
86
|
+
extensions: z.object({
|
|
87
|
+
typescript: globList,
|
|
88
|
+
javascript: globList,
|
|
89
|
+
python: globList,
|
|
90
|
+
}),
|
|
91
|
+
}),
|
|
92
|
+
exclusions: z.object({
|
|
93
|
+
vendored: globList,
|
|
94
|
+
build_output: globList,
|
|
95
|
+
generated: globList,
|
|
96
|
+
minified: globList,
|
|
97
|
+
migrations: globList,
|
|
98
|
+
declarations: globList,
|
|
99
|
+
test_data: globList,
|
|
100
|
+
}),
|
|
101
|
+
file_roles: z.object({
|
|
102
|
+
test: z.object({ typescript_javascript: globList, python: globList }),
|
|
103
|
+
config: globList,
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ToolError } from '../scope/types.js';
|
|
5
|
+
const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'python', 'dead_code_helper.py');
|
|
6
|
+
/** Symbols, `__all__`, imports and class bases of every file, with one helper process. */
|
|
7
|
+
export function analyzePythonDeadCode(repoPath, files) {
|
|
8
|
+
if (files.length === 0)
|
|
9
|
+
return {};
|
|
10
|
+
const result = spawnSync('python3', [helperPath], {
|
|
11
|
+
cwd: repoPath,
|
|
12
|
+
input: JSON.stringify({ files }),
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
15
|
+
});
|
|
16
|
+
if (result.error)
|
|
17
|
+
throw new ToolError(`Could not run python3: ${result.error.message}`);
|
|
18
|
+
if (result.status !== 0) {
|
|
19
|
+
throw new ToolError(`dead_code_helper.py failed: ${result.stderr.trim() || `exit ${String(result.status)}`}`);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(result.stdout);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new ToolError('dead_code_helper.py printed invalid JSON');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { runPythonHelper } from './helper.js';
|
|
3
|
+
const cache = new Map();
|
|
4
|
+
/** Environment variable names read by each given Python file: one helper spawn per scan, memoized per repository. */
|
|
5
|
+
export function pythonEnvReads(repoPath, files) {
|
|
6
|
+
const key = path.resolve(repoPath);
|
|
7
|
+
const cached = cache.get(key);
|
|
8
|
+
if (cached)
|
|
9
|
+
return cached;
|
|
10
|
+
const reads = files.length === 0 ? {} : runPythonHelper('env_helper.py', repoPath, { files });
|
|
11
|
+
cache.set(key, reads);
|
|
12
|
+
return reads;
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ToolError } from '../scope/types.js';
|
|
5
|
+
const pythonDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'python');
|
|
6
|
+
/** Run one stdlib-only helper from `python/` with JSON in and JSON out. A missing interpreter or a crashed helper is a ToolError. */
|
|
7
|
+
export function runPythonHelper(helper, repoPath, input) {
|
|
8
|
+
const result = spawnSync('python3', [path.join(pythonDir, helper)], {
|
|
9
|
+
cwd: repoPath,
|
|
10
|
+
input: JSON.stringify(input),
|
|
11
|
+
encoding: 'utf8',
|
|
12
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
13
|
+
});
|
|
14
|
+
if (result.error)
|
|
15
|
+
throw new ToolError(`Could not run python3: ${result.error.message}`);
|
|
16
|
+
if (result.status !== 0)
|
|
17
|
+
throw new ToolError(`${helper} failed: ${result.stderr.trim() || `exit ${String(result.status)}`}`);
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(result.stdout);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new ToolError(`${helper} printed invalid JSON`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ToolError } from '../scope/types.js';
|
|
5
|
+
const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'python', 'imports_helper.py');
|
|
6
|
+
function runHelper(repoPath, files) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn('python3', [helperPath], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
9
|
+
let stdout = '';
|
|
10
|
+
let stderr = '';
|
|
11
|
+
child.stdout.on('data', (chunk) => {
|
|
12
|
+
stdout += chunk.toString('utf8');
|
|
13
|
+
});
|
|
14
|
+
child.stderr.on('data', (chunk) => {
|
|
15
|
+
stderr += chunk.toString('utf8');
|
|
16
|
+
});
|
|
17
|
+
child.on('error', (err) => reject(new ToolError(`Could not run python3: ${err.message}`)));
|
|
18
|
+
child.on('close', (code) => {
|
|
19
|
+
if (code !== 0) {
|
|
20
|
+
reject(new ToolError(`imports_helper.py failed: ${stderr.trim() || `exit ${String(code)}`}`));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
resolve(JSON.parse(stdout).files);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
reject(new ToolError('imports_helper.py printed invalid JSON'));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
child.stdin.on('error', () => undefined);
|
|
31
|
+
child.stdin.end(JSON.stringify({ root: path.resolve(repoPath), files }));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Import statements and builtin `open` calls of every in-scope Python file, one helper process per scan. */
|
|
35
|
+
export function readPythonImports(ctx) {
|
|
36
|
+
return ctx.tools.memo(`d3-py-imports:${ctx.repoPath}`, () => {
|
|
37
|
+
const files = ctx.scope.files.filter((f) => f.group === 'python').map((f) => f.path);
|
|
38
|
+
return files.length === 0 ? Promise.resolve({}) : runHelper(ctx.repoPath, files);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { trackedPaths } from '../scope/tracked.js';
|
|
3
|
+
import { runPythonHelper } from './helper.js';
|
|
4
|
+
const cache = new Map();
|
|
5
|
+
/** The sessions of the tracked `noxfile.py` in `file`'s directory (one helper spawn per scan, memoized per repository). Undefined when it does not parse. */
|
|
6
|
+
export function noxSessions(repoPath, file) {
|
|
7
|
+
const key = path.resolve(repoPath);
|
|
8
|
+
let all = cache.get(key);
|
|
9
|
+
if (!all) {
|
|
10
|
+
const files = trackedPaths(repoPath).filter((f) => path.posix.basename(f) === 'noxfile.py');
|
|
11
|
+
all = files.length === 0 ? {} : runPythonHelper('nox_helper.py', repoPath, { files });
|
|
12
|
+
cache.set(key, all);
|
|
13
|
+
}
|
|
14
|
+
const found = all[file];
|
|
15
|
+
return found?.ok ? found.sessions : undefined;
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ToolError } from '../scope/types.js';
|
|
5
|
+
const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'python', 'scope_helper.py');
|
|
6
|
+
export function pythonVersion() {
|
|
7
|
+
const result = spawnSync('python3', ['--version'], { encoding: 'utf8' });
|
|
8
|
+
if (result.error || result.status !== 0) {
|
|
9
|
+
throw new ToolError('python3 is not available on PATH, but the repository has Python files');
|
|
10
|
+
}
|
|
11
|
+
return `${result.stdout}${result.stderr}`.trim().replace(/^Python\s+/, '');
|
|
12
|
+
}
|
|
13
|
+
/** Classify all Python files with one helper process. */
|
|
14
|
+
export function analyzePythonFiles(repoPath, files) {
|
|
15
|
+
if (files.length === 0)
|
|
16
|
+
return {};
|
|
17
|
+
const result = spawnSync('python3', [helperPath], {
|
|
18
|
+
cwd: repoPath,
|
|
19
|
+
input: JSON.stringify({ files }),
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
22
|
+
});
|
|
23
|
+
if (result.error) {
|
|
24
|
+
throw new ToolError(`Could not run python3: ${result.error.message}`);
|
|
25
|
+
}
|
|
26
|
+
if (result.status !== 0) {
|
|
27
|
+
throw new ToolError(`scope_helper.py failed: ${result.stderr.trim() || `exit ${result.status}`}`);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(result.stdout);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new ToolError('scope_helper.py printed invalid JSON');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { trackedPaths } from '../scope/tracked.js';
|
|
3
|
+
import { runPythonHelper } from './helper.js';
|
|
4
|
+
const cache = new Map();
|
|
5
|
+
const NOT_FIRST_PARTY = /(?:^|\/)(?:node_modules|\.venv|venv|site-packages|__pycache__)\//;
|
|
6
|
+
/**
|
|
7
|
+
* Import and test-case facts of every tracked Python file, packages that only re-export included (they are not scoped files but
|
|
8
|
+
* 1.4 follows imports through them). One helper spawn per scan, memoized per repository.
|
|
9
|
+
*/
|
|
10
|
+
export function pythonTestFacts(repoPath) {
|
|
11
|
+
const key = path.resolve(repoPath);
|
|
12
|
+
const cached = cache.get(key);
|
|
13
|
+
if (cached)
|
|
14
|
+
return cached;
|
|
15
|
+
const files = trackedPaths(repoPath).filter((f) => f.endsWith('.py') && !NOT_FIRST_PARTY.test(f));
|
|
16
|
+
const facts = files.length === 0 ? {} : runPythonHelper('tests_helper.py', repoPath, { files });
|
|
17
|
+
cache.set(key, facts);
|
|
18
|
+
return facts;
|
|
19
|
+
}
|