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,318 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { readTracked, trackedPaths } from '../../../scope/tracked.js';
|
|
4
|
+
const CONFIG_NAMES = ['.dependency-cruiser.js', '.dependency-cruiser.cjs', '.dependency-cruiser.mjs', '.dependency-cruiser.json'];
|
|
5
|
+
class NonLiteral extends Error {
|
|
6
|
+
}
|
|
7
|
+
function evalLiteral(node) {
|
|
8
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node)) {
|
|
9
|
+
return evalLiteral(node.expression);
|
|
10
|
+
}
|
|
11
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
12
|
+
return node.text;
|
|
13
|
+
if (ts.isNumericLiteral(node))
|
|
14
|
+
return Number(node.text);
|
|
15
|
+
if (ts.isRegularExpressionLiteral(node))
|
|
16
|
+
return node.text.replace(/^\/(.*)\/[a-z]*$/s, '$1');
|
|
17
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword)
|
|
18
|
+
return true;
|
|
19
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword)
|
|
20
|
+
return false;
|
|
21
|
+
if (node.kind === ts.SyntaxKind.NullKeyword)
|
|
22
|
+
return null;
|
|
23
|
+
if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand)) {
|
|
24
|
+
return -Number(node.operand.text);
|
|
25
|
+
}
|
|
26
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
27
|
+
return node.elements.map((element) => {
|
|
28
|
+
if (ts.isSpreadElement(element))
|
|
29
|
+
throw new NonLiteral('spread element');
|
|
30
|
+
return evalLiteral(element);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const property of node.properties) {
|
|
36
|
+
if (!ts.isPropertyAssignment(property))
|
|
37
|
+
throw new NonLiteral('non-literal property');
|
|
38
|
+
const name = property.name;
|
|
39
|
+
const key = ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name) ? name.text : undefined;
|
|
40
|
+
if (key === undefined)
|
|
41
|
+
throw new NonLiteral('computed property name');
|
|
42
|
+
out[key] = evalLiteral(property.initializer);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
throw new NonLiteral(`${ts.SyntaxKind[node.kind]} is not a literal`);
|
|
47
|
+
}
|
|
48
|
+
/** The object literal a JS config exports: `module.exports = {...}`, `export default {...}` or a top-level const they name. */
|
|
49
|
+
function exportedObject(sourceFile) {
|
|
50
|
+
const consts = new Map();
|
|
51
|
+
for (const statement of sourceFile.statements) {
|
|
52
|
+
if (!ts.isVariableStatement(statement))
|
|
53
|
+
continue;
|
|
54
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
55
|
+
if (ts.isIdentifier(declaration.name) && declaration.initializer)
|
|
56
|
+
consts.set(declaration.name.text, declaration.initializer);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const resolve = (expression) => {
|
|
60
|
+
let current = expression;
|
|
61
|
+
while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current))
|
|
62
|
+
current = current.expression;
|
|
63
|
+
if (ts.isObjectLiteralExpression(current))
|
|
64
|
+
return current;
|
|
65
|
+
if (ts.isIdentifier(current)) {
|
|
66
|
+
const target = consts.get(current.text);
|
|
67
|
+
return target ? resolve(target) : undefined;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
};
|
|
71
|
+
for (const statement of sourceFile.statements) {
|
|
72
|
+
if (ts.isExportAssignment(statement))
|
|
73
|
+
return resolve(statement.expression);
|
|
74
|
+
if (ts.isExpressionStatement(statement) &&
|
|
75
|
+
ts.isBinaryExpression(statement.expression) &&
|
|
76
|
+
statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
77
|
+
ts.isPropertyAccessExpression(statement.expression.left) &&
|
|
78
|
+
ts.isIdentifier(statement.expression.left.expression) &&
|
|
79
|
+
statement.expression.left.expression.text === 'module' &&
|
|
80
|
+
statement.expression.left.name.text === 'exports') {
|
|
81
|
+
return resolve(statement.expression.right);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/** Elements of an array-literal property, one literal evaluation per element. */
|
|
87
|
+
function elementsOf(object, key) {
|
|
88
|
+
for (const property of object.properties) {
|
|
89
|
+
if (!ts.isPropertyAssignment(property))
|
|
90
|
+
continue;
|
|
91
|
+
const name = property.name;
|
|
92
|
+
if (!(ts.isIdentifier(name) || ts.isStringLiteral(name)) || name.text !== key)
|
|
93
|
+
continue;
|
|
94
|
+
let init = property.initializer;
|
|
95
|
+
while (ts.isParenthesizedExpression(init) || ts.isAsExpression(init))
|
|
96
|
+
init = init.expression;
|
|
97
|
+
if (!ts.isArrayLiteralExpression(init))
|
|
98
|
+
return [{ node: init, error: `${key} is not an array literal` }];
|
|
99
|
+
return init.elements.map((node) => {
|
|
100
|
+
try {
|
|
101
|
+
if (ts.isSpreadElement(node))
|
|
102
|
+
throw new NonLiteral('spread element');
|
|
103
|
+
return { node, value: evalLiteral(node) };
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const name = ts.isObjectLiteralExpression(node) ? scalar(node, 'name') : undefined;
|
|
107
|
+
return { node, error: err instanceof NonLiteral ? err.message : String(err), ...(typeof name === 'string' ? { name } : {}) };
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
function scalar(object, key) {
|
|
114
|
+
for (const property of object.properties) {
|
|
115
|
+
if (ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && property.name.text === key) {
|
|
116
|
+
try {
|
|
117
|
+
return evalLiteral(property.initializer);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const isObj = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
127
|
+
function patternList(value) {
|
|
128
|
+
if (typeof value === 'string')
|
|
129
|
+
return [value];
|
|
130
|
+
if (Array.isArray(value) && value.every((v) => typeof v === 'string'))
|
|
131
|
+
return value;
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
function pathSpec(value, label) {
|
|
135
|
+
if (value === undefined)
|
|
136
|
+
return {};
|
|
137
|
+
if (!isObj(value))
|
|
138
|
+
throw new NonLiteral(`${label} is not an object`);
|
|
139
|
+
const spec = {};
|
|
140
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
141
|
+
if (key !== 'path' && key !== 'pathNot')
|
|
142
|
+
throw new NonLiteral(`${label}.${key} needs an installed project`);
|
|
143
|
+
const list = patternList(entry);
|
|
144
|
+
if (!list)
|
|
145
|
+
throw new NonLiteral(`${label}.${key} is not a string or list of strings`);
|
|
146
|
+
for (const pattern of list)
|
|
147
|
+
new RegExp(pattern);
|
|
148
|
+
spec[key] = entry;
|
|
149
|
+
}
|
|
150
|
+
return spec;
|
|
151
|
+
}
|
|
152
|
+
const LITERAL = '(?:[\\w@-]|\\\\\\.)+';
|
|
153
|
+
const unescapeLiteral = (text) => text.replace(/\\\./g, '.').replace(/\\\//g, '/');
|
|
154
|
+
/** The directories a regex pattern names, or undefined when it does not name any. */
|
|
155
|
+
export function directoriesOf(pattern) {
|
|
156
|
+
const anywhere = new RegExp(`\\(\\^\\|\\\\?/\\)(${LITERAL})(?:\\\\?/)`).exec(pattern);
|
|
157
|
+
if (anywhere?.[1])
|
|
158
|
+
return [unescapeLiteral(anywhere[1])];
|
|
159
|
+
if (!pattern.startsWith('^'))
|
|
160
|
+
return undefined;
|
|
161
|
+
const group = new RegExp(`^\\^\\(((?:${LITERAL}(?:\\/${LITERAL})*)(?:\\|(?:${LITERAL}(?:\\/${LITERAL})*))*)\\)((?:\\/${LITERAL})*)(?:\\/|$)`).exec(pattern);
|
|
162
|
+
if (group?.[1])
|
|
163
|
+
return group[1].split('|').map((alt) => unescapeLiteral(`${alt}${group[2] ?? ''}`));
|
|
164
|
+
const literal = new RegExp(`^\\^(${LITERAL}(?:\\/${LITERAL})*)(?:\\/|$)`).exec(pattern);
|
|
165
|
+
if (literal?.[1])
|
|
166
|
+
return [unescapeLiteral(literal[1])];
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
function directorySet(value) {
|
|
170
|
+
const set = new Set();
|
|
171
|
+
for (const pattern of value === undefined ? [] : Array.isArray(value) ? value : [value]) {
|
|
172
|
+
for (const dir of directoriesOf(pattern) ?? [])
|
|
173
|
+
set.add(dir);
|
|
174
|
+
}
|
|
175
|
+
return set;
|
|
176
|
+
}
|
|
177
|
+
/** A direction rule: `from.path` and `to.path` both name directories and the two directory sets differ. */
|
|
178
|
+
export function isDirectionRule(rule) {
|
|
179
|
+
const from = directorySet(rule.from.path);
|
|
180
|
+
const to = directorySet(rule.to.path);
|
|
181
|
+
if (from.size === 0 || to.size === 0)
|
|
182
|
+
return false;
|
|
183
|
+
return from.size !== to.size || [...from].some((dir) => !to.has(dir));
|
|
184
|
+
}
|
|
185
|
+
function rawPathSpec(value) {
|
|
186
|
+
if (!isObj(value))
|
|
187
|
+
return {};
|
|
188
|
+
const spec = {};
|
|
189
|
+
for (const key of ['path', 'pathNot']) {
|
|
190
|
+
const list = patternList(value[key]);
|
|
191
|
+
if (list)
|
|
192
|
+
spec[key] = value[key];
|
|
193
|
+
}
|
|
194
|
+
return spec;
|
|
195
|
+
}
|
|
196
|
+
const RULE_KEYS = new Set(['name', 'severity', 'comment', 'from', 'to']);
|
|
197
|
+
/** Reads one rules file without executing it. Undefined when the file is absent or cannot be read. */
|
|
198
|
+
export function readDeclaredConfig(repoPath, file, warnings = []) {
|
|
199
|
+
const text = readTracked(repoPath, file);
|
|
200
|
+
if (text === null)
|
|
201
|
+
return undefined;
|
|
202
|
+
const config = { file, forbidden: [], allowed: undefined, allowedSeverity: 'warn', declared: false };
|
|
203
|
+
let forbidden;
|
|
204
|
+
let allowed;
|
|
205
|
+
let allowedSeverity;
|
|
206
|
+
try {
|
|
207
|
+
if (file.endsWith('.json')) {
|
|
208
|
+
const doc = JSON.parse(text);
|
|
209
|
+
if (!isObj(doc))
|
|
210
|
+
throw new Error('not an object');
|
|
211
|
+
const wrap = (value) => (Array.isArray(value) ? value.map((v) => ({ value: v })) : undefined);
|
|
212
|
+
forbidden = wrap(doc.forbidden);
|
|
213
|
+
allowed = wrap(doc.allowed);
|
|
214
|
+
allowedSeverity = doc.allowedSeverity;
|
|
215
|
+
if (doc.options !== undefined || doc.extends !== undefined)
|
|
216
|
+
warnings.push(`${file}: "options" and "extends" are ignored`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
|
220
|
+
const object = exportedObject(sourceFile);
|
|
221
|
+
if (!object)
|
|
222
|
+
throw new Error('no module.exports or export default object literal');
|
|
223
|
+
forbidden = elementsOf(object, 'forbidden');
|
|
224
|
+
allowed = elementsOf(object, 'allowed');
|
|
225
|
+
allowedSeverity = scalar(object, 'allowedSeverity');
|
|
226
|
+
if (object.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && (p.name.text === 'options' || p.name.text === 'extends'))) {
|
|
227
|
+
warnings.push(`${file}: "options" and "extends" are ignored`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
warnings.push(`${file}: could not be read, treated as absent (${err instanceof Error ? err.message : String(err)})`);
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
const ruleName = (value, index, known) => known ?? (isObj(value) && typeof value.name === 'string' ? value.name : `#${index + 1}`);
|
|
236
|
+
(forbidden ?? []).forEach((entry, index) => {
|
|
237
|
+
if (entry.error !== undefined || !isObj(entry.value)) {
|
|
238
|
+
warnings.push(`${file}: forbidden rule ${ruleName(entry.value, index, entry.name)} skipped (${entry.error ?? 'not an object'})`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const rule = entry.value;
|
|
242
|
+
const raw = { from: rawPathSpec(rule.from), to: rawPathSpec(rule.to) };
|
|
243
|
+
if (isDirectionRule(raw))
|
|
244
|
+
config.declared = true;
|
|
245
|
+
try {
|
|
246
|
+
const extra = Object.keys(rule).find((key) => !RULE_KEYS.has(key));
|
|
247
|
+
if (extra !== undefined)
|
|
248
|
+
throw new NonLiteral(`rule key ${extra} needs an installed project`);
|
|
249
|
+
if (typeof rule.name !== 'string')
|
|
250
|
+
throw new NonLiteral('rule has no name');
|
|
251
|
+
const severity = rule.severity;
|
|
252
|
+
if (severity !== 'error' && severity !== 'warn' && severity !== 'info' && severity !== 'ignore')
|
|
253
|
+
throw new NonLiteral('unknown severity');
|
|
254
|
+
const from = pathSpec(rule.from, 'from');
|
|
255
|
+
const to = pathSpec(rule.to, 'to');
|
|
256
|
+
if (severity === 'error' || severity === 'warn')
|
|
257
|
+
config.forbidden.push({ name: rule.name, severity, from, to });
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
warnings.push(`${file}: forbidden rule ${ruleName(rule, index)} skipped (${err instanceof Error ? err.message : String(err)})`);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
if (allowed) {
|
|
264
|
+
const rules = [];
|
|
265
|
+
let complete = true;
|
|
266
|
+
allowed.forEach((entry, index) => {
|
|
267
|
+
if (entry.error !== undefined || !isObj(entry.value)) {
|
|
268
|
+
warnings.push(`${file}: allowed rule ${ruleName(entry.value, index, entry.name)} skipped (${entry.error ?? 'not an object'})`);
|
|
269
|
+
complete = false;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const rule = entry.value;
|
|
273
|
+
const raw = { from: rawPathSpec(rule.from), to: rawPathSpec(rule.to) };
|
|
274
|
+
if (isDirectionRule(raw))
|
|
275
|
+
config.declared = true;
|
|
276
|
+
try {
|
|
277
|
+
const extra = Object.keys(rule).find((key) => !RULE_KEYS.has(key));
|
|
278
|
+
if (extra !== undefined)
|
|
279
|
+
throw new NonLiteral(`rule key ${extra} needs an installed project`);
|
|
280
|
+
rules.push({ from: pathSpec(rule.from, 'from'), to: pathSpec(rule.to, 'to') });
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
warnings.push(`${file}: allowed rule ${ruleName(rule, index)} skipped (${err instanceof Error ? err.message : String(err)})`);
|
|
284
|
+
complete = false;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
if (!complete)
|
|
288
|
+
warnings.push(`${file}: the whole "allowed" list is dropped because one of its rules was skipped`);
|
|
289
|
+
const severity = allowedSeverity === undefined ? 'warn' : allowedSeverity;
|
|
290
|
+
if (severity === 'error' || severity === 'warn')
|
|
291
|
+
config.allowedSeverity = severity;
|
|
292
|
+
if (complete && rules.length > 0 && (severity === 'error' || severity === 'warn'))
|
|
293
|
+
config.allowed = rules;
|
|
294
|
+
}
|
|
295
|
+
return config;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The nearest rules file from `packageDir` upwards that declares at least one direction rule.
|
|
299
|
+
* Files that cannot be read, or that declare nothing, are passed over.
|
|
300
|
+
*/
|
|
301
|
+
export function findDeclaredConfig(repoPath, packageDir, warnings, cache) {
|
|
302
|
+
const tracked = new Set(trackedPaths(repoPath));
|
|
303
|
+
for (let dir = path.posix.normalize(packageDir === '' ? '.' : packageDir);; dir = path.posix.dirname(dir)) {
|
|
304
|
+
for (const name of CONFIG_NAMES) {
|
|
305
|
+
const file = path.posix.join(dir, name);
|
|
306
|
+
if (!tracked.has(file))
|
|
307
|
+
continue;
|
|
308
|
+
if (!cache.has(file)) {
|
|
309
|
+
cache.set(file, readDeclaredConfig(repoPath, file, warnings));
|
|
310
|
+
}
|
|
311
|
+
const config = cache.get(file);
|
|
312
|
+
if (config?.declared)
|
|
313
|
+
return config;
|
|
314
|
+
}
|
|
315
|
+
if (dir === '.' || dir === '')
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { callsGlobalFetch, classifyImports, packageName } from './classify.js';
|
|
3
|
+
/** Logic files with a directory named exactly `domain` anywhere in their path. */
|
|
4
|
+
export function isDomainFile(file) {
|
|
5
|
+
return path.posix.dirname(file).split('/').includes('domain');
|
|
6
|
+
}
|
|
7
|
+
function inInfrastructure(file, directories) {
|
|
8
|
+
if (isDomainFile(file))
|
|
9
|
+
return false;
|
|
10
|
+
return path.posix.dirname(file).split('/').some((segment) => directories.includes(segment));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A domain file is impure when it imports a forbidden package (type-only included), imports an in-scope
|
|
14
|
+
* file under an infrastructure directory (unless that file is itself under `domain`), or calls the global `fetch`.
|
|
15
|
+
*/
|
|
16
|
+
export function isImpure(sourceFile, importedFiles, lists) {
|
|
17
|
+
const forbidden = new Set(lists.forbiddenImports.typescript_javascript);
|
|
18
|
+
if (classifyImports(sourceFile).some((occurrence) => forbidden.has(packageName(occurrence.specifier))))
|
|
19
|
+
return true;
|
|
20
|
+
if (importedFiles.some((file) => inInfrastructure(file, lists.infrastructureDirectories)))
|
|
21
|
+
return true;
|
|
22
|
+
return callsGlobalFetch(sourceFile);
|
|
23
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2
|
+
function emit(node) {
|
|
3
|
+
const alternatives = [];
|
|
4
|
+
for (const [segment, child] of node.children) {
|
|
5
|
+
const rest = child.children.size > 0 ? `(?:/${emit(child)})${child.end ? '?' : ''}` : '';
|
|
6
|
+
alternatives.push(`${escape(segment)}${rest}`);
|
|
7
|
+
}
|
|
8
|
+
return alternatives.length === 1 ? alternatives[0] : `(?:${alternatives.join('|')})`;
|
|
9
|
+
}
|
|
10
|
+
/** An anchored regex that matches exactly the given repo-relative paths, built from a segment trie. */
|
|
11
|
+
export function pathsToRegex(paths) {
|
|
12
|
+
const root = { children: new Map(), end: false };
|
|
13
|
+
for (const file of paths) {
|
|
14
|
+
let node = root;
|
|
15
|
+
for (const segment of file.split('/')) {
|
|
16
|
+
let child = node.children.get(segment);
|
|
17
|
+
if (!child) {
|
|
18
|
+
child = { children: new Map(), end: false };
|
|
19
|
+
node.children.set(segment, child);
|
|
20
|
+
}
|
|
21
|
+
node = child;
|
|
22
|
+
}
|
|
23
|
+
node.end = true;
|
|
24
|
+
}
|
|
25
|
+
return `^${emit(root)}$`;
|
|
26
|
+
}
|
|
27
|
+
/** Package names as a regex that also matches an import below the package (`pkg/sub`). */
|
|
28
|
+
export function packagesToRegex(packages) {
|
|
29
|
+
return `^(?:${packages.map(escape).join('|')})(?:/|$)`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Preset rules as dependency-cruiser rules for one unit. `layerFiles` maps a layer name to the
|
|
33
|
+
* unit's files in it; `unitFiles` is every in-scope file of the unit. One rule per (preset rule,
|
|
34
|
+
* `to` alternative), named `<package>:<rule>` with a `#packages` suffix on the packages alternative.
|
|
35
|
+
*/
|
|
36
|
+
export function presetRules(packageDir, rules, layerFiles, unitFiles) {
|
|
37
|
+
const out = [];
|
|
38
|
+
const meta = new Map();
|
|
39
|
+
const union = (names) => [...new Set(names.flatMap((n) => [...(layerFiles.get(n) ?? [])]))];
|
|
40
|
+
for (const rule of rules) {
|
|
41
|
+
if (rule.severity === 'info')
|
|
42
|
+
continue;
|
|
43
|
+
let fromFiles;
|
|
44
|
+
if (rule.from.in)
|
|
45
|
+
fromFiles = union(rule.from.in);
|
|
46
|
+
else {
|
|
47
|
+
const excluded = new Set(union(rule.from.not_in ?? []));
|
|
48
|
+
fromFiles = unitFiles.filter((f) => !excluded.has(f));
|
|
49
|
+
}
|
|
50
|
+
if (fromFiles.length === 0)
|
|
51
|
+
continue;
|
|
52
|
+
const from = { path: pathsToRegex(fromFiles) };
|
|
53
|
+
const base = `${packageDir}:${rule.name}`;
|
|
54
|
+
const toFiles = rule.to.in ? union(rule.to.in) : [];
|
|
55
|
+
const add = (name, to) => {
|
|
56
|
+
out.push({ name, severity: rule.severity, from, to });
|
|
57
|
+
meta.set(name, { rule: rule.name, excludesTypeOnly: rule.type_only_imports === 'excluded' });
|
|
58
|
+
};
|
|
59
|
+
if (toFiles.length > 0)
|
|
60
|
+
add(base, { path: pathsToRegex(toFiles) });
|
|
61
|
+
if (rule.to.packages && rule.to.packages.length > 0)
|
|
62
|
+
add(`${base}#packages`, { path: packagesToRegex(rule.to.packages) });
|
|
63
|
+
}
|
|
64
|
+
return { rules: out, meta };
|
|
65
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** CVSS v3.0 and v3.1 base score from a vector string. Other versions give undefined. */
|
|
2
|
+
const WEIGHTS = {
|
|
3
|
+
AV: { N: 0.85, A: 0.62, L: 0.55, P: 0.2 },
|
|
4
|
+
AC: { L: 0.77, H: 0.44 },
|
|
5
|
+
UI: { N: 0.85, R: 0.62 },
|
|
6
|
+
CIA: { H: 0.56, L: 0.22, N: 0 },
|
|
7
|
+
};
|
|
8
|
+
function pr(value, changed) {
|
|
9
|
+
if (value === 'N')
|
|
10
|
+
return 0.85;
|
|
11
|
+
if (value === 'L')
|
|
12
|
+
return changed ? 0.68 : 0.62;
|
|
13
|
+
if (value === 'H')
|
|
14
|
+
return changed ? 0.5 : 0.27;
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Smallest number, to one decimal, that is >= the input (CVSS v3.1 Roundup). */
|
|
18
|
+
function roundUp(input) {
|
|
19
|
+
const int = Math.round(input * 100000);
|
|
20
|
+
if (int % 10000 === 0)
|
|
21
|
+
return int / 100000;
|
|
22
|
+
return (Math.floor(int / 10000) + 1) / 10;
|
|
23
|
+
}
|
|
24
|
+
export function cvss3BaseScore(vector) {
|
|
25
|
+
const parts = vector.trim().split('/');
|
|
26
|
+
if (parts[0] !== 'CVSS:3.0' && parts[0] !== 'CVSS:3.1')
|
|
27
|
+
return undefined;
|
|
28
|
+
const metrics = new Map();
|
|
29
|
+
for (const part of parts.slice(1)) {
|
|
30
|
+
const [key, value] = part.split(':');
|
|
31
|
+
if (key && value)
|
|
32
|
+
metrics.set(key, value);
|
|
33
|
+
}
|
|
34
|
+
const get = (key) => metrics.get(key) ?? '';
|
|
35
|
+
const scope = get('S');
|
|
36
|
+
if (scope !== 'U' && scope !== 'C')
|
|
37
|
+
return undefined;
|
|
38
|
+
const changed = scope === 'C';
|
|
39
|
+
const av = WEIGHTS.AV[get('AV')];
|
|
40
|
+
const ac = WEIGHTS.AC[get('AC')];
|
|
41
|
+
const ui = WEIGHTS.UI[get('UI')];
|
|
42
|
+
const c = WEIGHTS.CIA[get('C')];
|
|
43
|
+
const i = WEIGHTS.CIA[get('I')];
|
|
44
|
+
const a = WEIGHTS.CIA[get('A')];
|
|
45
|
+
const priv = pr(get('PR'), changed);
|
|
46
|
+
if ([av, ac, ui, c, i, a, priv].some((n) => n === undefined))
|
|
47
|
+
return undefined;
|
|
48
|
+
const iss = 1 - (1 - (c ?? 0)) * (1 - (i ?? 0)) * (1 - (a ?? 0));
|
|
49
|
+
const impact = changed ? 7.52 * (iss - 0.029) - 3.25 * (iss - 0.02) ** 15 : 6.42 * iss;
|
|
50
|
+
if (impact <= 0)
|
|
51
|
+
return 0;
|
|
52
|
+
const exploitability = 8.22 * (av ?? 0) * (ac ?? 0) * (priv ?? 0) * (ui ?? 0);
|
|
53
|
+
const raw = changed ? Math.min(1.08 * (impact + exploitability), 10) : Math.min(impact + exploitability, 10);
|
|
54
|
+
return roundUp(raw);
|
|
55
|
+
}
|
|
56
|
+
export function band(score) {
|
|
57
|
+
if (score >= 9)
|
|
58
|
+
return 'critical';
|
|
59
|
+
if (score >= 7)
|
|
60
|
+
return 'high';
|
|
61
|
+
if (score >= 4)
|
|
62
|
+
return 'moderate';
|
|
63
|
+
if (score > 0)
|
|
64
|
+
return 'low';
|
|
65
|
+
return 'none';
|
|
66
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
import { isValidSpecifier, normalizeName, poetryToPep440 } from '../../util/pep440.js';
|
|
4
|
+
import { readToml } from '../../util/toml.js';
|
|
5
|
+
import { joinRel, readTrackedText, trackedIn, trackedSet } from './files.js';
|
|
6
|
+
function record(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
// ---- npm --------------------------------------------------------------------------------
|
|
10
|
+
function npmManifest(repoPath, pkg) {
|
|
11
|
+
const file = joinRel(pkg, 'package.json');
|
|
12
|
+
const text = readTrackedText(repoPath, file);
|
|
13
|
+
if (text === null)
|
|
14
|
+
return undefined;
|
|
15
|
+
const warnings = [];
|
|
16
|
+
let json;
|
|
17
|
+
try {
|
|
18
|
+
json = record(JSON.parse(text));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
warnings.push(`${file}: invalid JSON, ignored`);
|
|
22
|
+
}
|
|
23
|
+
const deps = [];
|
|
24
|
+
const sections = [
|
|
25
|
+
['dependencies', false],
|
|
26
|
+
['devDependencies', true],
|
|
27
|
+
['optionalDependencies', false],
|
|
28
|
+
];
|
|
29
|
+
for (const [section, dev] of sections) {
|
|
30
|
+
const optional = section === 'optionalDependencies';
|
|
31
|
+
for (const [name, spec] of Object.entries(record(json?.[section]) ?? {})) {
|
|
32
|
+
if (typeof spec !== 'string')
|
|
33
|
+
continue;
|
|
34
|
+
deps.push({ name, spec, dev, registry: semver.validRange(spec) !== null, ...(optional ? { optional } : {}) });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { file, kind: 'package.json', deps, warnings };
|
|
38
|
+
}
|
|
39
|
+
// ---- Python -----------------------------------------------------------------------------
|
|
40
|
+
/** One PEP 508 requirement string, or undefined when it is not a requirement. */
|
|
41
|
+
function parseRequirement(line, dev) {
|
|
42
|
+
const withoutMarker = (line.split(';')[0] ?? '').trim();
|
|
43
|
+
const m = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[[^\]]*\])?\s*(.*)$/.exec(withoutMarker);
|
|
44
|
+
if (!m?.[1])
|
|
45
|
+
return undefined;
|
|
46
|
+
let rest = (m[2] ?? '').trim();
|
|
47
|
+
if (rest.startsWith('@'))
|
|
48
|
+
return { name: normalizeName(m[1]), spec: '', dev, registry: false };
|
|
49
|
+
if (rest.startsWith('(') && rest.endsWith(')'))
|
|
50
|
+
rest = rest.slice(1, -1).trim();
|
|
51
|
+
rest = rest.replace(/\s+/g, '');
|
|
52
|
+
if (!isValidSpecifier(rest))
|
|
53
|
+
return undefined;
|
|
54
|
+
return { name: normalizeName(m[1]), spec: rest, dev, registry: true };
|
|
55
|
+
}
|
|
56
|
+
function requirementList(value, dev) {
|
|
57
|
+
if (!Array.isArray(value))
|
|
58
|
+
return [];
|
|
59
|
+
const found = [];
|
|
60
|
+
for (const entry of value) {
|
|
61
|
+
if (typeof entry !== 'string')
|
|
62
|
+
continue; // include-group tables are skipped
|
|
63
|
+
const dep = parseRequirement(entry, dev);
|
|
64
|
+
if (dep)
|
|
65
|
+
found.push(dep);
|
|
66
|
+
}
|
|
67
|
+
return found;
|
|
68
|
+
}
|
|
69
|
+
function groupedLists(value, dev) {
|
|
70
|
+
return Object.values(record(value) ?? {}).flatMap((list) => requirementList(list, dev));
|
|
71
|
+
}
|
|
72
|
+
function poetryTable(value, dev) {
|
|
73
|
+
const found = [];
|
|
74
|
+
for (const [rawName, entry] of Object.entries(record(value) ?? {})) {
|
|
75
|
+
if (rawName.toLowerCase() === 'python')
|
|
76
|
+
continue;
|
|
77
|
+
const name = normalizeName(rawName);
|
|
78
|
+
const first = Array.isArray(entry) ? entry[0] : entry;
|
|
79
|
+
if (typeof first === 'string') {
|
|
80
|
+
found.push({ name, spec: poetryToPep440(first), dev, registry: true });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const table = record(first);
|
|
84
|
+
const version = table?.version;
|
|
85
|
+
const nonRegistry = table === undefined || table.git !== undefined || table.path !== undefined || table.url !== undefined;
|
|
86
|
+
if (nonRegistry || typeof version !== 'string')
|
|
87
|
+
found.push({ name, spec: '', dev, registry: false });
|
|
88
|
+
else
|
|
89
|
+
found.push({ name, spec: poetryToPep440(version), dev, registry: true });
|
|
90
|
+
}
|
|
91
|
+
return found;
|
|
92
|
+
}
|
|
93
|
+
function pyprojectDeps(doc) {
|
|
94
|
+
const project = record(doc.project);
|
|
95
|
+
const tool = record(doc.tool);
|
|
96
|
+
const poetry = record(tool?.poetry);
|
|
97
|
+
const uv = record(tool?.uv);
|
|
98
|
+
const pdm = record(tool?.pdm);
|
|
99
|
+
const sections = [];
|
|
100
|
+
let present = false;
|
|
101
|
+
const take = (value, make) => {
|
|
102
|
+
if (value !== undefined) {
|
|
103
|
+
present = true;
|
|
104
|
+
sections.push(make());
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
take(project?.dependencies, () => requirementList(project?.dependencies, false));
|
|
108
|
+
take(project?.['optional-dependencies'], () => groupedLists(project?.['optional-dependencies'], true));
|
|
109
|
+
take(doc['dependency-groups'], () => groupedLists(doc['dependency-groups'], true));
|
|
110
|
+
take(uv?.['dev-dependencies'], () => requirementList(uv?.['dev-dependencies'], true));
|
|
111
|
+
take(poetry?.dependencies, () => poetryTable(poetry?.dependencies, false));
|
|
112
|
+
take(poetry?.['dev-dependencies'], () => poetryTable(poetry?.['dev-dependencies'], true));
|
|
113
|
+
for (const group of Object.values(record(poetry?.group) ?? {})) {
|
|
114
|
+
take(record(group)?.dependencies, () => poetryTable(record(group)?.dependencies, true));
|
|
115
|
+
}
|
|
116
|
+
take(pdm?.['dev-dependencies'], () => groupedLists(pdm?.['dev-dependencies'], true));
|
|
117
|
+
return present ? sections.flat() : undefined;
|
|
118
|
+
}
|
|
119
|
+
function pipfileDeps(doc) {
|
|
120
|
+
const found = [];
|
|
121
|
+
for (const [section, dev] of [['packages', false], ['dev-packages', true]]) {
|
|
122
|
+
for (const [rawName, entry] of Object.entries(record(doc[section]) ?? {})) {
|
|
123
|
+
const name = normalizeName(rawName);
|
|
124
|
+
if (typeof entry === 'string') {
|
|
125
|
+
found.push({ name, spec: entry === '*' ? '' : entry.replace(/\s+/g, ''), dev, registry: isValidSpecifier(entry === '*' ? '' : entry.replace(/\s+/g, '')) });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const version = record(entry)?.version;
|
|
129
|
+
if (typeof version === 'string') {
|
|
130
|
+
const spec = version === '*' ? '' : version.replace(/\s+/g, '');
|
|
131
|
+
found.push({ name, spec, dev, registry: isValidSpecifier(spec) });
|
|
132
|
+
}
|
|
133
|
+
else
|
|
134
|
+
found.push({ name, spec: '', dev, registry: false });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return found;
|
|
138
|
+
}
|
|
139
|
+
/** Requirement lines of a requirements file: comments, options and hash continuations removed. */
|
|
140
|
+
export function requirementLines(text) {
|
|
141
|
+
const joined = text.replace(/\\\r?\n/g, ' ');
|
|
142
|
+
const lines = [];
|
|
143
|
+
for (const raw of joined.split(/\r?\n/)) {
|
|
144
|
+
const line = raw.replace(/(^|\s)#.*$/, '').trim();
|
|
145
|
+
if (line === '' || line.startsWith('-'))
|
|
146
|
+
continue;
|
|
147
|
+
const hashed = /--hash[=\s]/.test(line);
|
|
148
|
+
const clean = line.replace(/\s--\S+(?:[= ]\S+)?/g, '').trim();
|
|
149
|
+
lines.push({ line: clean, hashed });
|
|
150
|
+
}
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
export function requirementFiles(repoPath, pkg) {
|
|
154
|
+
return trackedIn(repoPath, pkg === '.' ? '' : pkg).filter((f) => /^requirements.*\.txt$/.test(path.posix.basename(f)));
|
|
155
|
+
}
|
|
156
|
+
function pythonManifest(repoPath, pkg) {
|
|
157
|
+
const warnings = [];
|
|
158
|
+
const dir = pkg === '.' ? '' : pkg;
|
|
159
|
+
const pyprojectFile = joinRel(dir, 'pyproject.toml');
|
|
160
|
+
const requirements = () => {
|
|
161
|
+
const found = new Map();
|
|
162
|
+
for (const file of requirementFiles(repoPath, pkg)) {
|
|
163
|
+
const text = readTrackedText(repoPath, file) ?? '';
|
|
164
|
+
for (const { line } of requirementLines(text)) {
|
|
165
|
+
const dep = parseRequirement(line, false);
|
|
166
|
+
if (dep && !found.has(dep.name))
|
|
167
|
+
found.set(dep.name, dep);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return [...found.values()];
|
|
171
|
+
};
|
|
172
|
+
if (trackedSet(repoPath).has(pyprojectFile)) {
|
|
173
|
+
const doc = readToml(path.join(path.resolve(repoPath), pyprojectFile));
|
|
174
|
+
if (doc === undefined)
|
|
175
|
+
warnings.push(`${pyprojectFile}: invalid TOML, ignored`);
|
|
176
|
+
const deps = doc ? pyprojectDeps(doc) : undefined;
|
|
177
|
+
return { file: pyprojectFile, kind: 'pyproject.toml', deps: deps ?? requirements(), warnings };
|
|
178
|
+
}
|
|
179
|
+
const pipfile = joinRel(dir, 'Pipfile');
|
|
180
|
+
if (trackedSet(repoPath).has(pipfile)) {
|
|
181
|
+
const doc = readToml(path.join(path.resolve(repoPath), pipfile));
|
|
182
|
+
if (doc === undefined)
|
|
183
|
+
warnings.push(`${pipfile}: invalid TOML, ignored`);
|
|
184
|
+
return { file: pipfile, kind: 'Pipfile', deps: pipfileDeps(doc ?? {}), warnings };
|
|
185
|
+
}
|
|
186
|
+
const files = requirementFiles(repoPath, pkg);
|
|
187
|
+
const first = files[0];
|
|
188
|
+
if (first === undefined)
|
|
189
|
+
return undefined;
|
|
190
|
+
return { file: first, kind: 'requirements', deps: requirements(), warnings };
|
|
191
|
+
}
|
|
192
|
+
/** The unit's declared direct dependencies, or undefined when its package directory has no manifest. */
|
|
193
|
+
export function readManifest(repoPath, pkg, group) {
|
|
194
|
+
return group === 'python' ? pythonManifest(repoPath, pkg) : npmManifest(repoPath, pkg);
|
|
195
|
+
}
|