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,142 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import { compareVersions, isStable, normalizeName, parseVersion } from '../../util/pep440.js';
|
|
3
|
+
import { endOfDay } from './osv.js';
|
|
4
|
+
/** Anything but a 404: the registry could not be read. */
|
|
5
|
+
export class RegistryError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'RegistryError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const TIMEOUT_MS = 20_000;
|
|
12
|
+
export const CONCURRENCY = 8;
|
|
13
|
+
async function getJson(ctx, url) {
|
|
14
|
+
let lastError = 'unknown error';
|
|
15
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
16
|
+
try {
|
|
17
|
+
const response = await ctx.tools.fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
18
|
+
if (response.status === 404)
|
|
19
|
+
return 'not_found';
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
lastError = `HTTP ${response.status}`;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
return (await response.json());
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw new RegistryError(`registry request ${url} failed: ${lastError}`);
|
|
31
|
+
}
|
|
32
|
+
function record(value) {
|
|
33
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
34
|
+
}
|
|
35
|
+
function npmVersions(doc) {
|
|
36
|
+
const time = record(doc.time) ?? {};
|
|
37
|
+
const found = [];
|
|
38
|
+
for (const version of Object.keys(record(doc.versions) ?? {})) {
|
|
39
|
+
const published = typeof time[version] === 'string' ? Date.parse(time[version]) : Number.NaN;
|
|
40
|
+
if (!Number.isNaN(published))
|
|
41
|
+
found.push({ version, time: published, yanked: false });
|
|
42
|
+
}
|
|
43
|
+
return found;
|
|
44
|
+
}
|
|
45
|
+
function pypiVersions(doc) {
|
|
46
|
+
const found = [];
|
|
47
|
+
for (const [version, files] of Object.entries(record(doc.releases) ?? {})) {
|
|
48
|
+
if (!Array.isArray(files) || files.length === 0)
|
|
49
|
+
continue;
|
|
50
|
+
const entries = files.map((f) => record(f)).filter((f) => f !== undefined);
|
|
51
|
+
const times = entries
|
|
52
|
+
.map((f) => (typeof f.upload_time_iso_8601 === 'string' ? Date.parse(f.upload_time_iso_8601) : Number.NaN))
|
|
53
|
+
.filter((t) => !Number.isNaN(t));
|
|
54
|
+
if (times.length === 0)
|
|
55
|
+
continue;
|
|
56
|
+
found.push({ version, time: Math.min(...times), yanked: entries.every((f) => f.yanked === true) });
|
|
57
|
+
}
|
|
58
|
+
return found;
|
|
59
|
+
}
|
|
60
|
+
/** Registry data for one package, memoized per scan. `null` when the registry has no such package (404). */
|
|
61
|
+
export function fetchRegistry(ctx, group, name) {
|
|
62
|
+
const key = group === 'python' ? `pypi:${normalizeName(name)}` : `npm:${name}`;
|
|
63
|
+
return ctx.tools.memo(key, async () => {
|
|
64
|
+
const url = group === 'python'
|
|
65
|
+
? `https://pypi.org/pypi/${encodeURIComponent(normalizeName(name))}/json`
|
|
66
|
+
: `https://registry.npmjs.org/${name.startsWith('@') ? name.replace('/', '%2F') : encodeURIComponent(name)}`;
|
|
67
|
+
const doc = await getJson(ctx, url);
|
|
68
|
+
if (doc === 'not_found')
|
|
69
|
+
return null;
|
|
70
|
+
if (!doc)
|
|
71
|
+
throw new RegistryError(`registry request ${url} returned no document`);
|
|
72
|
+
return { versions: group === 'python' ? pypiVersions(doc) : npmVersions(doc) };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Highest stable, non-yanked version published at or before the end of the as-of day. */
|
|
76
|
+
export function latestStable(data, group, asOf) {
|
|
77
|
+
const limit = endOfDay(asOf);
|
|
78
|
+
const candidates = data.versions.filter((v) => !v.yanked && v.time <= limit);
|
|
79
|
+
if (group === 'python') {
|
|
80
|
+
let top;
|
|
81
|
+
for (const { version } of candidates) {
|
|
82
|
+
const parsed = parseVersion(version);
|
|
83
|
+
if (!parsed || !isStable(parsed))
|
|
84
|
+
continue;
|
|
85
|
+
if (!top || compareVersions(parsed, top.parsed) > 0)
|
|
86
|
+
top = { text: version, parsed };
|
|
87
|
+
}
|
|
88
|
+
return top?.text;
|
|
89
|
+
}
|
|
90
|
+
return candidates
|
|
91
|
+
.map((v) => v.version)
|
|
92
|
+
.filter((v) => semver.valid(v) !== null && semver.prerelease(v) === null)
|
|
93
|
+
.sort(semver.compare)
|
|
94
|
+
.pop();
|
|
95
|
+
}
|
|
96
|
+
function parts(version, group) {
|
|
97
|
+
if (group === 'python') {
|
|
98
|
+
const parsed = parseVersion(version);
|
|
99
|
+
if (!parsed)
|
|
100
|
+
return undefined;
|
|
101
|
+
return {
|
|
102
|
+
release: parsed.release,
|
|
103
|
+
cmp: (other) => {
|
|
104
|
+
const o = parseVersion(other);
|
|
105
|
+
return o ? compareVersions(parsed, o) : 0;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const parsed = semver.parse(version);
|
|
110
|
+
if (!parsed)
|
|
111
|
+
return undefined;
|
|
112
|
+
return { release: [parsed.major, parsed.minor, parsed.patch], cmp: (other) => semver.compare(version, other) };
|
|
113
|
+
}
|
|
114
|
+
/** At least one major behind; for 0.x a minor bump counts as a major. A resolved version newer than latest is not behind. */
|
|
115
|
+
export function isMajorBehind(resolved, latest, group) {
|
|
116
|
+
const r = parts(resolved, group);
|
|
117
|
+
const l = parts(latest, group);
|
|
118
|
+
if (!r || !l)
|
|
119
|
+
return false;
|
|
120
|
+
if (r.cmp(latest) >= 0)
|
|
121
|
+
return false;
|
|
122
|
+
const [rMajor = 0, rMinor = 0] = r.release;
|
|
123
|
+
const [lMajor = 0, lMinor = 0] = l.release;
|
|
124
|
+
if (lMajor > rMajor)
|
|
125
|
+
return true;
|
|
126
|
+
return rMajor === 0 && lMajor === 0 && lMinor > rMinor;
|
|
127
|
+
}
|
|
128
|
+
/** Population and behind count of `direct` against one registry snapshot. Keys of `registryData`: `<group>:<name>`. */
|
|
129
|
+
export function majorsBehind(direct, registryData, asOf) {
|
|
130
|
+
let numerator = 0;
|
|
131
|
+
let denominator = 0;
|
|
132
|
+
for (const dep of direct) {
|
|
133
|
+
const data = registryData.get(`${dep.group}:${dep.group === 'python' ? normalizeName(dep.name) : dep.name}`);
|
|
134
|
+
if (!data)
|
|
135
|
+
continue;
|
|
136
|
+
denominator += 1;
|
|
137
|
+
const latest = latestStable(data, dep.group, asOf);
|
|
138
|
+
if (latest !== undefined && isMajorBehind(dep.version, latest, dep.group))
|
|
139
|
+
numerator += 1;
|
|
140
|
+
}
|
|
141
|
+
return { numerator, denominator };
|
|
142
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
import { compareVersions, parseVersion, satisfies as pySatisfies } from '../../util/pep440.js';
|
|
4
|
+
import { readManifest } from './declared.js';
|
|
5
|
+
import { trackedIn } from './files.js';
|
|
6
|
+
import { chooseJsLockfile, entrySatisfies, findLockfiles, jsLockfiles, readLockfile } from './lockfiles.js';
|
|
7
|
+
/** Manifest and lockfiles of one unit, read once per scan. */
|
|
8
|
+
export function unitDeps(ctx, pkg, group) {
|
|
9
|
+
return ctx.tools.memo(`deps:${pkg}\0${group}`, async () => {
|
|
10
|
+
const manifest = readManifest(ctx.repoPath, pkg, group);
|
|
11
|
+
const found = manifest ? findLockfiles(ctx.repoPath, pkg, group) : [];
|
|
12
|
+
const { refs, warnings } = group === 'typescript_javascript' ? installedLockfile(ctx.repoPath, found) : { refs: found, warnings: [] };
|
|
13
|
+
const lockfiles = refs.map((ref) => ({ ref, reader: readLockfile(ctx.repoPath, ref) }));
|
|
14
|
+
return { package: pkg, group, manifest, lockfiles, warnings };
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* With several JS lockfiles in the lockfile directory, keep only the one the install step uses
|
|
19
|
+
* (`chooseJsLockfile`) and warn. A binary `bun.lockb` cannot be read, so then every readable candidate stays.
|
|
20
|
+
*/
|
|
21
|
+
function installedLockfile(repoPath, refs) {
|
|
22
|
+
const dir = refs[0]?.dir;
|
|
23
|
+
if (dir === undefined)
|
|
24
|
+
return { refs, warnings: [] };
|
|
25
|
+
const names = jsLockfiles(trackedIn(repoPath, dir).map((file) => path.posix.basename(file)));
|
|
26
|
+
if (names.length < 2)
|
|
27
|
+
return { refs, warnings: [] };
|
|
28
|
+
const chosen = chooseJsLockfile(names);
|
|
29
|
+
const where = dir === '' ? 'the repository root' : dir;
|
|
30
|
+
const picked = refs.filter((ref) => path.posix.basename(ref.path) === chosen);
|
|
31
|
+
if (picked.length === 0) {
|
|
32
|
+
return { refs, warnings: [`5.3: ${where} has several JS lockfiles (${names.join(', ')}); install uses ${chosen}, which is binary, so 5.3 checks ${refs.map((r) => path.posix.basename(r.path)).join(', ')}`] };
|
|
33
|
+
}
|
|
34
|
+
return { refs: picked, warnings: [`5.3: ${where} has several JS lockfiles (${names.join(', ')}); install and 5.3 use ${chosen}`] };
|
|
35
|
+
}
|
|
36
|
+
/** The unit's package directory relative to a lockfile's directory ('' when they are the same). */
|
|
37
|
+
function relToLock(pkg, lockDir) {
|
|
38
|
+
const dir = pkg === '.' ? '' : pkg;
|
|
39
|
+
if (lockDir === '')
|
|
40
|
+
return dir;
|
|
41
|
+
return dir === lockDir ? '' : path.posix.relative(lockDir, dir);
|
|
42
|
+
}
|
|
43
|
+
export function lockLevel(unit) {
|
|
44
|
+
if (!unit.manifest || unit.lockfiles.length === 0)
|
|
45
|
+
return 'none';
|
|
46
|
+
const inSync = unit.lockfiles.some(({ ref, reader }) => {
|
|
47
|
+
if (!reader)
|
|
48
|
+
return false;
|
|
49
|
+
const rel = relToLock(unit.package, ref.dir);
|
|
50
|
+
return (unit.manifest?.deps ?? []).every((dep) => entrySatisfies(dep, reader.lookup(dep, rel), unit.group));
|
|
51
|
+
});
|
|
52
|
+
return inSync ? 'in_sync' : 'out_of_sync';
|
|
53
|
+
}
|
|
54
|
+
function best(versions, dep, group) {
|
|
55
|
+
const fits = versions.filter((v) => group === 'python' ? pySatisfies(v, dep.spec) : semver.satisfies(v, dep.spec, { includePrerelease: true }));
|
|
56
|
+
const pool = fits.length > 0 ? fits : versions;
|
|
57
|
+
if (group === 'python') {
|
|
58
|
+
return [...pool].sort((a, b) => {
|
|
59
|
+
const pa = parseVersion(a);
|
|
60
|
+
const pb = parseVersion(b);
|
|
61
|
+
return pa && pb ? compareVersions(pb, pa) : 0;
|
|
62
|
+
})[0];
|
|
63
|
+
}
|
|
64
|
+
return pool[0];
|
|
65
|
+
}
|
|
66
|
+
/** Declared direct dependencies with the version the first lockfile holding them resolves. */
|
|
67
|
+
export function resolveDirect(unit) {
|
|
68
|
+
return (unit.manifest?.deps ?? []).map((dep) => {
|
|
69
|
+
for (const { ref, reader } of unit.lockfiles) {
|
|
70
|
+
if (!reader)
|
|
71
|
+
continue;
|
|
72
|
+
const entry = reader.lookup(dep, relToLock(unit.package, ref.dir));
|
|
73
|
+
if (entry.present)
|
|
74
|
+
return { name: dep.name, version: best(entry.versions, dep, unit.group), dev: dep.dev, registry: dep.registry };
|
|
75
|
+
}
|
|
76
|
+
return { name: dep.name, version: undefined, dev: dep.dev, registry: dep.registry };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { emptyTallies, logicFiles } from '../units.js';
|
|
2
|
+
/** Scope units: every (package, language group) with logic files, weight = its logic lines. */
|
|
3
|
+
export function scopeUnits(ctx) {
|
|
4
|
+
return [...emptyTallies([...logicFiles(ctx.scope, 'typescript_javascript'), ...logicFiles(ctx.scope, 'python')]).values()].sort((a, b) => a.package.localeCompare(b.package) || a.group.localeCompare(b.group));
|
|
5
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports once for every function node (declarations, expressions, arrows, methods).
|
|
3
|
+
* `sonarjs/cognitive-complexity` registers on the same `:function` selector, so this is the
|
|
4
|
+
* denominator for 4.1: all functions as enumerated by the tool.
|
|
5
|
+
*/
|
|
6
|
+
const functionRule = {
|
|
7
|
+
meta: { type: 'problem', schema: [], messages: { fn: 'function' } },
|
|
8
|
+
create(context) {
|
|
9
|
+
return {
|
|
10
|
+
':function'(node) {
|
|
11
|
+
context.report({ node, messageId: 'fn' });
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
export const healthPlugin = {
|
|
17
|
+
meta: { name: 'health' },
|
|
18
|
+
rules: { function: functionRule },
|
|
19
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { splitSimple, tokenize } from '../../scripts/expand.js';
|
|
2
|
+
const ALIASES = { bunx: ['npx'], uvx: ['uv', 'run'], pip3: ['pip'], python3: ['python'] };
|
|
3
|
+
const SKIPPED = new Set(['sudo']);
|
|
4
|
+
/** First word of a simple command after `VAR=value` words and `sudo`; aliases (`bunx`, `uvx`, `pip3`, `python3`) map to their base word. */
|
|
5
|
+
function leadWord(tokens) {
|
|
6
|
+
let rest = tokens;
|
|
7
|
+
while (rest[0] !== undefined && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(rest[0]) || SKIPPED.has(rest[0])))
|
|
8
|
+
rest = rest.slice(1);
|
|
9
|
+
const first = rest[0];
|
|
10
|
+
if (first === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
const alias = ALIASES[first];
|
|
13
|
+
if (alias)
|
|
14
|
+
rest = [...alias, ...rest.slice(1)];
|
|
15
|
+
return { word: rest[0], rest };
|
|
16
|
+
}
|
|
17
|
+
/** Candidates from fenced blocks (every line) and inline code spans (one per span), before splitting into simple commands. */
|
|
18
|
+
function candidates(text) {
|
|
19
|
+
const out = [];
|
|
20
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
21
|
+
let fence;
|
|
22
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
23
|
+
const line = lines[i] ?? '';
|
|
24
|
+
const marker = /^\s*(```+|~~~+)/.exec(line)?.[1];
|
|
25
|
+
if (marker !== undefined) {
|
|
26
|
+
fence = fence === undefined ? marker[0] : fence === marker[0] ? undefined : fence;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (fence === undefined) {
|
|
30
|
+
for (const span of line.matchAll(/`+([^`\n]+)`+/g))
|
|
31
|
+
out.push({ line: i + 1, text: (span[1] ?? '').trim(), inline: true });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
let candidate = line.trim();
|
|
35
|
+
const start = i + 1;
|
|
36
|
+
while (candidate.endsWith('\\') && i + 1 < lines.length) {
|
|
37
|
+
i += 1;
|
|
38
|
+
candidate = `${candidate.slice(0, -1).trimEnd()} ${(lines[i] ?? '').trim()}`;
|
|
39
|
+
}
|
|
40
|
+
candidate = candidate.replace(/^[$>]\s+/, '');
|
|
41
|
+
if (candidate === '' || candidate.startsWith('#'))
|
|
42
|
+
continue;
|
|
43
|
+
out.push({ line: start, text: candidate, inline: false });
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The documented commands of a file. A command is a simple command whose first word is a package manager or recognised tool.
|
|
49
|
+
* An inline span is a candidate only when it has a space (`npm` alone is not a command; `package.json` and `src/index.ts` are not).
|
|
50
|
+
*/
|
|
51
|
+
export function extractCommands(text, words) {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const candidate of candidates(text)) {
|
|
54
|
+
if (candidate.inline && !/\s/.test(candidate.text))
|
|
55
|
+
continue;
|
|
56
|
+
let cd = '.';
|
|
57
|
+
for (const simple of splitSimple(candidate.text)) {
|
|
58
|
+
const tokens = tokenize(simple.text);
|
|
59
|
+
const lead = leadWord(tokens);
|
|
60
|
+
if (!lead)
|
|
61
|
+
continue;
|
|
62
|
+
if (lead.word === 'cd' && lead.rest[1] !== undefined && !/[$`*?]/.test(lead.rest[1])) {
|
|
63
|
+
if (simple.sep === '&&')
|
|
64
|
+
cd = cd === '.' ? lead.rest[1].replace(/^\.\//, '') : `${cd}/${lead.rest[1]}`;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!words.commandWords.has(lead.word))
|
|
68
|
+
continue;
|
|
69
|
+
out.push({ line: candidate.line, text: simple.text.trim(), cd, tokens: [lead.word, ...lead.rest.slice(1)] });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readPackageJson } from '../../manifest/package-json.js';
|
|
3
|
+
import { pythonDeclaredDeps } from '../../manifest/python.js';
|
|
4
|
+
import { trackedPaths } from '../../scope/tracked.js';
|
|
5
|
+
import { findFile } from '../../scripts/entry-points/find.js';
|
|
6
|
+
import { justfileEntryPoints } from '../../scripts/entry-points/justfile.js';
|
|
7
|
+
import { makefileEntryPoints } from '../../scripts/entry-points/makefile.js';
|
|
8
|
+
import { noxEntryPoints } from '../../scripts/entry-points/nox.js';
|
|
9
|
+
import { packageJsonEntryPoints } from '../../scripts/entry-points/package-json.js';
|
|
10
|
+
import { pyprojectEntryPoints } from '../../scripts/entry-points/pyproject.js';
|
|
11
|
+
import { toxEntryPoints } from '../../scripts/entry-points/tox.js';
|
|
12
|
+
import { dirsUpward } from './entry-points.js';
|
|
13
|
+
const NODE_BUILTINS = new Set([
|
|
14
|
+
'install', 'i', 'ci', 'add', 'remove', 'update', 'up', 'link', 'init', 'dlx', 'audit', 'outdated', 'publish', 'pack', 'version', 'config',
|
|
15
|
+
'cache', 'rebuild', 'prune', 'dedupe', 'why', 'ls', 'list', 'x',
|
|
16
|
+
]);
|
|
17
|
+
const UV_BUILTINS = new Set(['sync', 'lock', 'add', 'remove', 'venv', 'pip', 'python', 'tool', 'init', 'build', 'publish']);
|
|
18
|
+
const POETRY_BUILTINS = new Set(['install', 'add', 'remove', 'lock', 'shell', 'build', 'publish', 'init', 'update', 'show', 'env', 'config']);
|
|
19
|
+
const PDM_BUILTINS = new Set(['install', 'add', 'remove', 'lock', 'sync', 'build', 'publish', 'init']);
|
|
20
|
+
const PYTHON_STDLIB = new Set(['venv', 'pip', 'http.server', 'json.tool', 'unittest', 'pytest', 'ensurepip', 'build', 'compileall', 'timeit', 'zipapp', 'http', 'site', 'sysconfig']);
|
|
21
|
+
const WORKSPACE_FLAGS = new Set(['-w', '--workspace', '--workspaces', '--filter', '-F', '-r', '--recursive', '--cwd']);
|
|
22
|
+
const VALUE_FLAGS = new Set(['--prefix', '-C', '--cwd', '--filter', '-F', '--workspace', '--directory']);
|
|
23
|
+
const MAKEFILES = ['Makefile', 'makefile', 'GNUmakefile'];
|
|
24
|
+
const join = (dir, next) => (next === undefined ? dir : path.posix.normalize(path.posix.join(dir, next)));
|
|
25
|
+
export class DocResolver {
|
|
26
|
+
repoPath;
|
|
27
|
+
tracked;
|
|
28
|
+
manifests;
|
|
29
|
+
toolNames;
|
|
30
|
+
constructor(repoPath, lists) {
|
|
31
|
+
this.repoPath = repoPath;
|
|
32
|
+
this.tracked = new Set(trackedPaths(repoPath));
|
|
33
|
+
this.toolNames = new Set([...lists.typecheckers, ...lists.linters, ...lists.testRunners].map((t) => (t === 'node --test' ? 'node' : t.replace(/^python3? -m /, ''))));
|
|
34
|
+
}
|
|
35
|
+
allManifests() {
|
|
36
|
+
this.manifests ??= [...this.tracked]
|
|
37
|
+
.filter((f) => path.posix.basename(f) === 'package.json')
|
|
38
|
+
.map((f) => {
|
|
39
|
+
const dir = path.posix.dirname(f);
|
|
40
|
+
const pkg = readPackageJson(this.repoPath, dir);
|
|
41
|
+
return { dir, deps: new Set(Object.keys(pkg?.dependencies ?? {})), scripts: new Set(Object.keys(pkg?.scripts ?? {})) };
|
|
42
|
+
});
|
|
43
|
+
return this.manifests;
|
|
44
|
+
}
|
|
45
|
+
hasScript(dir, name) {
|
|
46
|
+
return packageJsonEntryPoints.lookup(this.repoPath, dir, name) !== undefined;
|
|
47
|
+
}
|
|
48
|
+
declaredPython(dir, name) {
|
|
49
|
+
const wanted = name.toLowerCase().replace(/[-_.]+/g, '-');
|
|
50
|
+
return dirsUpward(dir).some((d) => pythonDeclaredDeps(this.repoPath, d).has(wanted));
|
|
51
|
+
}
|
|
52
|
+
firstPartyModule(dir, name) {
|
|
53
|
+
const top = name.split('.').join('/');
|
|
54
|
+
return [dir, 'src'].some((base) => [`${top}.py`, `${top}/__init__.py`, `${top}/__main__.py`].some((f) => this.tracked.has(path.posix.normalize(path.posix.join(base, f)))));
|
|
55
|
+
}
|
|
56
|
+
/** `npx <t>`, `uv run <t>`, `python -m <t>` and a bare recognised tool: resolves for a script, a recognised tool, a declared dependency, a stdlib or first-party module. */
|
|
57
|
+
toolRef(tool, dir, runner) {
|
|
58
|
+
if (tool === undefined)
|
|
59
|
+
return true;
|
|
60
|
+
if (runner === 'python -m' && (PYTHON_STDLIB.has(tool) || this.firstPartyModule(dir, tool)))
|
|
61
|
+
return true;
|
|
62
|
+
if ((runner === 'uv run' || runner === 'poetry run') && pyprojectEntryPoints.lookup(this.repoPath, dir, `scripts:${tool}`) !== undefined)
|
|
63
|
+
return true;
|
|
64
|
+
if (runner === 'pdm run' && pyprojectEntryPoints.lookup(this.repoPath, dir, `pdm:${tool}`) !== undefined)
|
|
65
|
+
return true;
|
|
66
|
+
if (this.toolNames.has(tool))
|
|
67
|
+
return true;
|
|
68
|
+
if (this.allManifests().some((m) => m.deps.has(tool)))
|
|
69
|
+
return true;
|
|
70
|
+
return this.declaredPython(dir, tool);
|
|
71
|
+
}
|
|
72
|
+
nodeCommand(runner, tokens, baseDir) {
|
|
73
|
+
let dir = baseDir;
|
|
74
|
+
let workspace = false;
|
|
75
|
+
const args = [];
|
|
76
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
77
|
+
const token = tokens[i];
|
|
78
|
+
if (token === '--')
|
|
79
|
+
break;
|
|
80
|
+
if (WORKSPACE_FLAGS.has(token))
|
|
81
|
+
workspace = true;
|
|
82
|
+
if (token === '--prefix' || token === '-C' || token === '--cwd') {
|
|
83
|
+
dir = join(baseDir, tokens[i + 1]);
|
|
84
|
+
i += 1;
|
|
85
|
+
}
|
|
86
|
+
else if (VALUE_FLAGS.has(token))
|
|
87
|
+
i += 1;
|
|
88
|
+
else if (token.startsWith('--prefix='))
|
|
89
|
+
dir = join(baseDir, token.slice(9));
|
|
90
|
+
else if (!token.startsWith('-'))
|
|
91
|
+
args.push(token);
|
|
92
|
+
}
|
|
93
|
+
const sub = args[0];
|
|
94
|
+
if (sub === undefined || NODE_BUILTINS.has(sub))
|
|
95
|
+
return true;
|
|
96
|
+
if (sub === 'exec' || (runner === 'bun' && sub === 'x'))
|
|
97
|
+
return this.toolRef(args[1], dir, runner === 'pnpm' ? 'pnpm exec' : runner);
|
|
98
|
+
let name;
|
|
99
|
+
let toolFallback = false;
|
|
100
|
+
if (sub === 'workspace')
|
|
101
|
+
name = args[2];
|
|
102
|
+
else if (sub === 'run' || sub === 'run-script')
|
|
103
|
+
name = args[1];
|
|
104
|
+
else if (['test', 'start', 'stop', 'restart'].includes(sub))
|
|
105
|
+
name = sub;
|
|
106
|
+
else if (runner === 'yarn' || runner === 'pnpm') {
|
|
107
|
+
name = sub;
|
|
108
|
+
toolFallback = true;
|
|
109
|
+
}
|
|
110
|
+
else
|
|
111
|
+
return true;
|
|
112
|
+
if (name === undefined)
|
|
113
|
+
return true;
|
|
114
|
+
if (workspace || sub === 'workspace' ? this.allManifests().some((m) => m.scripts.has(name)) : this.hasScript(dir, name))
|
|
115
|
+
return true;
|
|
116
|
+
if (name === 'start' && runner === 'npm' && this.tracked.has(path.posix.join(dir, 'server.js')))
|
|
117
|
+
return true;
|
|
118
|
+
return toolFallback && this.toolRef(name, dir, runner);
|
|
119
|
+
}
|
|
120
|
+
makeCommand(tokens, baseDir) {
|
|
121
|
+
let dir = baseDir;
|
|
122
|
+
const targets = [];
|
|
123
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
124
|
+
const token = tokens[i];
|
|
125
|
+
if (token === '-C' || token === '--directory') {
|
|
126
|
+
dir = join(baseDir, tokens[i + 1]);
|
|
127
|
+
i += 1;
|
|
128
|
+
}
|
|
129
|
+
else if (token === '-f' || token === '--file')
|
|
130
|
+
i += 1;
|
|
131
|
+
else if (!token.startsWith('-') && !token.includes('='))
|
|
132
|
+
targets.push(token);
|
|
133
|
+
}
|
|
134
|
+
const names = new Set(makefileEntryPoints.list(this.repoPath, dir).map((e) => e.name));
|
|
135
|
+
const exists = MAKEFILES.some((m) => this.tracked.has(path.posix.join(dir, m)));
|
|
136
|
+
return targets.length === 0 ? exists : targets.every((t) => names.has(t));
|
|
137
|
+
}
|
|
138
|
+
justCommand(tokens, baseDir) {
|
|
139
|
+
let dir = baseDir;
|
|
140
|
+
const recipes = [];
|
|
141
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
142
|
+
const token = tokens[i];
|
|
143
|
+
if (token === '-d' || token === '--working-directory') {
|
|
144
|
+
dir = join(baseDir, tokens[i + 1]);
|
|
145
|
+
i += 1;
|
|
146
|
+
}
|
|
147
|
+
else if (token === '-f' || token === '--justfile')
|
|
148
|
+
i += 1;
|
|
149
|
+
else if (!token.startsWith('-') && !token.includes('='))
|
|
150
|
+
recipes.push(token);
|
|
151
|
+
}
|
|
152
|
+
if (recipes.length === 0)
|
|
153
|
+
return findFile(this.repoPath, dir, ['justfile', 'Justfile', '.justfile'], true) !== undefined;
|
|
154
|
+
return recipes.every((r) => justfileEntryPoints.lookup(this.repoPath, dir, r) !== undefined);
|
|
155
|
+
}
|
|
156
|
+
/** Whether one documented command resolves. `dir` is the document's directory with the command's `cd` already applied. */
|
|
157
|
+
resolves(command, docDir) {
|
|
158
|
+
const dir = join(docDir, command.cd === '.' ? undefined : command.cd);
|
|
159
|
+
const tokens = command.tokens;
|
|
160
|
+
const first = tokens[0];
|
|
161
|
+
switch (first) {
|
|
162
|
+
case 'npm':
|
|
163
|
+
case 'pnpm':
|
|
164
|
+
case 'yarn':
|
|
165
|
+
case 'bun':
|
|
166
|
+
return this.nodeCommand(first, tokens, dir);
|
|
167
|
+
case 'npx': {
|
|
168
|
+
const tool = tokens.slice(1).find((t) => !t.startsWith('-'));
|
|
169
|
+
return this.toolRef(tool, dir, 'npx');
|
|
170
|
+
}
|
|
171
|
+
case 'make':
|
|
172
|
+
return this.makeCommand(tokens, dir);
|
|
173
|
+
case 'just':
|
|
174
|
+
return this.justCommand(tokens, dir);
|
|
175
|
+
case 'pip':
|
|
176
|
+
case 'pipenv':
|
|
177
|
+
return true;
|
|
178
|
+
case 'uv':
|
|
179
|
+
case 'poetry':
|
|
180
|
+
case 'pdm': {
|
|
181
|
+
const builtins = first === 'uv' ? UV_BUILTINS : first === 'poetry' ? POETRY_BUILTINS : PDM_BUILTINS;
|
|
182
|
+
const sub = tokens[1];
|
|
183
|
+
if (sub === undefined || builtins.has(sub))
|
|
184
|
+
return true;
|
|
185
|
+
if (sub !== 'run')
|
|
186
|
+
return first === 'uv';
|
|
187
|
+
const tool = tokens.slice(2).find((t) => !t.startsWith('-'));
|
|
188
|
+
if (tool === 'python' || tool === 'python3')
|
|
189
|
+
return this.pythonCommand(tokens.slice(tokens.indexOf(tool)), dir);
|
|
190
|
+
return this.toolRef(tool, dir, `${first} run`);
|
|
191
|
+
}
|
|
192
|
+
case 'python':
|
|
193
|
+
return this.pythonCommand(tokens, dir);
|
|
194
|
+
case 'tox': {
|
|
195
|
+
const envs = tokens.flatMap((t, i) => (tokens[i - 1] === '-e' ? t.split(',') : []));
|
|
196
|
+
return envs.length === 0 ? toxEntryPoints.fileFor?.(this.repoPath, dir) !== undefined : envs.every((e) => toxEntryPoints.lookup(this.repoPath, dir, e) !== undefined);
|
|
197
|
+
}
|
|
198
|
+
case 'nox': {
|
|
199
|
+
const sessions = tokens.flatMap((t, i) => (['-s', '--session', '-e', '--sessions'].includes(tokens[i - 1] ?? '') ? t.split(',') : []));
|
|
200
|
+
return sessions.length === 0 ? noxEntryPoints.fileFor?.(this.repoPath, dir) !== undefined : sessions.every((s) => noxEntryPoints.lookup(this.repoPath, dir, s) !== undefined);
|
|
201
|
+
}
|
|
202
|
+
default:
|
|
203
|
+
return this.toolRef(first, dir, 'bare');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
pythonCommand(tokens, dir) {
|
|
207
|
+
const arg = tokens[1];
|
|
208
|
+
if (arg === undefined || arg === '-c' || arg === '-' || arg === '-m') {
|
|
209
|
+
if (arg === '-m')
|
|
210
|
+
return this.toolRef(tokens[2], dir, 'python -m');
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
if (arg.startsWith('-'))
|
|
214
|
+
return true;
|
|
215
|
+
return this.tracked.has(path.posix.normalize(path.posix.join(dir, arg)));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { checkKinds, UNRECOGNISED_RUNNERS } from '../../scripts/checks.js';
|
|
3
|
+
import { entryPointKinds } from '../../scripts/entry-points/index.js';
|
|
4
|
+
import { expandCommand } from '../../scripts/expand.js';
|
|
5
|
+
/** The directory itself and each ancestor up to the repository root ("."). */
|
|
6
|
+
export function dirsUpward(dir) {
|
|
7
|
+
const out = [];
|
|
8
|
+
for (let current = path.posix.normalize(dir === '' ? '.' : dir);; current = path.posix.dirname(current)) {
|
|
9
|
+
out.push(current);
|
|
10
|
+
if (current === '.' || current === '' || current.startsWith('..'))
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Entry points defined in exactly `dir`, expanded up to 3 levels, with the checks each one runs. Memoized per scan. */
|
|
15
|
+
export class EntryPointChecks {
|
|
16
|
+
repoPath;
|
|
17
|
+
rec;
|
|
18
|
+
cache = new Map();
|
|
19
|
+
constructor(repoPath, rec) {
|
|
20
|
+
this.repoPath = repoPath;
|
|
21
|
+
this.rec = rec;
|
|
22
|
+
}
|
|
23
|
+
inDir(dir) {
|
|
24
|
+
const cached = this.cache.get(dir);
|
|
25
|
+
if (cached)
|
|
26
|
+
return cached;
|
|
27
|
+
const results = [];
|
|
28
|
+
for (const kind of entryPointKinds) {
|
|
29
|
+
for (const entry of kind.list(this.repoPath, dir)) {
|
|
30
|
+
const commands = expandCommand(this.repoPath, entry.invoke, { dir, file: dir }, { maxDepth: 3 });
|
|
31
|
+
const kinds = new Set();
|
|
32
|
+
const unrecognised = new Set();
|
|
33
|
+
for (const command of commands) {
|
|
34
|
+
for (const found of checkKinds(command, this.rec))
|
|
35
|
+
kinds.add(found);
|
|
36
|
+
if (command.tool !== null && UNRECOGNISED_RUNNERS.has(command.tool))
|
|
37
|
+
unrecognised.add(command.tool);
|
|
38
|
+
}
|
|
39
|
+
results.push({ name: `${path.posix.join(dir, kind.id)}:${entry.name}`, kinds, unrecognised: [...unrecognised] });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
this.cache.set(dir, results);
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
/** Candidate entry points for a unit: those of its package directory and of every ancestor (a root `verify` covers every package). */
|
|
46
|
+
forUnit(dir) {
|
|
47
|
+
return dirsUpward(dir).flatMap((d) => this.inDir(d));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** `KEY=value`, `export KEY=value`, blank lines, `#` comment lines and quoted values. A commented-out `# KEY=` line does not count. */
|
|
2
|
+
export function parseEnvExample(text) {
|
|
3
|
+
const out = {};
|
|
4
|
+
for (const raw of text.replace(/\r\n?/g, '\n').split('\n')) {
|
|
5
|
+
const line = raw.trim();
|
|
6
|
+
if (line === '' || line.startsWith('#'))
|
|
7
|
+
continue;
|
|
8
|
+
const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_.]*)\s*=\s*(.*)$/.exec(line);
|
|
9
|
+
if (!match?.[1])
|
|
10
|
+
continue;
|
|
11
|
+
let value = (match[2] ?? '').trim();
|
|
12
|
+
const quote = value[0];
|
|
13
|
+
if ((quote === '"' || quote === "'") && value.lastIndexOf(quote) > 0)
|
|
14
|
+
value = value.slice(1, value.lastIndexOf(quote));
|
|
15
|
+
else
|
|
16
|
+
value = value.replace(/\s+#.*$/, '');
|
|
17
|
+
out[match[1]] = value;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
const isName = (node, text) => ts.isIdentifier(node) && node.text === text;
|
|
3
|
+
/** `process.env` */
|
|
4
|
+
function isProcessEnv(node) {
|
|
5
|
+
return ts.isPropertyAccessExpression(node) && isName(node.expression, 'process') && node.name.text === 'env';
|
|
6
|
+
}
|
|
7
|
+
/** `import.meta.env` */
|
|
8
|
+
function isImportMetaEnv(node) {
|
|
9
|
+
return (ts.isPropertyAccessExpression(node) &&
|
|
10
|
+
node.name.text === 'env' &&
|
|
11
|
+
ts.isMetaProperty(node.expression) &&
|
|
12
|
+
node.expression.keywordToken === ts.SyntaxKind.ImportKeyword &&
|
|
13
|
+
node.expression.name.text === 'meta');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Names read through `process.env.X`, `process.env["X"]`, destructuring from `process.env` (a rest element or computed key is skipped),
|
|
17
|
+
* `import.meta.env.X` and `import.meta.env["X"]`. An alias (`const env = process.env`) and a non-literal key are not counted.
|
|
18
|
+
*/
|
|
19
|
+
export function tsEnvReads(sf) {
|
|
20
|
+
const names = new Set();
|
|
21
|
+
const visit = (node) => {
|
|
22
|
+
if (ts.isPropertyAccessExpression(node) && (isProcessEnv(node.expression) || isImportMetaEnv(node.expression)))
|
|
23
|
+
names.add(node.name.text);
|
|
24
|
+
else if (ts.isElementAccessExpression(node) && (isProcessEnv(node.expression) || isImportMetaEnv(node.expression)) && ts.isStringLiteralLike(node.argumentExpression)) {
|
|
25
|
+
names.add(node.argumentExpression.text);
|
|
26
|
+
}
|
|
27
|
+
else if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && node.initializer && isProcessEnv(node.initializer)) {
|
|
28
|
+
for (const element of node.name.elements) {
|
|
29
|
+
if (element.dotDotDotToken)
|
|
30
|
+
continue;
|
|
31
|
+
const key = element.propertyName ?? element.name;
|
|
32
|
+
if (ts.isIdentifier(key))
|
|
33
|
+
names.add(key.text);
|
|
34
|
+
else if (ts.isStringLiteralLike(key))
|
|
35
|
+
names.add(key.text);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
ts.forEachChild(node, visit);
|
|
39
|
+
};
|
|
40
|
+
visit(sf);
|
|
41
|
+
return names;
|
|
42
|
+
}
|
|
43
|
+
/** True when a name is excluded from 6.5: one of the YAML names, or starting with one of the YAML prefixes. */
|
|
44
|
+
export function isExcludedEnvName(name, lists) {
|
|
45
|
+
return lists.envExcludedNames.includes(name) || lists.envExcludedPrefixes.some((prefix) => name.startsWith(prefix));
|
|
46
|
+
}
|