pr-checkmate 2.1.2 → 3.0.1
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 -268
- package/cspell.json +470 -447
- package/dist/cli.js +1 -1
- package/dist/config/constants.d.ts +6 -1
- package/dist/config/constants.js +9 -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 +1 -1
- package/dist/core/checks/dependencies/grype-scan.js +2 -1
- 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/ruff-shared.d.ts +3 -3
- package/dist/core/checks/languages/ruff-shared.js +27 -4
- 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/coverage.js +6 -1
- package/dist/core/checks/quality/dead-code.js +12 -3
- package/dist/core/checks/quality/duplicate-check.js +54 -27
- 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/package-size.d.ts +15 -0
- package/dist/core/checks/quality/package-size.js +130 -0
- 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/commands.js +1 -0
- package/dist/core/context.js +9 -1
- package/dist/core/install/cache.d.ts +20 -1
- package/dist/core/install/cache.js +65 -12
- package/dist/core/install/doctor.js +49 -15
- package/dist/core/install/ensure.d.ts +31 -14
- package/dist/core/install/ensure.js +165 -61
- package/dist/core/install/lock.d.ts +2 -1
- package/dist/core/install/merge.d.ts +47 -0
- package/dist/core/install/merge.js +91 -0
- package/dist/core/install/prepare.d.ts +76 -0
- package/dist/core/install/prepare.js +147 -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 +12 -18
- 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 +8 -2
- package/dist/init.js +21 -1
- package/dist/interfaces/index.d.ts +6 -0
- package/dist/utils/resolve-bin.d.ts +56 -6
- package/dist/utils/resolve-bin.js +136 -26
- package/gitleaks.toml +14 -0
- package/install-manifests/commitlint/package-lock.json +128 -250
- package/install-manifests/commitlint/package.json +2 -2
- package/install-manifests/core/package-lock.json +76 -76
- package/install-manifests/core/package.json +1 -1
- package/install-manifests/duplicate/package-lock.json +45 -45
- package/install-manifests/duplicate/package.json +1 -1
- package/install-manifests/go/package-lock.json +24 -0
- package/install-manifests/go/package.json +8 -0
- package/install-manifests/js/package-lock.json +248 -283
- package/install-manifests/js/package.json +9 -9
- package/install-manifests/python/package-lock.json +4 -4
- package/install-manifests/python/package.json +1 -1
- package/install-manifests/pytypes/package-lock.json +45 -0
- package/install-manifests/pytypes/package.json +8 -0
- package/package.json +21 -19
- package/pr-checkmate.schema.json +41 -13
- package/src/config/cspell.json +470 -447
- package/src/config/pr-checkmate.schema.json +41 -13
- 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,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
|
};
|
|
@@ -2,9 +2,9 @@ type RuffModule = typeof import('@astral-sh/ruff-wasm-nodejs');
|
|
|
2
2
|
type RuffWorkspace = InstanceType<RuffModule['Workspace']>;
|
|
3
3
|
export declare const PY_GLOBS: string[];
|
|
4
4
|
/**
|
|
5
|
-
* Instantiate a Ruff WASM workspace
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Instantiate a Ruff WASM workspace. Shared by the Ruff lint and format checks so
|
|
6
|
+
* their setup stays identical, and kept as a function (not a module-level
|
|
7
|
+
* singleton) so the WASM module loads only when a check runs.
|
|
8
8
|
*/
|
|
9
9
|
export declare function createRuffWorkspace(): RuffWorkspace;
|
|
10
10
|
export {};
|
|
@@ -4,11 +4,34 @@ exports.PY_GLOBS = void 0;
|
|
|
4
4
|
exports.createRuffWorkspace = createRuffWorkspace;
|
|
5
5
|
exports.PY_GLOBS = ['*.py'];
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* (
|
|
7
|
+
* The rules we lint Python with — pinned here rather than left to Ruff's defaults.
|
|
8
|
+
*
|
|
9
|
+
* `E4`/`E7`/`E9` (import placement, statement style, syntax) plus `F` (pyflakes:
|
|
10
|
+
* undefined names, unused imports, unreachable code) — the set Ruff itself
|
|
11
|
+
* shipped as its default through 0.15.
|
|
12
|
+
*
|
|
13
|
+
* Written down because leaving it implicit made our verdict change under us.
|
|
14
|
+
* Ruff 0.16 added the isort rules (`I`) to its defaults, and a routine dependency
|
|
15
|
+
* bump turned that into a 5- to 48-fold increase in findings across seventeen
|
|
16
|
+
* Python repositories — every one of them `I001`, "import block is un-sorted".
|
|
17
|
+
* Two repositories went from a clean report to a warning on code nobody had
|
|
18
|
+
* touched. Import order is a matter of taste, and a check that fires on every
|
|
19
|
+
* file in a repository is how people learn to ignore the whole report.
|
|
20
|
+
*
|
|
21
|
+
* Adding a rule to this list is a decision with a measurement behind it, which
|
|
22
|
+
* is the point: it is ours to make, not our dependency's to make for us.
|
|
23
|
+
*/
|
|
24
|
+
const SELECTED_RULES = ['E4', 'E7', 'E9', 'F'];
|
|
25
|
+
/**
|
|
26
|
+
* Instantiate a Ruff WASM workspace. Shared by the Ruff lint and format checks so
|
|
27
|
+
* their setup stays identical, and kept as a function (not a module-level
|
|
28
|
+
* singleton) so the WASM module loads only when a check runs.
|
|
10
29
|
*/
|
|
11
30
|
function createRuffWorkspace() {
|
|
12
31
|
const { Workspace, PositionEncoding } = require('@astral-sh/ruff-wasm-nodejs');
|
|
13
|
-
|
|
32
|
+
const settings = {
|
|
33
|
+
...Workspace.defaultSettings(),
|
|
34
|
+
lint: { select: SELECTED_RULES },
|
|
35
|
+
};
|
|
36
|
+
return new Workspace(settings, PositionEncoding.Utf16);
|
|
14
37
|
}
|
|
@@ -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;
|
|
@@ -112,6 +112,35 @@ function validateFailOn(json) {
|
|
|
112
112
|
* checks what the user actually wrote, not the merged defaults. `//`-comment keys
|
|
113
113
|
* and `$schema` are ignored.
|
|
114
114
|
*/
|
|
115
|
+
/**
|
|
116
|
+
* Report a `sourcePath` that points nowhere.
|
|
117
|
+
*
|
|
118
|
+
* Every scoped check restricts itself to these directories, so a path that does
|
|
119
|
+
* not exist silences them — and silences them as a PASS. Measured on a fixture
|
|
120
|
+
* with `"sourcePath": "src"` and the code in `app/`: a real conflict marker in
|
|
121
|
+
* `app/conflict.py` went unreported while Merge Conflict, a blocking check,
|
|
122
|
+
* printed ✅. Nothing else in the run mentioned that most of the repository had
|
|
123
|
+
* not been looked at.
|
|
124
|
+
*
|
|
125
|
+
* `init` defaults to `.` and recommends it, so this is not the common case — it
|
|
126
|
+
* is what happens when someone picks `src` at setup, or moves the code later, and
|
|
127
|
+
* that is precisely when nobody is looking for it.
|
|
128
|
+
*/
|
|
129
|
+
function validateSourcePath(json, cwd) {
|
|
130
|
+
const raw = json.sourcePath;
|
|
131
|
+
if (raw === undefined)
|
|
132
|
+
return [];
|
|
133
|
+
const paths = (Array.isArray(raw) ? raw : [raw]).filter((p) => typeof p === 'string' && p.trim().length > 0);
|
|
134
|
+
return paths
|
|
135
|
+
.filter(p => {
|
|
136
|
+
const normalised = p.replace(/^\.\/?/, '').replace(/\/+$/, '');
|
|
137
|
+
// `.` and `./` mean the whole repository and always exist.
|
|
138
|
+
if (normalised === '')
|
|
139
|
+
return false;
|
|
140
|
+
return !node_fs_1.default.existsSync(node_path_1.default.join(cwd, normalised));
|
|
141
|
+
})
|
|
142
|
+
.map(p => `sourcePath "${p}" does not exist — every scoped check silently reviews nothing, and reports a pass`);
|
|
143
|
+
}
|
|
115
144
|
exports.configValidationCheck = {
|
|
116
145
|
name: 'Config Validation',
|
|
117
146
|
phase: 'informational',
|
|
@@ -153,6 +182,7 @@ exports.configValidationCheck = {
|
|
|
153
182
|
issues.push(...validateSeverity(json, checkNames));
|
|
154
183
|
}
|
|
155
184
|
issues.push(...validateFailOn(json));
|
|
185
|
+
issues.push(...validateSourcePath(json, ctx.cwd));
|
|
156
186
|
if (issues.length === 0)
|
|
157
187
|
return (0, types_1.pass)();
|
|
158
188
|
// Advisory by default: a config problem is worth knowing about, but a repo
|
|
@@ -48,7 +48,12 @@ exports.coverageCheck = {
|
|
|
48
48
|
: DEFAULT_REPORTS;
|
|
49
49
|
const found = candidates.find(rel => {
|
|
50
50
|
try {
|
|
51
|
-
|
|
51
|
+
// `isFile()` as well as the size: `statSync` reports a non-zero size for
|
|
52
|
+
// a directory too, so a path that happens to be one would read as a
|
|
53
|
+
// report. Non-empty matters because a zero-byte report is what a test
|
|
54
|
+
// run that died halfway leaves behind — the case this check exists for.
|
|
55
|
+
const stat = node_fs_1.default.statSync(node_path_1.default.join(ctx.cwd, rel));
|
|
56
|
+
return stat.isFile() && stat.size > 0;
|
|
52
57
|
}
|
|
53
58
|
catch {
|
|
54
59
|
return false;
|
|
@@ -44,7 +44,7 @@ exports.deadCodeCheck = {
|
|
|
44
44
|
const ignore = compileIgnorePatterns(ctx.config.deadCode?.ignoreFiles ?? constants_1.DEAD_CODE_IGNORE_DEFAULTS);
|
|
45
45
|
// ts-unused-exports prints one `path: sym1, sym2` line per file, preceded
|
|
46
46
|
// by a `N modules with unused exports` summary line we skip.
|
|
47
|
-
const
|
|
47
|
+
const parsed = output
|
|
48
48
|
.split('\n')
|
|
49
49
|
.map(line => line.trim().match(/^(.+\.[cm]?tsx?): (.+)$/))
|
|
50
50
|
.filter((match) => match !== null)
|
|
@@ -54,8 +54,17 @@ exports.deadCodeCheck = {
|
|
|
54
54
|
.split(',')
|
|
55
55
|
.map(sym => sym.trim())
|
|
56
56
|
.filter(Boolean),
|
|
57
|
-
}))
|
|
58
|
-
|
|
57
|
+
}));
|
|
58
|
+
// The tool said something and none of it was a report. That is not a clean
|
|
59
|
+
// repository — it is an answer this check cannot read, and calling it a
|
|
60
|
+
// pass is how Circular Deps stayed green on every repository for its whole
|
|
61
|
+
// existence while grepping for console wording that had changed. Same
|
|
62
|
+
// resolution as Grype Scan: say so rather than invent a verdict.
|
|
63
|
+
if (parsed.length === 0) {
|
|
64
|
+
utils_1.logger.warn(`[Dead Code]: ⚠️ ts-unused-exports produced no parseable output:\n${output}`);
|
|
65
|
+
return (0, types_1.warn)('ts-unused-exports produced no parseable output');
|
|
66
|
+
}
|
|
67
|
+
const entries = parsed.filter(entry => !ignore.some(re => re.test(entry.file)));
|
|
59
68
|
if (entries.length === 0) {
|
|
60
69
|
utils_1.logger.info('[Dead Code]: ✅ No unused exports found');
|
|
61
70
|
return (0, types_1.pass)();
|
|
@@ -1,12 +1,19 @@
|
|
|
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.duplicateCheck = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
4
10
|
const config_1 = require("../../../config");
|
|
5
11
|
const constants_1 = require("../../../config/constants");
|
|
6
12
|
const utils_1 = require("../../../utils");
|
|
7
13
|
const execa_1 = require("../../../utils/execa");
|
|
8
14
|
const targets_1 = require("../../targets");
|
|
9
15
|
const types_1 = require("../../types");
|
|
16
|
+
const findings_1 = require("../findings");
|
|
10
17
|
/**
|
|
11
18
|
* The files to hand jscpd, per language.
|
|
12
19
|
*
|
|
@@ -43,22 +50,16 @@ function globsFor(languages) {
|
|
|
43
50
|
// not logic, and are commonly shipped in two formats (e.g. .eslintrc + flat
|
|
44
51
|
// config), so their repetition is expected rather than a code smell.
|
|
45
52
|
const BASE_IGNORE = ['node_modules/**', 'dist/**', '.git/**', ...constants_1.CONFIG_FILE_PATTERNS];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
'
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'
|
|
55
|
-
|
|
56
|
-
function stripJscpdPromo(text) {
|
|
57
|
-
return text
|
|
58
|
-
.split('\n')
|
|
59
|
-
.filter(line => !JSCPD_PROMO_MARKERS.some(marker => line.includes(marker)))
|
|
60
|
-
.join('\n')
|
|
61
|
-
.trimEnd();
|
|
53
|
+
/** `<path>:<start>-<end>`, repo-relative, for one end of a clone. */
|
|
54
|
+
function locate(cwd, at) {
|
|
55
|
+
const name = at?.name ?? '';
|
|
56
|
+
const rel = name ? node_path_1.default.relative(cwd, name) || node_path_1.default.basename(name) : '<unknown file>';
|
|
57
|
+
return `${rel}:${at?.start ?? '?'}-${at?.end ?? '?'}`;
|
|
58
|
+
}
|
|
59
|
+
/** One reportable line per clone: both ends, and how big it is. */
|
|
60
|
+
function describe(cwd, clone) {
|
|
61
|
+
const size = clone.lines === undefined ? '' : ` (${clone.lines} lines)`;
|
|
62
|
+
return `${locate(cwd, clone.firstFile)} ↔ ${locate(cwd, clone.secondFile)}${size}`;
|
|
62
63
|
}
|
|
63
64
|
exports.duplicateCheck = {
|
|
64
65
|
name: 'Duplicate Code',
|
|
@@ -72,7 +73,7 @@ exports.duplicateCheck = {
|
|
|
72
73
|
async run(ctx) {
|
|
73
74
|
const minLines = ctx.config.duplicate?.minLines ?? 5;
|
|
74
75
|
const minTokens = ctx.config.duplicate?.minTokens ?? 50;
|
|
75
|
-
const targetPaths = await (0, targets_1.resolveScopedTargetFiles)(ctx, globsFor(ctx.languages), (0, config_1.getSourcePaths)());
|
|
76
|
+
const targetPaths = await (0, targets_1.resolveScopedTargetFiles)(ctx, globsFor(ctx.languages), (0, config_1.getSourcePaths)(ctx.cwd));
|
|
76
77
|
if (targetPaths === null)
|
|
77
78
|
return (0, types_1.skip)('git unavailable');
|
|
78
79
|
if (targetPaths.length === 0)
|
|
@@ -86,7 +87,16 @@ exports.duplicateCheck = {
|
|
|
86
87
|
// defaulting to source-code formats so data files (yaml/json/markdown) are
|
|
87
88
|
// not scanned. Clients can add them back explicitly.
|
|
88
89
|
const formats = (ctx.config.duplicate?.formats ?? constants_1.DUPLICATE_CODE_FORMATS).join(',');
|
|
90
|
+
// The JSON reporter writes a file, so it needs somewhere to write it. Outside
|
|
91
|
+
// the repository deliberately: with no `--output`, jscpd creates `report/`
|
|
92
|
+
// in the client's working tree, and a check that leaves a directory behind is
|
|
93
|
+
// a check that shows up in the next commit.
|
|
94
|
+
const reportDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'pr-checkmate-jscpd-'));
|
|
89
95
|
try {
|
|
96
|
+
// `--silent` keeps jscpd's own console output away from the client entirely.
|
|
97
|
+
// That is what retired `stripJscpdPromo` — six string markers matching the
|
|
98
|
+
// funding lines jscpd v5 prints after its report, each good until the next
|
|
99
|
+
// release of somebody else's package.
|
|
90
100
|
const result = await (0, execa_1.execa)(process.execPath, [
|
|
91
101
|
jscpdBin,
|
|
92
102
|
'--min-lines',
|
|
@@ -94,27 +104,44 @@ exports.duplicateCheck = {
|
|
|
94
104
|
'--min-tokens',
|
|
95
105
|
String(minTokens),
|
|
96
106
|
'--reporters',
|
|
97
|
-
'
|
|
107
|
+
'json',
|
|
108
|
+
'--output',
|
|
109
|
+
reportDir,
|
|
110
|
+
'--absolute',
|
|
111
|
+
'--silent',
|
|
98
112
|
'--format',
|
|
99
113
|
formats,
|
|
100
114
|
'--ignore',
|
|
101
115
|
ignore,
|
|
102
116
|
...targetPaths,
|
|
103
117
|
], { cwd: ctx.cwd, reject: false });
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return (0, types_1.
|
|
118
|
+
const reportPath = node_path_1.default.join(reportDir, 'jscpd-report.json');
|
|
119
|
+
if (!node_fs_1.default.existsSync(reportPath)) {
|
|
120
|
+
// No report and nothing to read it from: jscpd did not get as far as
|
|
121
|
+
// writing one. Its stderr is the only thing that can say why, and a skip
|
|
122
|
+
// is the honest outcome — an empty report would read as "no duplication".
|
|
123
|
+
utils_1.logger.warn(`[Duplicate Code]: ⚠️ jscpd wrote no report (exit ${result.exitCode ?? 'killed'})` +
|
|
124
|
+
`${result.stderr ? `\n${result.stderr}` : ''}`);
|
|
125
|
+
return (0, types_1.skip)('jscpd produced no report');
|
|
112
126
|
}
|
|
113
|
-
|
|
127
|
+
const report = JSON.parse(node_fs_1.default.readFileSync(reportPath, 'utf-8'));
|
|
128
|
+
const clones = report.duplicates ?? [];
|
|
129
|
+
// jscpd's own count, not a regex over its prose. `output.match(/(\d+)\s+clone/i)`
|
|
130
|
+
// read the number out of an English sentence, so a reworded summary upstream
|
|
131
|
+
// would have reported zero duplication on a repository full of it — and
|
|
132
|
+
// reported it as a pass.
|
|
133
|
+
const cloneCount = report.statistics?.total?.clones ?? clones.length;
|
|
134
|
+
if (cloneCount === 0)
|
|
135
|
+
return (0, types_1.pass)();
|
|
136
|
+
(0, findings_1.logSample)('Duplicate Code', `${cloneCount} duplicate block(s):`, clones.map(clone => describe(ctx.cwd, clone)));
|
|
137
|
+
return (0, types_1.warn)(`${cloneCount} duplicate block(s)`);
|
|
114
138
|
}
|
|
115
139
|
catch (err) {
|
|
116
140
|
utils_1.logger.warn(`[Duplicate Code]: ⚠️ Could not run duplicate check\n${err}`);
|
|
117
141
|
return (0, types_1.skip)('jscpd unavailable');
|
|
118
142
|
}
|
|
143
|
+
finally {
|
|
144
|
+
node_fs_1.default.rmSync(reportDir, { recursive: true, force: true });
|
|
145
|
+
}
|
|
119
146
|
},
|
|
120
147
|
};
|
|
@@ -8,6 +8,7 @@ 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 targets_1 = require("../../targets");
|
|
11
12
|
const types_1 = require("../../types");
|
|
12
13
|
/** Source extensions a license header is expected on. */
|
|
13
14
|
const SOURCE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|kt|java|go|rs|cs|rb|php|swift|c|cc|cpp|h|hpp)$/i;
|
|
@@ -49,10 +50,12 @@ exports.licenseHeaderCheck = {
|
|
|
49
50
|
? ['diff', '--name-only', '--diff-filter=A', ctx.baseSha, ctx.headSha ?? 'HEAD']
|
|
50
51
|
: ['ls-files'];
|
|
51
52
|
const { stdout } = await (0, execa_1.execa)('git', args, { cwd: ctx.cwd });
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// Its own git call rather than `resolveTargets`, and for one reason:
|
|
54
|
+
// `--diff-filter=A`. This check is about files a PR ADDS, and the shared
|
|
55
|
+
// resolver asks for ACMR — routing through it would start demanding a
|
|
56
|
+
// header on every modified file too, which is a different check. So the
|
|
57
|
+
// client's `excludePaths` is applied here explicitly instead (ticket 080).
|
|
58
|
+
added = (0, targets_1.excludeClientPaths)(stdout.split('\n').filter(Boolean), ctx.cwd).filter(f => SOURCE_EXT.test(f));
|
|
56
59
|
}
|
|
57
60
|
catch (err) {
|
|
58
61
|
utils_1.logger.warn(`[License Header]: ⚠️ Could not analyze diff\n${err}`);
|
|
@@ -47,12 +47,7 @@ exports.markdownLintCheck = {
|
|
|
47
47
|
const userRules = userConfig.rules ?? {};
|
|
48
48
|
let tempConfigFile = null;
|
|
49
49
|
let configArg;
|
|
50
|
-
if ((0, config_1.hasLocalConfig)([
|
|
51
|
-
'.markdownlint.json',
|
|
52
|
-
'.markdownlint.jsonc',
|
|
53
|
-
'.markdownlint.yaml',
|
|
54
|
-
'.markdownlint.yml',
|
|
55
|
-
])) {
|
|
50
|
+
if ((0, config_1.hasLocalConfig)(['.markdownlint.json', '.markdownlint.jsonc', '.markdownlint.yaml', '.markdownlint.yml'], ctx.cwd)) {
|
|
56
51
|
configArg = [];
|
|
57
52
|
}
|
|
58
53
|
else {
|