pr-checkmate 2.1.0 → 3.0.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/README.md +63 -267
- package/cspell.json +155 -133
- package/dist/config/constants.d.ts +13 -1
- package/dist/config/constants.js +20 -7
- package/dist/config/index.d.ts +23 -11
- package/dist/config/index.js +27 -33
- package/dist/core/checks/dependencies/banned-imports.js +4 -1
- package/dist/core/checks/dependencies/circular-deps.js +14 -5
- package/dist/core/checks/dependencies/dependency-check.js +2 -2
- package/dist/core/checks/dependencies/grype-scan.js +2 -1
- package/dist/core/checks/dependencies/license-check.js +1 -6
- package/dist/core/checks/dependencies/npm-audit.js +1 -1
- package/dist/core/checks/dependencies/outdated-deps.js +1 -6
- package/dist/core/checks/dependencies/vuln-scan.js +2 -1
- package/dist/core/checks/findings.d.ts +46 -2
- package/dist/core/checks/findings.js +140 -2
- package/dist/core/checks/git/custom-rules.js +2 -0
- package/dist/core/checks/git/diff-smell.js +6 -1
- package/dist/core/checks/git/leftover-debug.js +8 -2
- package/dist/core/checks/git/lockfile-drift.js +6 -1
- package/dist/core/checks/git/merge-conflict.js +1 -1
- package/dist/core/checks/git/missing-tests.js +1 -1
- package/dist/core/checks/git/todo-fixme.js +10 -2
- package/dist/core/checks/languages/cpp-format.js +19 -42
- package/dist/core/checks/languages/external-checks.d.ts +77 -0
- package/dist/core/checks/languages/external-checks.js +200 -0
- package/dist/core/checks/languages/external-runner.d.ts +119 -0
- package/dist/core/checks/languages/external-runner.js +143 -0
- package/dist/core/checks/languages/go-format.d.ts +4 -4
- package/dist/core/checks/languages/go-format.js +151 -30
- package/dist/core/checks/languages/lint.js +19 -19
- package/dist/core/checks/languages/prettier.js +2 -7
- package/dist/core/checks/languages/python-typecheck.d.ts +10 -5
- package/dist/core/checks/languages/python-typecheck.js +161 -37
- package/dist/core/checks/languages/typecheck.js +10 -8
- package/dist/core/checks/pr/commitlint.js +7 -1
- package/dist/core/checks/quality/case-collision.js +6 -4
- package/dist/core/checks/quality/config-validation.d.ts +0 -7
- package/dist/core/checks/quality/config-validation.js +30 -0
- package/dist/core/checks/quality/duplicate-check.js +86 -28
- package/dist/core/checks/quality/license-header.js +7 -4
- package/dist/core/checks/quality/markdown-lint.js +1 -6
- package/dist/core/checks/quality/spellcheck.js +34 -5
- package/dist/core/checks/quality/symlinks.js +6 -4
- package/dist/core/checks/security/diff-security.js +26 -18
- package/dist/core/checks/security/dockerfile-security.js +7 -1
- package/dist/core/checks/security/migration-safety.js +32 -4
- package/dist/core/checks/security/security.d.ts +56 -0
- package/dist/core/checks/security/security.js +135 -11
- package/dist/core/checks/security/sensitive-file-guard.js +40 -6
- package/dist/core/checks/security/workflow-security.js +7 -1
- package/dist/core/checks/tool-install.d.ts +102 -0
- package/dist/core/checks/tool-install.js +69 -0
- package/dist/core/context.js +9 -1
- package/dist/core/install/cache.js +42 -11
- package/dist/core/install/doctor.d.ts +2 -2
- package/dist/core/install/doctor.js +17 -16
- package/dist/core/install/ensure.js +22 -3
- package/dist/core/install/lock.d.ts +2 -1
- package/dist/core/install/prepare.d.ts +75 -0
- package/dist/core/install/prepare.js +151 -0
- package/dist/core/install/profiles.d.ts +17 -3
- package/dist/core/install/profiles.js +27 -0
- package/dist/core/install/reuse.d.ts +41 -0
- package/dist/core/install/reuse.js +107 -0
- package/dist/core/registry.js +10 -18
- package/dist/core/reporters.js +8 -2
- package/dist/core/run-checks.js +74 -3
- package/dist/core/targets.d.ts +116 -5
- package/dist/core/targets.js +101 -16
- package/dist/core/types.d.ts +26 -2
- package/dist/core/types.js +28 -0
- package/dist/init.js +21 -1
- package/dist/interfaces/index.d.ts +1 -0
- package/dist/utils/detect-language.js +11 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/package-version.d.ts +4 -0
- package/dist/utils/package-version.js +41 -0
- package/dist/utils/resolve-bin.d.ts +56 -6
- package/dist/utils/resolve-bin.js +136 -26
- package/dist/utils/update-notifier.js +5 -7
- package/gitleaks.toml +14 -0
- package/install-manifests/go/package-lock.json +24 -0
- package/install-manifests/go/package.json +8 -0
- package/install-manifests/pytypes/package-lock.json +45 -0
- package/install-manifests/pytypes/package.json +8 -0
- package/package.json +4 -2
- package/pr-checkmate.schema.json +8 -2
- package/src/config/cspell.json +155 -133
- package/src/config/pr-checkmate.schema.json +8 -2
- package/dist/core/checks/languages/csharp-format.d.ts +0 -13
- package/dist/core/checks/languages/csharp-format.js +0 -52
- package/dist/core/checks/languages/go-lint.d.ts +0 -8
- package/dist/core/checks/languages/go-lint.js +0 -48
- package/dist/core/checks/languages/kotlin-lint.d.ts +0 -2
- package/dist/core/checks/languages/kotlin-lint.js +0 -42
- package/dist/core/checks/languages/php-format.d.ts +0 -9
- package/dist/core/checks/languages/php-format.js +0 -49
- package/dist/core/checks/languages/ruby-lint.d.ts +0 -8
- package/dist/core/checks/languages/ruby-lint.js +0 -50
- package/dist/core/checks/languages/rust-format.d.ts +0 -9
- package/dist/core/checks/languages/rust-format.js +0 -45
- package/dist/core/checks/languages/rust-lint.d.ts +0 -8
- package/dist/core/checks/languages/rust-lint.js +0 -50
- package/dist/core/checks/languages/shellcheck.d.ts +0 -8
- package/dist/core/checks/languages/shellcheck.js +0 -55
- package/dist/core/checks/languages/swift-lint.d.ts +0 -2
- package/dist/core/checks/languages/swift-lint.js +0 -46
|
@@ -1,16 +1,146 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.goFormatCheck = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_url_1 = require("node:url");
|
|
4
10
|
const utils_1 = require("../../../utils");
|
|
5
|
-
const execa_1 = require("../../../utils/execa");
|
|
6
11
|
const targets_1 = require("../../targets");
|
|
7
12
|
const types_1 = require("../../types");
|
|
13
|
+
const tool_install_1 = require("../tool-install");
|
|
14
|
+
const external_runner_1 = require("./external-runner");
|
|
8
15
|
const GO_GLOBS = ['*.go'];
|
|
9
16
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
17
|
+
* Load the bundled WASM gofmt, or explain why it could not be loaded.
|
|
18
|
+
*
|
|
19
|
+
* This is what stopped `⏭️ Go Format: gofmt not installed` from being the answer
|
|
20
|
+
* on every runner without a Go toolchain — a ~285 KB WASM build of gofmt does
|
|
21
|
+
* the same job with nothing to install, the same way Ruff covers Python and
|
|
22
|
+
* clang-format covers C++.
|
|
23
|
+
*
|
|
24
|
+
* Three details, each of which was a way to get this wrong:
|
|
25
|
+
*
|
|
26
|
+
* - **Resolved through `resolvePackageBin`, not a bare specifier.** The package
|
|
27
|
+
* may live in a fetched profile directory rather than in our own tree, and only
|
|
28
|
+
* the resolver knows about those. A bare `import('@wasm-fmt/gofmt')` resolves
|
|
29
|
+
* relative to this file and would miss it.
|
|
30
|
+
* - **`gofmt_node.js` explicitly**, because the package's `exports` map routes
|
|
31
|
+
* `.` per runtime and the node entry is the one that reads the `.wasm` off disk.
|
|
32
|
+
* - **A dynamic `import()`.** The package is ESM and we emit CommonJS;
|
|
33
|
+
* `require` cannot load it, and TypeScript preserves dynamic `import()` in CJS
|
|
34
|
+
* output under `module: NodeNext` precisely so this works.
|
|
35
|
+
*/
|
|
36
|
+
async function loadBundledGofmt() {
|
|
37
|
+
try {
|
|
38
|
+
const entry = (0, utils_1.resolvePackageBin)('@wasm-fmt/gofmt', 'gofmt_node.js');
|
|
39
|
+
const mod = (await import((0, node_url_1.pathToFileURL)(entry).href));
|
|
40
|
+
return mod.format;
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
utils_1.logger.warn(`[Go Format]: ⚠️ bundled gofmt could not be loaded\n${err}`);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The verdict, given the files that turned out to need formatting.
|
|
49
|
+
*
|
|
50
|
+
* Shared by both routes on purpose. It was written out twice — once after
|
|
51
|
+
* `gofmt -l` and once after the in-memory comparison — and the two copies are how
|
|
52
|
+
* the wording of a verdict comes to depend on whether Go happens to be installed
|
|
53
|
+
* on the runner (ticket 079). What a client is told about their code must not be a
|
|
54
|
+
* function of our own resolution order.
|
|
55
|
+
*
|
|
56
|
+
* Called only once the rewriting, if any, is done — so reaching the last branch
|
|
57
|
+
* means the files really were rewritten and CI has something to commit. The
|
|
58
|
+
* routes differ in when that happens (the system route shells out to `gofmt -w`,
|
|
59
|
+
* the bundled one wrote each file as it went) and in nothing this function can
|
|
60
|
+
* see.
|
|
61
|
+
*/
|
|
62
|
+
function verdictFor(ctx, unformatted) {
|
|
63
|
+
if (unformatted.length === 0)
|
|
64
|
+
return (0, types_1.pass)();
|
|
65
|
+
if (!ctx.write) {
|
|
66
|
+
utils_1.logger.warn(`[Go Format]: ⚠️ ${unformatted.length} Go file(s) need formatting`);
|
|
67
|
+
unformatted.slice(0, 10).forEach(file => utils_1.logger.warn(` ${file}`));
|
|
68
|
+
return (0, types_1.warn)(`${unformatted.length} file(s) need formatting (run with write to fix)`);
|
|
69
|
+
}
|
|
70
|
+
utils_1.logger.info(`[Go Format]: ✅ Reformatted ${unformatted.length} Go file(s)`);
|
|
71
|
+
return (0, types_1.pass)(`reformatted ${unformatted.length} file(s)`);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Format with the runner's own `gofmt`, when it has one.
|
|
75
|
+
*
|
|
76
|
+
* Tried ahead of the bundled copy on purpose, and it is the same policy as
|
|
77
|
+
* preferring a repo's `.clang-format` or its own ESLint: gofmt's output is a
|
|
78
|
+
* function of the Go release, and the one release we know a repository agrees
|
|
79
|
+
* with is the one installed beside it. Reaching for ours first would mean
|
|
80
|
+
* rewriting a Go 1.18 tree by current rules in `write` mode — us and their CI
|
|
81
|
+
* reformatting the same files in opposite directions.
|
|
82
|
+
*
|
|
83
|
+
* @returns The outcome, or `null` when there is no system `gofmt` to ask.
|
|
84
|
+
*/
|
|
85
|
+
async function formatWithSystemGofmt(ctx, targets) {
|
|
86
|
+
const list = await (0, external_runner_1.runTool)(ctx, 'gofmt', ['-l', ...targets]);
|
|
87
|
+
if (list.missing)
|
|
88
|
+
return null;
|
|
89
|
+
const unformatted = list.stdout.split('\n').filter(Boolean);
|
|
90
|
+
// Decided before the rewrite, so a read-only run answers without `gofmt -w`
|
|
91
|
+
// having been considered at all.
|
|
92
|
+
if (unformatted.length === 0 || !ctx.write)
|
|
93
|
+
return verdictFor(ctx, unformatted);
|
|
94
|
+
const fix = await (0, external_runner_1.runTool)(ctx, 'gofmt', ['-w', ...unformatted]);
|
|
95
|
+
if (fix.missing || fix.exitCode !== 0) {
|
|
96
|
+
utils_1.logger.warn(`[Go Format]: ⚠️ gofmt -w failed\n${fix.missing ? '' : fix.stderr}`);
|
|
97
|
+
return (0, types_1.warn)('gofmt failed to rewrite files');
|
|
98
|
+
}
|
|
99
|
+
return verdictFor(ctx, unformatted);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Format in-process with the bundled WASM gofmt — the route taken on a runner
|
|
103
|
+
* with no Go toolchain, which is most of them.
|
|
104
|
+
*
|
|
105
|
+
* Compares rather than trusting an exit code, because the WASM build is a
|
|
106
|
+
* library and not a CLI: it formats a string and says nothing about whether it
|
|
107
|
+
* changed anything. Mirrors the Ruff format check for that reason.
|
|
108
|
+
*/
|
|
109
|
+
function formatWithBundledGofmt(ctx, targets, format) {
|
|
110
|
+
const unformatted = [];
|
|
111
|
+
for (const rel of targets) {
|
|
112
|
+
const abs = node_path_1.default.join(ctx.cwd, rel);
|
|
113
|
+
let content;
|
|
114
|
+
try {
|
|
115
|
+
content = node_fs_1.default.readFileSync(abs, 'utf-8');
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
continue; // renamed or removed between the diff and the read
|
|
119
|
+
}
|
|
120
|
+
let formatted;
|
|
121
|
+
try {
|
|
122
|
+
formatted = format(content);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
// gofmt parses before it formats, so a throw here is a syntax error. That
|
|
126
|
+
// is Go Vet's finding to report, not a formatting verdict — and calling it
|
|
127
|
+
// one sends people looking for a style problem that does not exist.
|
|
128
|
+
utils_1.logger.warn(`[Go Format]: ⚠️ could not parse ${rel} (syntax error?)\n${err}`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (formatted === content)
|
|
132
|
+
continue;
|
|
133
|
+
unformatted.push(rel);
|
|
134
|
+
if (ctx.write)
|
|
135
|
+
node_fs_1.default.writeFileSync(abs, formatted);
|
|
136
|
+
}
|
|
137
|
+
return verdictFor(ctx, unformatted);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Formats changed Go files with gofmt — the runner's own when it has one, and a
|
|
141
|
+
* bundled WASM build otherwise, so the check has no runner dependency at all.
|
|
142
|
+
* Read-only in check mode; rewrites in place when `write`, so CI can auto-commit
|
|
143
|
+
* — mirrors the Prettier/C++/Ruff format checks.
|
|
14
144
|
*/
|
|
15
145
|
exports.goFormatCheck = {
|
|
16
146
|
name: 'Go Format',
|
|
@@ -19,30 +149,21 @@ exports.goFormatCheck = {
|
|
|
19
149
|
return ctx.config.go?.enabled !== false && ctx.languages.includes('go');
|
|
20
150
|
},
|
|
21
151
|
async run(ctx) {
|
|
22
|
-
const targets = await (0, targets_1.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (targets
|
|
26
|
-
return
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return (0, types_1.warn)(`${unformatted.length} file(s) need formatting (run with write to fix)`);
|
|
39
|
-
}
|
|
40
|
-
const fix = await (0, execa_1.execa)('gofmt', ['-w', ...unformatted], { cwd: ctx.cwd, reject: false });
|
|
41
|
-
if (fix.exitCode !== 0) {
|
|
42
|
-
utils_1.logger.warn(`[Go Format]: ⚠️ gofmt -w failed\n${fix.stderr}`);
|
|
43
|
-
return (0, types_1.warn)('gofmt failed to rewrite files');
|
|
44
|
-
}
|
|
45
|
-
utils_1.logger.info(`[Go Format]: ✅ Reformatted ${unformatted.length} Go file(s)`);
|
|
46
|
-
return (0, types_1.pass)(`reformatted ${unformatted.length} file(s)`);
|
|
152
|
+
const targets = await (0, targets_1.resolveTargets)(ctx, GO_GLOBS, {
|
|
153
|
+
onEmpty: { pass: 'no Go files changed' },
|
|
154
|
+
});
|
|
155
|
+
if (!Array.isArray(targets))
|
|
156
|
+
return targets;
|
|
157
|
+
const viaSystem = await formatWithSystemGofmt(ctx, targets);
|
|
158
|
+
if (viaSystem)
|
|
159
|
+
return viaSystem;
|
|
160
|
+
const format = await loadBundledGofmt();
|
|
161
|
+
// Neither route: no Go toolchain on the runner AND our own copy could not be
|
|
162
|
+
// loaded, which means a partial install of this package. Installing Go is
|
|
163
|
+
// the fix a client can actually act on.
|
|
164
|
+
if (!format)
|
|
165
|
+
return (0, tool_install_1.notInstalled)('gofmt', 'gofmt');
|
|
166
|
+
utils_1.logger.debug('[Go Format]: no system gofmt — using the bundled WASM formatter');
|
|
167
|
+
return formatWithBundledGofmt(ctx, targets, format);
|
|
47
168
|
},
|
|
48
169
|
};
|
|
@@ -4,20 +4,23 @@ exports.lintCheck = void 0;
|
|
|
4
4
|
const config_1 = require("../../../config");
|
|
5
5
|
const utils_1 = require("../../../utils");
|
|
6
6
|
const execa_1 = require("../../../utils/execa");
|
|
7
|
+
const reuse_1 = require("../../install/reuse");
|
|
7
8
|
const targets_1 = require("../../targets");
|
|
8
9
|
const types_1 = require("../../types");
|
|
9
10
|
/**
|
|
10
|
-
* Resolve the ESLint binary
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Resolve the ESLint binary under the client's `install.reuseLocal` policy.
|
|
12
|
+
*
|
|
13
|
+
* A repo pinned to ESLint 8 with a legacy `.eslintrc.*` must run ITS ESLint —
|
|
14
|
+
* ours does not read eslintrc — so when the client owns the config the version is
|
|
15
|
+
* not asked about. When the config is the flat one WE generate, it is: ESLint 8
|
|
16
|
+
* cannot read it, and reusing their copy there produced a failure about our
|
|
17
|
+
* configuration rather than about their code. That asymmetry is the whole reason
|
|
18
|
+
* `clientOwnsConfig` exists (ticket 064, lever A).
|
|
13
19
|
*/
|
|
14
|
-
function resolveESLintBin(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return (0, utils_1.resolvePackageBin)('eslint', 'bin/eslint.js');
|
|
20
|
-
}
|
|
20
|
+
function resolveESLintBin(ctx, clientOwnsConfig) {
|
|
21
|
+
return (0, utils_1.resolvePackageBin)('eslint', 'bin/eslint.js', {
|
|
22
|
+
paths: (0, reuse_1.clientToolPaths)('eslint', ctx, { clientOwnsConfig }),
|
|
23
|
+
});
|
|
21
24
|
}
|
|
22
25
|
async function getESLintMajorVersion(eslintBin) {
|
|
23
26
|
try {
|
|
@@ -36,19 +39,16 @@ exports.lintCheck = {
|
|
|
36
39
|
return ctx.languages.includes('typescript') || ctx.languages.includes('javascript');
|
|
37
40
|
},
|
|
38
41
|
async run(ctx) {
|
|
39
|
-
const
|
|
40
|
-
const targets = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx'], (0, config_1.getSourcePaths)());
|
|
42
|
+
const targets = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx'], (0, config_1.getSourcePaths)(ctx.cwd));
|
|
41
43
|
if (targets === null)
|
|
42
44
|
return (0, types_1.skip)('git unavailable');
|
|
43
45
|
if (targets.length === 0)
|
|
44
46
|
return (0, types_1.pass)('no TypeScript/JavaScript files to lint');
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'.eslintrc',
|
|
51
|
-
]);
|
|
47
|
+
// Whose config decides whose binary, so this is read BEFORE resolving one.
|
|
48
|
+
// It used to be the other way round, which is how a client's ESLint 8 ended
|
|
49
|
+
// up pointed at the flat config we generate.
|
|
50
|
+
const hasLocal = (0, config_1.hasLocalConfig)(['eslint.config.mjs', 'eslint.config.js', '.eslintrc.js', '.eslintrc.json', '.eslintrc'], ctx.cwd);
|
|
51
|
+
const eslintBin = resolveESLintBin(ctx, hasLocal);
|
|
52
52
|
const version = await getESLintMajorVersion(eslintBin);
|
|
53
53
|
const args = [...targets];
|
|
54
54
|
// `--ext` is an eslintrc-only flag. Under flat config (ESLint 9+) it is
|
|
@@ -42,7 +42,7 @@ exports.prettierCheck = {
|
|
|
42
42
|
return ctx.languages.includes('typescript') || ctx.languages.includes('javascript');
|
|
43
43
|
},
|
|
44
44
|
async run(ctx) {
|
|
45
|
-
const targets = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx', '*.json', '*.md'], (0, config_1.getSourcePaths)());
|
|
45
|
+
const targets = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx', '*.json', '*.md'], (0, config_1.getSourcePaths)(ctx.cwd));
|
|
46
46
|
if (targets === null)
|
|
47
47
|
return (0, types_1.skip)('git unavailable');
|
|
48
48
|
if (targets.length === 0)
|
|
@@ -57,12 +57,7 @@ exports.prettierCheck = {
|
|
|
57
57
|
ignorePath = gitignore;
|
|
58
58
|
else
|
|
59
59
|
ignorePath = tempIgnore = createTempIgnoreFile(ctx.cwd);
|
|
60
|
-
const hasLocal = (0, config_1.hasLocalConfig)([
|
|
61
|
-
'.prettierrc',
|
|
62
|
-
'.prettierrc.json',
|
|
63
|
-
'.prettierrc.js',
|
|
64
|
-
'prettier.config.js',
|
|
65
|
-
]);
|
|
60
|
+
const hasLocal = (0, config_1.hasLocalConfig)(['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js'], ctx.cwd);
|
|
66
61
|
// write=true → format in place; write=false → report-only (`--check`, never mutates files).
|
|
67
62
|
const mode = ctx.write ? '--write' : '--check';
|
|
68
63
|
let tempConfig = null;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { Check } from '../../types';
|
|
2
2
|
/**
|
|
3
|
-
* Type-checks changed Python files — the Python analogue of
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* Type-checks changed Python files — the Python analogue of `tsc --noEmit`.
|
|
4
|
+
*
|
|
5
|
+
* Two checkers, and the order is the policy. **mypy first**, because a repository
|
|
6
|
+
* that installed mypy configured it too (`mypy.ini`, `[tool.mypy]`), and its
|
|
7
|
+
* settled opinion about its own code beats ours. **Our bundled pyright
|
|
8
|
+
* otherwise**, which is what stopped `⏭️ mypy/pyright not installed` from being
|
|
9
|
+
* the answer on every runner: pyright is a Node bundle carrying its own typeshed,
|
|
10
|
+
* so it needs neither Python nor pip. Force either with `python.typeChecker`.
|
|
11
|
+
*
|
|
12
|
+
* Informational by design, like Ruff lint, so teams can adopt gradually.
|
|
8
13
|
*/
|
|
9
14
|
export declare const pythonTypecheckCheck: Check;
|
|
@@ -1,27 +1,154 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.pythonTypecheckCheck = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
4
9
|
const utils_1 = require("../../../utils");
|
|
5
|
-
const execa_1 = require("../../../utils/execa");
|
|
6
10
|
const targets_1 = require("../../targets");
|
|
7
11
|
const types_1 = require("../../types");
|
|
12
|
+
const findings_1 = require("../findings");
|
|
13
|
+
const tool_install_1 = require("../tool-install");
|
|
14
|
+
const external_runner_1 = require("./external-runner");
|
|
8
15
|
const ruff_shared_1 = require("./ruff-shared");
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
/** How many diagnostics reach the log before the rest are counted instead. */
|
|
17
|
+
const MAX_LOGGED = 10;
|
|
18
|
+
/** The check's display name — the log prefix, and the row in the report. */
|
|
19
|
+
const NAME = 'Python Types';
|
|
20
|
+
/**
|
|
21
|
+
* Diagnostics that mean pyright could not see the repository's dependencies.
|
|
22
|
+
*
|
|
23
|
+
* Their presence is a verdict about the *setup*, not about the code, and the
|
|
24
|
+
* first attempt at this got it backwards: it dropped these two rules and
|
|
25
|
+
* reported everything else. That removes the cause and keeps the consequences,
|
|
26
|
+
* which is worse than either extreme — on a real client repository (ticket 076)
|
|
27
|
+
* it produced fifteen confident "type errors" of which nine were one cascade:
|
|
28
|
+
*
|
|
29
|
+
* ```python
|
|
30
|
+
* @click.group()
|
|
31
|
+
* def main() -> None: ...
|
|
32
|
+
*
|
|
33
|
+
* @main.command("list-jobs") # nine of these
|
|
34
|
+
* def list_jobs() -> None: ...
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* With `click` absent, pyright sees `main` as a plain `FunctionType` and every
|
|
38
|
+
* `@main.command(...)` becomes an error. Pyright is answering correctly; the
|
|
39
|
+
* question is wrong. The same shape follows from typer, flask, celery and pytest
|
|
40
|
+
* fixtures — most Python projects, in any CI job that does not install first.
|
|
41
|
+
*
|
|
42
|
+
* So an unresolved import means we cannot type-check at all, and the honest
|
|
43
|
+
* answer is a skip that says so. That is exactly what the TypeScript check does
|
|
44
|
+
* with a missing `node_modules`, in the same words.
|
|
45
|
+
*/
|
|
46
|
+
const UNRESOLVED_IMPORT_RULES = new Set(['reportMissingImports', 'reportMissingModuleSource']);
|
|
47
|
+
/** How many unresolved module names the skip message names before stopping. */
|
|
48
|
+
const NAMED_IMPORTS = 3;
|
|
49
|
+
/** The module a `reportMissingImports` diagnostic is about, for the skip message. */
|
|
50
|
+
function unresolvedModule(message) {
|
|
51
|
+
return /"([^"]+)"/.exec(message)?.[1] ?? null;
|
|
52
|
+
}
|
|
53
|
+
/** Whether the repository configures pyright itself. */
|
|
54
|
+
function hasOwnPyrightConfig(cwd) {
|
|
55
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, 'pyrightconfig.json')))
|
|
56
|
+
return true;
|
|
57
|
+
try {
|
|
58
|
+
return node_fs_1.default.readFileSync(node_path_1.default.join(cwd, 'pyproject.toml'), 'utf-8').includes('[tool.pyright]');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Where our copy of pyright lives — bundled, or in a fetched profile. */
|
|
65
|
+
function resolvePyright() {
|
|
66
|
+
try {
|
|
67
|
+
return (0, utils_1.resolvePackageBin)('pyright', 'index.js');
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
13
70
|
return null;
|
|
14
|
-
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Read mypy's text output: one diagnostic per line, with its own final count. */
|
|
74
|
+
function reportMypy(output) {
|
|
75
|
+
const lines = output.split('\n').filter(line => /\berror\b|\bwarning\b/i.test(line));
|
|
76
|
+
if (lines.length === 0)
|
|
77
|
+
return (0, types_1.pass)();
|
|
78
|
+
// "Found 3 errors in 1 file (checked 12 source files)" — mypy's own count says
|
|
79
|
+
// more than a tally of lines that merely contained the word.
|
|
80
|
+
const summary = lines.find(line => /found\s+\d+\s+error/i.test(line))?.trim();
|
|
81
|
+
(0, findings_1.logSample)(NAME, 'type issues (mypy):', lines);
|
|
82
|
+
return (0, types_1.warn)(summary ?? `${lines.length} type issue(s)`);
|
|
15
83
|
}
|
|
16
84
|
/**
|
|
17
|
-
* Type-
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
85
|
+
* Type-check with our own pyright, and report only what the client can act on.
|
|
86
|
+
*
|
|
87
|
+
* Pyright is a Node bundle, so it runs through `process.execPath` rather than as
|
|
88
|
+
* a binary on the path — the same reason clang-format does.
|
|
89
|
+
*/
|
|
90
|
+
function reportPyright(ctx, stdout) {
|
|
91
|
+
let report;
|
|
92
|
+
try {
|
|
93
|
+
report = JSON.parse(stdout);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Pyright exits non-zero for a bad invocation too, and calling that a type
|
|
97
|
+
// error would put a finding in the report that is really our own bug.
|
|
98
|
+
utils_1.logger.warn(`[${NAME}]: ⚠️ pyright produced no readable report`);
|
|
99
|
+
return (0, types_1.skip)('pyright produced no readable report');
|
|
100
|
+
}
|
|
101
|
+
const diagnostics = (report.generalDiagnostics ?? []).filter(diagnostic => diagnostic.severity !== 'information');
|
|
102
|
+
// Nothing downstream of an unresolved import can be trusted, so this is
|
|
103
|
+
// checked before anything is reported — and skipped rather than filtered.
|
|
104
|
+
//
|
|
105
|
+
// Not applied to a repository that configures pyright itself: having written
|
|
106
|
+
// `pyrightconfig.json` (or a `[tool.pyright]` section) is having an opinion
|
|
107
|
+
// about import resolution — a `venvPath`, or these rules downgraded on
|
|
108
|
+
// purpose — and overriding it with a skip would ignore the one client who
|
|
109
|
+
// said what they wanted.
|
|
110
|
+
const unresolved = diagnostics.filter(diagnostic => UNRESOLVED_IMPORT_RULES.has(diagnostic.rule ?? ''));
|
|
111
|
+
if (unresolved.length > 0 && !hasOwnPyrightConfig(ctx.cwd)) {
|
|
112
|
+
const modules = [
|
|
113
|
+
...new Set(unresolved
|
|
114
|
+
.map(diagnostic => unresolvedModule(diagnostic.message))
|
|
115
|
+
.filter((name) => Boolean(name))),
|
|
116
|
+
];
|
|
117
|
+
const named = modules.slice(0, NAMED_IMPORTS).join(', ');
|
|
118
|
+
const rest = modules.length > NAMED_IMPORTS ? ` and ${modules.length - NAMED_IMPORTS} more` : '';
|
|
119
|
+
utils_1.logger.warn(`[${NAME}]: ⚠️ ${unresolved.length} import(s) could not be resolved — not type-checking`);
|
|
120
|
+
return (0, types_1.skip)(`project dependencies not installed (${named || `${unresolved.length} import(s)`}${rest}) ` +
|
|
121
|
+
'— run `pip install` before typecheck');
|
|
122
|
+
}
|
|
123
|
+
if (diagnostics.length === 0) {
|
|
124
|
+
utils_1.logger.info(`[${NAME}]: ✅ No type errors (pyright)`);
|
|
125
|
+
return (0, types_1.pass)();
|
|
126
|
+
}
|
|
127
|
+
const errors = diagnostics.filter(diagnostic => diagnostic.severity === 'error').length;
|
|
128
|
+
const summary = `${errors} error(s), ${diagnostics.length - errors} warning(s)`;
|
|
129
|
+
(0, findings_1.logSample)(NAME, `type issues (pyright): ${summary}`, diagnostics.slice(0, MAX_LOGGED).map(diagnostic => {
|
|
130
|
+
const rel = node_path_1.default.relative(ctx.cwd, diagnostic.file) || diagnostic.file;
|
|
131
|
+
// Pyright counts lines from zero; every other tool in the report counts
|
|
132
|
+
// from one, and a line number that is off by one is worse than none.
|
|
133
|
+
const line = (diagnostic.range?.start?.line ?? 0) + 1;
|
|
134
|
+
return `${rel}:${line} ${diagnostic.severity}: ${diagnostic.message.split('\n')[0]}`;
|
|
135
|
+
}));
|
|
136
|
+
return (0, types_1.warn)(summary);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Type-checks changed Python files — the Python analogue of `tsc --noEmit`.
|
|
140
|
+
*
|
|
141
|
+
* Two checkers, and the order is the policy. **mypy first**, because a repository
|
|
142
|
+
* that installed mypy configured it too (`mypy.ini`, `[tool.mypy]`), and its
|
|
143
|
+
* settled opinion about its own code beats ours. **Our bundled pyright
|
|
144
|
+
* otherwise**, which is what stopped `⏭️ mypy/pyright not installed` from being
|
|
145
|
+
* the answer on every runner: pyright is a Node bundle carrying its own typeshed,
|
|
146
|
+
* so it needs neither Python nor pip. Force either with `python.typeChecker`.
|
|
147
|
+
*
|
|
148
|
+
* Informational by design, like Ruff lint, so teams can adopt gradually.
|
|
22
149
|
*/
|
|
23
150
|
exports.pythonTypecheckCheck = {
|
|
24
|
-
name:
|
|
151
|
+
name: NAME,
|
|
25
152
|
phase: 'informational',
|
|
26
153
|
applies(ctx) {
|
|
27
154
|
return (ctx.config.python?.enabled !== false &&
|
|
@@ -35,32 +162,29 @@ exports.pythonTypecheckCheck = {
|
|
|
35
162
|
if (targets.length === 0)
|
|
36
163
|
return (0, types_1.pass)('no Python files changed');
|
|
37
164
|
const forced = ctx.config.python?.typeChecker;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
165
|
+
if (forced !== 'pyright') {
|
|
166
|
+
const mypy = await (0, external_runner_1.runTool)(ctx, 'mypy', targets);
|
|
167
|
+
if (!mypy.missing) {
|
|
168
|
+
if (mypy.exitCode === 0) {
|
|
169
|
+
utils_1.logger.info(`[${NAME}]: ✅ No type errors (mypy)`);
|
|
170
|
+
return (0, types_1.pass)();
|
|
171
|
+
}
|
|
172
|
+
return reportMypy([mypy.stdout, mypy.stderr].filter(Boolean).join('\n'));
|
|
46
173
|
}
|
|
174
|
+
// Asking for mypy by name and not having it is a configuration error the
|
|
175
|
+
// client meant to make visible; falling back to pyright would hide it.
|
|
176
|
+
if (forced === 'mypy')
|
|
177
|
+
return (0, tool_install_1.notInstalled)('mypy', 'mypy');
|
|
47
178
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
.filter(l => /\berror\b|\bwarning\b/i.test(l));
|
|
59
|
-
utils_1.logger.warn(`[Python Types]: ⚠️ type issues (${used}):`);
|
|
60
|
-
lines.slice(0, 10).forEach(l => utils_1.logger.warn(` ${l}`));
|
|
61
|
-
if (lines.length > 10)
|
|
62
|
-
utils_1.logger.warn(` ... and ${lines.length - 10} more`);
|
|
63
|
-
const summary = lines.find(l => /found\s+\d+\s+error/i.test(l)) ?? `${lines.length} type issue(s)`;
|
|
64
|
-
return (0, types_1.warn)(summary.trim());
|
|
179
|
+
const entry = resolvePyright();
|
|
180
|
+
if (!entry)
|
|
181
|
+
return (0, tool_install_1.notInstalled)('pyright', 'pyright');
|
|
182
|
+
const result = await (0, external_runner_1.runTool)(ctx, process.execPath, [entry, '--outputjson', ...targets]);
|
|
183
|
+
// The binary here is the Node running this process, so `missing` is not a
|
|
184
|
+
// reachable state — it is a case of the type, folded in rather than asserted
|
|
185
|
+
// away.
|
|
186
|
+
if (result.missing)
|
|
187
|
+
return (0, tool_install_1.notInstalled)('pyright', 'pyright');
|
|
188
|
+
return reportPyright(ctx, result.stdout);
|
|
65
189
|
},
|
|
66
190
|
};
|
|
@@ -8,18 +8,20 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const utils_1 = require("../../../utils");
|
|
10
10
|
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const reuse_1 = require("../../install/reuse");
|
|
11
12
|
const types_1 = require("../../types");
|
|
12
13
|
/**
|
|
13
14
|
* Prefer the consumer project's own TypeScript so its tsconfig is compiled by the
|
|
14
15
|
* version it was written for; fall back to the compiler bundled with pr-checkmate.
|
|
15
16
|
*/
|
|
16
|
-
function resolveTsc(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
function resolveTsc(ctx) {
|
|
18
|
+
// `clientOwnsConfig` unconditionally: this check does not apply without a
|
|
19
|
+
// `tsconfig.json` in the repository, so the compiler their config was written
|
|
20
|
+
// for is always theirs. `install.reuseLocal: "off"` still overrides, for a
|
|
21
|
+
// client who wants our pinned compiler for reproducibility.
|
|
22
|
+
return (0, utils_1.resolvePackageBin)('typescript', 'bin/tsc', {
|
|
23
|
+
paths: (0, reuse_1.clientToolPaths)('typescript', ctx, { clientOwnsConfig: true }),
|
|
24
|
+
});
|
|
23
25
|
}
|
|
24
26
|
exports.typecheckCheck = {
|
|
25
27
|
name: 'TypeScript',
|
|
@@ -42,7 +44,7 @@ exports.typecheckCheck = {
|
|
|
42
44
|
if (!node_fs_1.default.existsSync(node_path_1.default.join(ctx.cwd, 'node_modules'))) {
|
|
43
45
|
return (0, types_1.skip)('project dependencies not installed (no node_modules) — run `npm ci` before typecheck');
|
|
44
46
|
}
|
|
45
|
-
const tscBin = resolveTsc(ctx
|
|
47
|
+
const tscBin = resolveTsc(ctx);
|
|
46
48
|
const result = await (0, execa_1.execa)(process.execPath, [tscBin, '--noEmit'], {
|
|
47
49
|
cwd: ctx.cwd,
|
|
48
50
|
reject: false,
|
|
@@ -60,8 +60,14 @@ exports.commitlintCheck = {
|
|
|
60
60
|
return ctx.config.commitlint?.enabled !== false;
|
|
61
61
|
},
|
|
62
62
|
async run(ctx) {
|
|
63
|
+
// This check needs a commit to lint. Without a repository `--last` finds
|
|
64
|
+
// none, commitlint prints nothing and exits 0, and we reported `pass` — a
|
|
65
|
+
// verdict on a commit message that does not exist. Ticket 072: a check that
|
|
66
|
+
// examined nothing must not read as a check that found nothing.
|
|
67
|
+
if (!(await (0, utils_1.resolveGitRef)(ctx.cwd, 'HEAD')))
|
|
68
|
+
return (0, types_1.skip)('git unavailable');
|
|
63
69
|
const commitlintBin = (0, utils_1.resolvePackageBin)('@commitlint/cli', 'cli.js');
|
|
64
|
-
const hasLocal = (0, config_1.hasLocalConfig)(LOCAL_CONFIG_FILES);
|
|
70
|
+
const hasLocal = (0, config_1.hasLocalConfig)(LOCAL_CONFIG_FILES, ctx.cwd);
|
|
65
71
|
let tempConfigFile = null;
|
|
66
72
|
// With no diff range (a full-repo review, or `--full`) the check still has a
|
|
67
73
|
// meaningful subject: the newest commit message. `HEAD~1` cannot be assumed to
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.caseCollisionCheck = void 0;
|
|
4
|
-
const config_1 = require("../../../config");
|
|
5
4
|
const utils_1 = require("../../../utils");
|
|
6
5
|
const targets_1 = require("../../targets");
|
|
7
6
|
const types_1 = require("../../types");
|
|
@@ -18,9 +17,12 @@ exports.caseCollisionCheck = {
|
|
|
18
17
|
return ctx.config.caseCollision?.enabled !== false;
|
|
19
18
|
},
|
|
20
19
|
async run(ctx) {
|
|
21
|
-
const files = await (0, targets_1.
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const files = await (0, targets_1.resolveTargets)(ctx, ['*'], {
|
|
21
|
+
wholeTree: true,
|
|
22
|
+
onEmpty: { skip: 'no tracked files' },
|
|
23
|
+
});
|
|
24
|
+
if (!Array.isArray(files))
|
|
25
|
+
return files;
|
|
24
26
|
const byLower = new Map();
|
|
25
27
|
for (const f of files) {
|
|
26
28
|
const key = f.toLowerCase();
|
|
@@ -8,11 +8,4 @@ import { Check } from '../../types';
|
|
|
8
8
|
* slip or two.
|
|
9
9
|
*/
|
|
10
10
|
export declare function suggestName(input: string, candidates: string[]): string | null;
|
|
11
|
-
/**
|
|
12
|
-
* Validates pr-checkmate.json itself: unknown top-level keys (typos like
|
|
13
|
-
* `mergeConfict` that silently do nothing), invalid values in `severity` and
|
|
14
|
-
* `failOn`, and names in `severity` that match no check. Reads the raw file so it
|
|
15
|
-
* checks what the user actually wrote, not the merged defaults. `//`-comment keys
|
|
16
|
-
* and `$schema` are ignored.
|
|
17
|
-
*/
|
|
18
11
|
export declare const configValidationCheck: Check;
|