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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.phpFormatCheck = exports.csharpFormatCheck = exports.rustFormatCheck = exports.rustLintCheck = exports.goLintCheck = exports.shellcheckCheck = exports.rubyLintCheck = exports.swiftLintCheck = exports.kotlinLintCheck = void 0;
|
|
4
|
+
const external_runner_1 = require("./external-runner");
|
|
5
|
+
/**
|
|
6
|
+
* Every check that runs a linter or formatter we do not ship, as a table.
|
|
7
|
+
*
|
|
8
|
+
* The engine is in `external-runner.ts`; this file is only the data. What is
|
|
9
|
+
* genuinely per-language turns out to be four things — the binary, its
|
|
10
|
+
* arguments, the files it looks at, and how to read its output — and everything
|
|
11
|
+
* else was the same in all eleven of the modules this replaces.
|
|
12
|
+
*
|
|
13
|
+
* Keeping the entries together rather than one per file is the point: it is now
|
|
14
|
+
* possible to read what nine checks do at once, and a divergence between two of
|
|
15
|
+
* them is visible instead of two directories apart. Adding a tenth is an entry,
|
|
16
|
+
* not a file.
|
|
17
|
+
*
|
|
18
|
+
* The user-facing documentation stays per check, in
|
|
19
|
+
* `documentation/checks/languages/`, because that is organised around the reader's
|
|
20
|
+
* question ("how do I configure ktlint") rather than around ours.
|
|
21
|
+
*/
|
|
22
|
+
// ── lint ────────────────────────────────────────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* Lints changed Kotlin files with ktlint (style and formatting both, so it
|
|
25
|
+
* doubles as the Kotlin format signal). Uses the repo's own `.editorconfig`
|
|
26
|
+
* when present.
|
|
27
|
+
*/
|
|
28
|
+
exports.kotlinLintCheck = (0, external_runner_1.defineExternalLint)({
|
|
29
|
+
name: 'ktlint',
|
|
30
|
+
bin: 'ktlint',
|
|
31
|
+
tool: 'ktlint',
|
|
32
|
+
globs: ['*.kt', '*.kts'],
|
|
33
|
+
language: 'kotlin',
|
|
34
|
+
configKey: 'kotlin',
|
|
35
|
+
subject: 'Kotlin files',
|
|
36
|
+
args: (_ctx, targets) => ['--reporter=plain', ...targets],
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* Lints Swift with SwiftLint, which needs SourceKit and therefore a macOS
|
|
40
|
+
* runner. Runs over the directory rather than a file list — SwiftLint resolves
|
|
41
|
+
* its own `.swiftlint.yml` scope, and a file list would bypass the excludes a
|
|
42
|
+
* repo wrote there — so the resolved targets only decide whether it runs at all.
|
|
43
|
+
*/
|
|
44
|
+
exports.swiftLintCheck = (0, external_runner_1.defineExternalLint)({
|
|
45
|
+
name: 'SwiftLint',
|
|
46
|
+
bin: 'swiftlint',
|
|
47
|
+
tool: 'swiftlint',
|
|
48
|
+
globs: ['*.swift'],
|
|
49
|
+
language: 'swift',
|
|
50
|
+
configKey: 'swift',
|
|
51
|
+
subject: 'Swift files',
|
|
52
|
+
args: ctx => ['lint', '--quiet', '--path', ctx.cwd],
|
|
53
|
+
// SwiftLint's plain output carries progress and summary lines too; only the
|
|
54
|
+
// diagnostics are findings, and the two levels are worth reporting apart.
|
|
55
|
+
keep: line => line.includes(': warning:') || line.includes(': error:'),
|
|
56
|
+
summary: lines => {
|
|
57
|
+
const errors = lines.filter(line => line.includes(': error:')).length;
|
|
58
|
+
return `${errors} error(s), ${lines.length - errors} warning(s)`;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* Lints changed Ruby files with RuboCop, which covers layout as well as style,
|
|
63
|
+
* so it doubles as the Ruby format signal. Uses the repo's own `.rubocop.yml`
|
|
64
|
+
* when present.
|
|
65
|
+
*/
|
|
66
|
+
exports.rubyLintCheck = (0, external_runner_1.defineExternalLint)({
|
|
67
|
+
name: 'RuboCop',
|
|
68
|
+
bin: 'rubocop',
|
|
69
|
+
tool: 'rubocop',
|
|
70
|
+
globs: ['*.rb', '*.rake'],
|
|
71
|
+
language: 'ruby',
|
|
72
|
+
configKey: 'ruby',
|
|
73
|
+
subject: 'Ruby files',
|
|
74
|
+
args: (_ctx, targets) => ['--format', 'simple', ...targets],
|
|
75
|
+
// RuboCop ends with its own count — "12 offenses detected" — which says more
|
|
76
|
+
// than a line tally of an output format that mixes offences and file headers.
|
|
77
|
+
summary: lines => lines.find(line => /offense/i.test(line))?.trim() ?? `${lines.length} line(s) of output`,
|
|
78
|
+
});
|
|
79
|
+
/**
|
|
80
|
+
* Lints changed shell scripts with ShellCheck. `-f gcc` gives one line per
|
|
81
|
+
* finding ending in an `[SC…]` code, which both a human and the count below can
|
|
82
|
+
* read directly.
|
|
83
|
+
*
|
|
84
|
+
* No language gate: shell is not something detection reports, and a `deploy.sh`
|
|
85
|
+
* in a Python repository is exactly the file this check exists for.
|
|
86
|
+
*/
|
|
87
|
+
exports.shellcheckCheck = (0, external_runner_1.defineExternalLint)({
|
|
88
|
+
name: 'ShellCheck',
|
|
89
|
+
bin: 'shellcheck',
|
|
90
|
+
tool: 'shellcheck',
|
|
91
|
+
globs: ['*.sh', '*.bash', '*.ksh'],
|
|
92
|
+
configKey: 'shellcheck',
|
|
93
|
+
subject: 'shell scripts',
|
|
94
|
+
args: (ctx, targets) => {
|
|
95
|
+
const args = ['-f', 'gcc'];
|
|
96
|
+
const { minSeverity, ignore = [] } = ctx.config.shellcheck ?? {};
|
|
97
|
+
if (minSeverity)
|
|
98
|
+
args.push('-S', minSeverity);
|
|
99
|
+
if (ignore.length > 0)
|
|
100
|
+
args.push('-e', ignore.join(','));
|
|
101
|
+
return [...args, ...targets];
|
|
102
|
+
},
|
|
103
|
+
summary: (lines, targets) => `${lines.length} issue(s) in ${targets.length} file(s)`,
|
|
104
|
+
});
|
|
105
|
+
/**
|
|
106
|
+
* Vets changed Go code with `go vet ./...`, which needs the Go toolchain (the
|
|
107
|
+
* formatter next door no longer does — see `go-format.ts`). `go vet` operates
|
|
108
|
+
* per package, so it runs over the module rather than a file list; the delta
|
|
109
|
+
* only decides whether it runs.
|
|
110
|
+
*/
|
|
111
|
+
exports.goLintCheck = (0, external_runner_1.defineExternalLint)({
|
|
112
|
+
name: 'Go Vet',
|
|
113
|
+
bin: 'go',
|
|
114
|
+
tool: 'go',
|
|
115
|
+
toolLabel: 'go',
|
|
116
|
+
globs: ['*.go'],
|
|
117
|
+
language: 'go',
|
|
118
|
+
configKey: 'go',
|
|
119
|
+
subject: 'Go files',
|
|
120
|
+
args: () => ['vet', './...'],
|
|
121
|
+
// Diagnostics and build errors both land on stderr.
|
|
122
|
+
stderrFirst: true,
|
|
123
|
+
summary: lines => `${lines.length} diagnostic line(s)`,
|
|
124
|
+
});
|
|
125
|
+
/**
|
|
126
|
+
* Lints changed Rust with Clippy. It compiles the crate, so it runs over the
|
|
127
|
+
* crate rather than a file list; the delta only decides whether it runs.
|
|
128
|
+
*/
|
|
129
|
+
exports.rustLintCheck = (0, external_runner_1.defineExternalLint)({
|
|
130
|
+
name: 'Clippy',
|
|
131
|
+
bin: 'cargo',
|
|
132
|
+
tool: 'clippy',
|
|
133
|
+
toolLabel: 'cargo/clippy',
|
|
134
|
+
globs: ['*.rs'],
|
|
135
|
+
language: 'rust',
|
|
136
|
+
configKey: 'rust',
|
|
137
|
+
subject: 'Rust files',
|
|
138
|
+
args: () => ['clippy', '--quiet', '--', '-D', 'warnings'],
|
|
139
|
+
stderrFirst: true,
|
|
140
|
+
// Clippy interleaves diagnostics with compilation progress; only the lines
|
|
141
|
+
// that carry a level are findings.
|
|
142
|
+
keep: line => /\b(warning|error)\b/.test(line),
|
|
143
|
+
summary: lines => `${lines.length} clippy diagnostic(s)`,
|
|
144
|
+
});
|
|
145
|
+
// ── format ──────────────────────────────────────────────────────────────────
|
|
146
|
+
/**
|
|
147
|
+
* Formats changed Rust with `cargo fmt`, not a bare `rustfmt`: cargo reads the
|
|
148
|
+
* crate's edition from `Cargo.toml`, and rustfmt invoked directly does not —
|
|
149
|
+
* which silently formats 2015-edition code by 2024 rules.
|
|
150
|
+
*/
|
|
151
|
+
exports.rustFormatCheck = (0, external_runner_1.defineExternalFormat)({
|
|
152
|
+
name: 'Rustfmt',
|
|
153
|
+
bin: 'cargo',
|
|
154
|
+
tool: 'rustfmt',
|
|
155
|
+
toolLabel: 'cargo',
|
|
156
|
+
globs: ['*.rs'],
|
|
157
|
+
language: 'rust',
|
|
158
|
+
configKey: 'rust',
|
|
159
|
+
subject: 'Rust files',
|
|
160
|
+
code: 'Rust code',
|
|
161
|
+
checkArgs: () => ['fmt', '--check'],
|
|
162
|
+
writeArgs: () => ['fmt'],
|
|
163
|
+
});
|
|
164
|
+
/**
|
|
165
|
+
* Verifies C# formatting with `dotnet format`, which also runs the project's
|
|
166
|
+
* configured analyzers and style rules. It operates on the project or solution
|
|
167
|
+
* (and expects a prior `dotnet restore`), so it runs over the whole project; the
|
|
168
|
+
* delta only decides whether it runs.
|
|
169
|
+
*/
|
|
170
|
+
exports.csharpFormatCheck = (0, external_runner_1.defineExternalFormat)({
|
|
171
|
+
name: 'C# Format',
|
|
172
|
+
bin: 'dotnet',
|
|
173
|
+
tool: 'dotnet',
|
|
174
|
+
toolLabel: 'dotnet SDK',
|
|
175
|
+
globs: ['*.cs'],
|
|
176
|
+
language: 'csharp',
|
|
177
|
+
configKey: 'csharp',
|
|
178
|
+
subject: 'C# files',
|
|
179
|
+
code: 'C# code',
|
|
180
|
+
checkArgs: () => ['format', '--verify-no-changes'],
|
|
181
|
+
writeArgs: () => ['format'],
|
|
182
|
+
});
|
|
183
|
+
/**
|
|
184
|
+
* Formats changed PHP with PHP-CS-Fixer, using the repo's own
|
|
185
|
+
* `.php-cs-fixer.php` when present. `--using-cache=no` because the cache keys on
|
|
186
|
+
* absolute paths, and a CI checkout directory that changes between runs makes a
|
|
187
|
+
* stale cache report files as clean that were never examined.
|
|
188
|
+
*/
|
|
189
|
+
exports.phpFormatCheck = (0, external_runner_1.defineExternalFormat)({
|
|
190
|
+
name: 'PHP CS Fixer',
|
|
191
|
+
bin: 'php-cs-fixer',
|
|
192
|
+
tool: 'php-cs-fixer',
|
|
193
|
+
globs: ['*.php'],
|
|
194
|
+
language: 'php',
|
|
195
|
+
configKey: 'php',
|
|
196
|
+
subject: 'PHP files',
|
|
197
|
+
code: 'PHP code',
|
|
198
|
+
checkArgs: targets => ['fix', '--using-cache=no', ...targets, '--dry-run'],
|
|
199
|
+
writeArgs: targets => ['fix', '--using-cache=no', ...targets],
|
|
200
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Check, CheckContext, type Language } from '../../types';
|
|
2
|
+
import { type ExternalTool } from '../tool-install';
|
|
3
|
+
/**
|
|
4
|
+
* The one shape every check that shells out to somebody else's linter has.
|
|
5
|
+
*
|
|
6
|
+
* Eleven of these were written by copying a neighbour, and copying is what they
|
|
7
|
+
* had in common: `resolveTargetFiles` → `execa` → the same seven-line comment
|
|
8
|
+
* about `ENOENT` → split the output → log ten lines and a "… and N more". Of the
|
|
9
|
+
* ~600 lines that produced, about sixty said anything specific to a language.
|
|
10
|
+
*
|
|
11
|
+
* The cost of that was not the duplication. It was that each copy diverged
|
|
12
|
+
* slightly, so a fix landed in one of eleven places: the `git unavailable` skip
|
|
13
|
+
* was missing from some, the ten-line truncation counted differently in others,
|
|
14
|
+
* and `⏭️ ktlint: ktlint not installed` outlived four rewrites of the message it
|
|
15
|
+
* was supposed to match. Behaviour that is meant to be identical everywhere
|
|
16
|
+
* belongs in one function; what is genuinely per-language belongs in a table.
|
|
17
|
+
*
|
|
18
|
+
* Two factories rather than one, because a linter and a formatter differ in the
|
|
19
|
+
* only part that matters — a linter reads output, a formatter runs twice and may
|
|
20
|
+
* write. Folding them together would mean a spec with half its fields unused.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Config blocks that gate a language check with `enabled: false`.
|
|
24
|
+
*
|
|
25
|
+
* Named as a union rather than typed as `keyof PRCheckMateConfig` so a spec
|
|
26
|
+
* cannot point at `reporter` or `install` and typecheck.
|
|
27
|
+
*/
|
|
28
|
+
type LanguageConfigKey = 'csharp' | 'go' | 'kotlin' | 'php' | 'ruby' | 'rust' | 'shellcheck' | 'swift';
|
|
29
|
+
/**
|
|
30
|
+
* A finished tool run, with "the tool is not here" as a case of its own rather
|
|
31
|
+
* than an exception.
|
|
32
|
+
*/
|
|
33
|
+
type ToolRun = {
|
|
34
|
+
missing: true;
|
|
35
|
+
} | {
|
|
36
|
+
missing: false;
|
|
37
|
+
exitCode: number | undefined;
|
|
38
|
+
stdout: string;
|
|
39
|
+
stderr: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Run an external tool, telling an absent binary apart from a failing one.
|
|
43
|
+
*
|
|
44
|
+
* execa v9 with `reject: false` does NOT throw on a missing binary — it resolves
|
|
45
|
+
* with `code: 'ENOENT'`. Detecting that on the result rather than in a
|
|
46
|
+
* `try`/`catch` (which never fires here) is the difference between an honest skip
|
|
47
|
+
* and a missing linter reporting a clean pass. This comment used to be repeated
|
|
48
|
+
* verbatim in eleven files, which is roughly how long it takes for one of the
|
|
49
|
+
* copies to lose it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function runTool(ctx: CheckContext, bin: string, args: string[]): Promise<ToolRun>;
|
|
52
|
+
/** The fields both factories read; each adds the part that differs. */
|
|
53
|
+
interface ExternalSpecBase {
|
|
54
|
+
/** Display name — the report row, and the log prefix. */
|
|
55
|
+
name: string;
|
|
56
|
+
/** The binary to run. */
|
|
57
|
+
bin: string;
|
|
58
|
+
/** Key into the install-hint table, so the skip says how to fix itself. */
|
|
59
|
+
tool: ExternalTool;
|
|
60
|
+
/**
|
|
61
|
+
* How to name the tool in the skip. Omit when the check's own name already is
|
|
62
|
+
* the binary's name, or the message stutters (`ktlint: ktlint not installed`).
|
|
63
|
+
*/
|
|
64
|
+
toolLabel?: string;
|
|
65
|
+
/** Pathspecs the check operates on, e.g. `['*.kt', '*.kts']`. */
|
|
66
|
+
globs: string[];
|
|
67
|
+
/**
|
|
68
|
+
* Language gate. Omit for a check that applies to any repository — ShellCheck
|
|
69
|
+
* runs wherever there is a `.sh`, and shell is not a detected language.
|
|
70
|
+
*/
|
|
71
|
+
language?: Language;
|
|
72
|
+
/** Config block whose `enabled: false` switches the check off. */
|
|
73
|
+
configKey: LanguageConfigKey;
|
|
74
|
+
/** Plural noun for the nothing-to-do pass: `no Kotlin files changed`. */
|
|
75
|
+
subject: string;
|
|
76
|
+
}
|
|
77
|
+
/** A check that runs somebody else's linter and reports what it said. */
|
|
78
|
+
export interface ExternalLintSpec extends ExternalSpecBase {
|
|
79
|
+
/**
|
|
80
|
+
* Arguments for the run. Given `ctx` as well as the files because not every
|
|
81
|
+
* linter takes a file list — `go vet` works per package and SwiftLint per
|
|
82
|
+
* directory, and for those the resolved targets only decide whether to run.
|
|
83
|
+
*/
|
|
84
|
+
args(ctx: CheckContext, targets: string[]): string[];
|
|
85
|
+
/** Which output lines are findings. Default: every non-blank line. */
|
|
86
|
+
keep?(line: string): boolean;
|
|
87
|
+
/** How to phrase the count. Default: `N issue(s)`. */
|
|
88
|
+
summary?(lines: string[], targets: string[]): string;
|
|
89
|
+
/** Read stderr ahead of stdout, for tools that diagnose there. */
|
|
90
|
+
stderrFirst?: boolean;
|
|
91
|
+
}
|
|
92
|
+
/** A check that runs somebody else's formatter, read-only unless `write`. */
|
|
93
|
+
export interface ExternalFormatSpec extends ExternalSpecBase {
|
|
94
|
+
/** Singular noun for the verdicts: `PHP code needs formatting`. */
|
|
95
|
+
code: string;
|
|
96
|
+
/** Arguments for the read-only pass. */
|
|
97
|
+
checkArgs(targets: string[]): string[];
|
|
98
|
+
/** Arguments for the pass that rewrites files. */
|
|
99
|
+
writeArgs(targets: string[]): string[];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Build an informational lint check from its spec.
|
|
103
|
+
*
|
|
104
|
+
* Informational for all of them, and not a field: these checks depend on a tool
|
|
105
|
+
* we do not ship, so on a runner without it they cannot distinguish clean code
|
|
106
|
+
* from unexamined code. A gate that silently stops gating is worse than no gate,
|
|
107
|
+
* and a client who wants one promotes the check with `severity`.
|
|
108
|
+
*/
|
|
109
|
+
export declare function defineExternalLint(spec: ExternalLintSpec): Check;
|
|
110
|
+
/**
|
|
111
|
+
* Build a format check from its spec: detect, then rewrite only when asked.
|
|
112
|
+
*
|
|
113
|
+
* The two passes are deliberate. A check run must never change the tree unless
|
|
114
|
+
* the caller said `write`, and in `write` mode CI needs the files actually
|
|
115
|
+
* rewritten so it can commit them — so the read-only pass is the one that decides
|
|
116
|
+
* the verdict, and the second only carries it out.
|
|
117
|
+
*/
|
|
118
|
+
export declare function defineExternalFormat(spec: ExternalFormatSpec): Check;
|
|
119
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runTool = runTool;
|
|
4
|
+
exports.defineExternalLint = defineExternalLint;
|
|
5
|
+
exports.defineExternalFormat = defineExternalFormat;
|
|
6
|
+
const utils_1 = require("../../../utils");
|
|
7
|
+
const execa_1 = require("../../../utils/execa");
|
|
8
|
+
const targets_1 = require("../../targets");
|
|
9
|
+
const types_1 = require("../../types");
|
|
10
|
+
const findings_1 = require("../findings");
|
|
11
|
+
const tool_install_1 = require("../tool-install");
|
|
12
|
+
/**
|
|
13
|
+
* Run an external tool, telling an absent binary apart from a failing one.
|
|
14
|
+
*
|
|
15
|
+
* execa v9 with `reject: false` does NOT throw on a missing binary — it resolves
|
|
16
|
+
* with `code: 'ENOENT'`. Detecting that on the result rather than in a
|
|
17
|
+
* `try`/`catch` (which never fires here) is the difference between an honest skip
|
|
18
|
+
* and a missing linter reporting a clean pass. This comment used to be repeated
|
|
19
|
+
* verbatim in eleven files, which is roughly how long it takes for one of the
|
|
20
|
+
* copies to lose it.
|
|
21
|
+
*/
|
|
22
|
+
async function runTool(ctx, bin, args) {
|
|
23
|
+
const result = await (0, execa_1.execa)(bin, args, { cwd: ctx.cwd, reject: false });
|
|
24
|
+
if (result.code === 'ENOENT')
|
|
25
|
+
return { missing: true };
|
|
26
|
+
return {
|
|
27
|
+
missing: false,
|
|
28
|
+
exitCode: result.exitCode,
|
|
29
|
+
stdout: result.stdout,
|
|
30
|
+
stderr: result.stderr,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Both streams as one blob — some tools diagnose on stdout, some on stderr. */
|
|
34
|
+
function combinedOutput(run, stderrFirst) {
|
|
35
|
+
const streams = stderrFirst ? [run.stderr, run.stdout] : [run.stdout, run.stderr];
|
|
36
|
+
return streams.filter(Boolean).join('\n').trim();
|
|
37
|
+
}
|
|
38
|
+
/** Every non-blank line is a finding, unless a spec says otherwise. */
|
|
39
|
+
function isNotBlank(line) {
|
|
40
|
+
return line.trim().length > 0;
|
|
41
|
+
}
|
|
42
|
+
/** `N issue(s)`, unless a spec phrases its count differently. */
|
|
43
|
+
function countIssues(lines) {
|
|
44
|
+
return `${lines.length} issue(s)`;
|
|
45
|
+
}
|
|
46
|
+
/** Whether a language check is switched on for this repository. */
|
|
47
|
+
function isEnabled(ctx, key, language) {
|
|
48
|
+
if (ctx.config[key]?.enabled === false)
|
|
49
|
+
return false;
|
|
50
|
+
return language === undefined || ctx.languages.includes(language);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Assemble the `Check` both factories return, given the part that differs.
|
|
54
|
+
*
|
|
55
|
+
* Everything here is identical between a linter and a formatter — the name, the
|
|
56
|
+
* `applies` gate, and resolving the files — and only `phase` and `work` are not.
|
|
57
|
+
* Kept as a third function rather than merged into one factory with a flag,
|
|
58
|
+
* because the two specs are genuinely different shapes: a linter reads output, a
|
|
59
|
+
* formatter runs twice and may write. One factory would mean a spec with half its
|
|
60
|
+
* fields unused, which is the trade this file's header argues against.
|
|
61
|
+
*/
|
|
62
|
+
function defineExternal(spec, phase, work) {
|
|
63
|
+
return {
|
|
64
|
+
name: spec.name,
|
|
65
|
+
phase,
|
|
66
|
+
applies(ctx) {
|
|
67
|
+
return isEnabled(ctx, spec.configKey, spec.language);
|
|
68
|
+
},
|
|
69
|
+
async run(ctx) {
|
|
70
|
+
const targets = await (0, targets_1.resolveTargets)(ctx, spec.globs, {
|
|
71
|
+
onEmpty: { pass: `no ${spec.subject} changed` },
|
|
72
|
+
});
|
|
73
|
+
if (!Array.isArray(targets))
|
|
74
|
+
return targets;
|
|
75
|
+
return work(ctx, targets);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build an informational lint check from its spec.
|
|
81
|
+
*
|
|
82
|
+
* Informational for all of them, and not a field: these checks depend on a tool
|
|
83
|
+
* we do not ship, so on a runner without it they cannot distinguish clean code
|
|
84
|
+
* from unexamined code. A gate that silently stops gating is worse than no gate,
|
|
85
|
+
* and a client who wants one promotes the check with `severity`.
|
|
86
|
+
*/
|
|
87
|
+
function defineExternalLint(spec) {
|
|
88
|
+
return defineExternal(spec, 'informational', async (ctx, targets) => {
|
|
89
|
+
const run = await runTool(ctx, spec.bin, spec.args(ctx, targets));
|
|
90
|
+
if (run.missing)
|
|
91
|
+
return (0, tool_install_1.notInstalled)(spec.tool, spec.toolLabel);
|
|
92
|
+
const output = combinedOutput(run, spec.stderrFirst);
|
|
93
|
+
// Exit 0 is the tool's verdict. Empty output on a non-zero exit is a tool
|
|
94
|
+
// that objected without saying to what, and there is nothing to put in a
|
|
95
|
+
// report either way — so both are a pass rather than a warn reading
|
|
96
|
+
// "0 issue(s)".
|
|
97
|
+
if (run.exitCode === 0 || !output) {
|
|
98
|
+
utils_1.logger.info(`[${spec.name}]: ✅ No issues found`);
|
|
99
|
+
return (0, types_1.pass)();
|
|
100
|
+
}
|
|
101
|
+
const lines = output.split('\n').filter(spec.keep ?? isNotBlank);
|
|
102
|
+
// The tool objected and none of its output matched what this check knows how
|
|
103
|
+
// to read — a build error ahead of the lint pass, usually. Naming the exit
|
|
104
|
+
// code is thin, but a clean pass over an objection is a lie.
|
|
105
|
+
const summary = lines.length > 0
|
|
106
|
+
? (spec.summary ?? countIssues)(lines, targets)
|
|
107
|
+
: `${spec.bin} exited ${run.exitCode ?? 'without a code'} with no readable diagnostics`;
|
|
108
|
+
(0, findings_1.logSample)(spec.name, `${summary}:`, lines);
|
|
109
|
+
return (0, types_1.warn)(summary);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build a format check from its spec: detect, then rewrite only when asked.
|
|
114
|
+
*
|
|
115
|
+
* The two passes are deliberate. A check run must never change the tree unless
|
|
116
|
+
* the caller said `write`, and in `write` mode CI needs the files actually
|
|
117
|
+
* rewritten so it can commit them — so the read-only pass is the one that decides
|
|
118
|
+
* the verdict, and the second only carries it out.
|
|
119
|
+
*/
|
|
120
|
+
function defineExternalFormat(spec) {
|
|
121
|
+
return defineExternal(spec, 'format', async (ctx, targets) => {
|
|
122
|
+
const dry = await runTool(ctx, spec.bin, spec.checkArgs(targets));
|
|
123
|
+
if (dry.missing)
|
|
124
|
+
return (0, tool_install_1.notInstalled)(spec.tool, spec.toolLabel);
|
|
125
|
+
if (dry.exitCode === 0)
|
|
126
|
+
return (0, types_1.pass)();
|
|
127
|
+
if (!ctx.write) {
|
|
128
|
+
utils_1.logger.warn(`[${spec.name}]: ⚠️ ${spec.code} needs formatting`);
|
|
129
|
+
return (0, types_1.warn)(`${spec.code} needs formatting (run with write to fix)`);
|
|
130
|
+
}
|
|
131
|
+
const fix = await runTool(ctx, spec.bin, spec.writeArgs(targets));
|
|
132
|
+
// `missing` cannot happen here — the same binary answered a moment ago — but
|
|
133
|
+
// it is a case of the type, and collapsing it into the failure branch is both
|
|
134
|
+
// correct and shorter than asserting it away.
|
|
135
|
+
if (fix.missing || fix.exitCode !== 0) {
|
|
136
|
+
const detail = fix.missing ? '' : `\n${fix.stderr}`;
|
|
137
|
+
utils_1.logger.warn(`[${spec.name}]: ⚠️ ${spec.bin} failed to rewrite files${detail}`);
|
|
138
|
+
return (0, types_1.warn)(`${spec.bin} failed to rewrite files`);
|
|
139
|
+
}
|
|
140
|
+
utils_1.logger.info(`[${spec.name}]: ✅ Reformatted ${spec.code}`);
|
|
141
|
+
return (0, types_1.pass)(`reformatted ${spec.code}`);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Check } from '../../types';
|
|
2
2
|
/**
|
|
3
|
-
* Formats changed Go files with
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Formats changed Go files with gofmt — the runner's own when it has one, and a
|
|
4
|
+
* bundled WASM build otherwise, so the check has no runner dependency at all.
|
|
5
|
+
* Read-only in check mode; rewrites in place when `write`, so CI can auto-commit
|
|
6
|
+
* — mirrors the Prettier/C++/Ruff format checks.
|
|
7
7
|
*/
|
|
8
8
|
export declare const goFormatCheck: Check;
|