pr-checkmate 1.22.8 → 1.23.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 +101 -333
- package/cspell.json +9 -1
- package/dist/cli.js +32 -2
- package/dist/config/constants.d.ts +9 -0
- package/dist/config/constants.js +68 -1
- package/dist/core/checks/dependencies/banned-imports.d.ts +2 -0
- package/dist/core/checks/dependencies/banned-imports.js +71 -0
- package/dist/core/checks/{circular-deps.d.ts → dependencies/circular-deps.d.ts} +1 -1
- package/dist/core/checks/{circular-deps.js → dependencies/circular-deps.js} +5 -5
- package/dist/core/checks/{dependency-check.d.ts → dependencies/dependency-check.d.ts} +1 -1
- package/dist/core/checks/{dependency-check.js → dependencies/dependency-check.js} +3 -3
- package/dist/core/checks/{license-check.d.ts → dependencies/license-check.d.ts} +1 -1
- package/dist/core/checks/{license-check.js → dependencies/license-check.js} +3 -3
- package/dist/core/checks/{npm-audit.d.ts → dependencies/npm-audit.d.ts} +1 -1
- package/dist/core/checks/{npm-audit.js → dependencies/npm-audit.js} +3 -3
- package/dist/core/checks/{outdated-deps.d.ts → dependencies/outdated-deps.d.ts} +1 -1
- package/dist/core/checks/{outdated-deps.js → dependencies/outdated-deps.js} +3 -3
- package/dist/core/checks/dependencies/vuln-scan.d.ts +9 -0
- package/dist/core/checks/dependencies/vuln-scan.js +41 -0
- package/dist/core/checks/findings.d.ts +37 -1
- package/dist/core/checks/findings.js +80 -3
- package/dist/core/checks/git/custom-rules.d.ts +2 -0
- package/dist/core/checks/git/custom-rules.js +102 -0
- package/dist/core/checks/{diff-smell.d.ts → git/diff-smell.d.ts} +1 -1
- package/dist/core/checks/{diff-smell.js → git/diff-smell.js} +3 -3
- package/dist/core/checks/git/large-files.d.ts +2 -0
- package/dist/core/checks/git/large-files.js +83 -0
- package/dist/core/checks/git/leftover-debug.d.ts +2 -0
- package/dist/core/checks/git/leftover-debug.js +77 -0
- package/dist/core/checks/git/lockfile-drift.d.ts +2 -0
- package/dist/core/checks/git/lockfile-drift.js +114 -0
- package/dist/core/checks/git/merge-conflict.d.ts +2 -0
- package/dist/core/checks/git/merge-conflict.js +109 -0
- package/dist/core/checks/git/missing-tests.d.ts +2 -0
- package/dist/core/checks/git/missing-tests.js +92 -0
- package/dist/core/checks/git/todo-fixme.d.ts +7 -0
- package/dist/core/checks/git/todo-fixme.js +74 -0
- package/dist/core/checks/{cpp-format.d.ts → languages/cpp-format.d.ts} +1 -1
- package/dist/core/checks/{cpp-format.js → languages/cpp-format.js} +3 -3
- package/dist/core/checks/languages/csharp-format.d.ts +13 -0
- package/dist/core/checks/languages/csharp-format.js +52 -0
- package/dist/core/checks/languages/go-format.d.ts +8 -0
- package/dist/core/checks/languages/go-format.js +48 -0
- package/dist/core/checks/languages/go-lint.d.ts +8 -0
- package/dist/core/checks/languages/go-lint.js +48 -0
- package/dist/core/checks/{kotlin-lint.d.ts → languages/kotlin-lint.d.ts} +1 -1
- package/dist/core/checks/{kotlin-lint.js → languages/kotlin-lint.js} +13 -17
- package/dist/core/checks/{lint.d.ts → languages/lint.d.ts} +1 -1
- package/dist/core/checks/{lint.js → languages/lint.js} +5 -5
- package/dist/core/checks/languages/php-format.d.ts +9 -0
- package/dist/core/checks/languages/php-format.js +49 -0
- package/dist/core/checks/{prettier.d.ts → languages/prettier.d.ts} +1 -1
- package/dist/core/checks/{prettier.js → languages/prettier.js} +5 -5
- package/dist/core/checks/{python-format.d.ts → languages/python-format.d.ts} +1 -1
- package/dist/core/checks/{python-format.js → languages/python-format.js} +3 -3
- package/dist/core/checks/{python-lint.d.ts → languages/python-lint.d.ts} +1 -1
- package/dist/core/checks/{python-lint.js → languages/python-lint.js} +3 -3
- package/dist/core/checks/languages/python-typecheck.d.ts +9 -0
- package/dist/core/checks/languages/python-typecheck.js +66 -0
- package/dist/core/checks/languages/ruby-lint.d.ts +8 -0
- package/dist/core/checks/languages/ruby-lint.js +50 -0
- package/dist/core/checks/languages/rust-format.d.ts +9 -0
- package/dist/core/checks/languages/rust-format.js +45 -0
- package/dist/core/checks/languages/rust-lint.d.ts +8 -0
- package/dist/core/checks/languages/rust-lint.js +50 -0
- package/dist/core/checks/{swift-lint.d.ts → languages/swift-lint.d.ts} +1 -1
- package/dist/core/checks/{swift-lint.js → languages/swift-lint.js} +13 -17
- package/dist/core/checks/{typecheck.d.ts → languages/typecheck.d.ts} +1 -1
- package/dist/core/checks/{typecheck.js → languages/typecheck.js} +3 -3
- package/dist/core/checks/{commitlint.d.ts → pr/commitlint.d.ts} +1 -1
- package/dist/core/checks/{commitlint.js → pr/commitlint.js} +4 -4
- package/dist/core/checks/{pr-body.d.ts → pr/pr-body.d.ts} +1 -1
- package/dist/core/checks/{pr-body.js → pr/pr-body.js} +2 -2
- package/dist/core/checks/{pr-size-check.d.ts → pr/pr-size-check.d.ts} +1 -1
- package/dist/core/checks/{pr-size-check.js → pr/pr-size-check.js} +13 -11
- package/dist/core/checks/{pr-title-lint.d.ts → pr/pr-title-lint.d.ts} +1 -1
- package/dist/core/checks/{pr-title-lint.js → pr/pr-title-lint.js} +2 -2
- package/dist/core/checks/quality/case-collision.d.ts +8 -0
- package/dist/core/checks/quality/case-collision.js +40 -0
- package/dist/core/checks/quality/config-validation.d.ts +8 -0
- package/dist/core/checks/quality/config-validation.js +63 -0
- package/dist/core/checks/quality/coverage.d.ts +2 -0
- package/dist/core/checks/quality/coverage.js +63 -0
- package/dist/core/checks/{dead-code.d.ts → quality/dead-code.d.ts} +1 -1
- package/dist/core/checks/{dead-code.js → quality/dead-code.js} +4 -4
- package/dist/core/checks/{duplicate-check.d.ts → quality/duplicate-check.d.ts} +1 -1
- package/dist/core/checks/{duplicate-check.js → quality/duplicate-check.js} +6 -6
- package/dist/core/checks/quality/license-header.d.ts +7 -0
- package/dist/core/checks/quality/license-header.js +74 -0
- package/dist/core/checks/{markdown-lint.d.ts → quality/markdown-lint.d.ts} +1 -1
- package/dist/core/checks/{markdown-lint.js → quality/markdown-lint.js} +5 -5
- package/dist/core/checks/{spellcheck.d.ts → quality/spellcheck.d.ts} +1 -1
- package/dist/core/checks/{spellcheck.js → quality/spellcheck.js} +5 -5
- package/dist/core/checks/{diff-security.d.ts → security/diff-security.d.ts} +1 -1
- package/dist/core/checks/security/diff-security.js +262 -0
- package/dist/core/checks/security/dockerfile-security.d.ts +2 -0
- package/dist/core/checks/security/dockerfile-security.js +92 -0
- package/dist/core/checks/security/migration-safety.d.ts +2 -0
- package/dist/core/checks/security/migration-safety.js +110 -0
- package/dist/core/checks/{security.d.ts → security/security.d.ts} +1 -1
- package/dist/core/checks/{security.js → security/security.js} +4 -4
- package/dist/core/checks/{sensitive-file-guard.d.ts → security/sensitive-file-guard.d.ts} +1 -1
- package/dist/core/checks/{sensitive-file-guard.js → security/sensitive-file-guard.js} +4 -4
- package/dist/core/checks/security/workflow-security.d.ts +2 -0
- package/dist/core/checks/security/workflow-security.js +75 -0
- package/dist/core/context.js +14 -0
- package/dist/core/registry.js +73 -28
- package/dist/core/reporters.d.ts +4 -3
- package/dist/core/reporters.js +104 -4
- package/dist/core/run-checks.js +21 -3
- package/dist/core/targets.d.ts +4 -1
- package/dist/core/targets.js +16 -3
- package/dist/core/types.d.ts +13 -1
- package/dist/init.js +63 -0
- package/dist/interfaces/index.d.ts +99 -0
- package/dist/scripts/copy-configs.js +1 -0
- package/dist/utils/detect-language.d.ts +1 -1
- package/dist/utils/detect-language.js +32 -3
- package/dist/utils/glob.d.ts +10 -0
- package/dist/utils/glob.js +25 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +2 -1
- package/pr-checkmate.schema.json +166 -0
- package/src/config/cspell.json +9 -1
- package/src/config/pr-checkmate.schema.json +166 -0
- package/dist/core/checks/diff-security.js +0 -145
- /package/dist/core/checks/{ruff-shared.d.ts → languages/ruff-shared.d.ts} +0 -0
- /package/dist/core/checks/{ruff-shared.js → languages/ruff-shared.js} +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bannedImportsCheck = void 0;
|
|
4
|
+
const utils_1 = require("../../../utils");
|
|
5
|
+
const types_1 = require("../../types");
|
|
6
|
+
const findings_1 = require("../findings");
|
|
7
|
+
const PREFIX = 'Banned Imports';
|
|
8
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
9
|
+
/**
|
|
10
|
+
* Compile `bannedImports` into matchers that catch a banned package via an ES
|
|
11
|
+
* import, a CommonJS require, or its addition to a manifest dependency list.
|
|
12
|
+
*/
|
|
13
|
+
function compileBans(ctx) {
|
|
14
|
+
const list = ctx.config.bannedImports ?? [];
|
|
15
|
+
return list
|
|
16
|
+
.filter(b => b?.name)
|
|
17
|
+
.map(b => {
|
|
18
|
+
const n = escapeRe(b.name);
|
|
19
|
+
const re = new RegExp(`from\\s+['"]${n}(?:/[^'"]*)?['"]|require\\(\\s*['"]${n}(?:/[^'"]*)?['"]|import\\s+['"]${n}['"]|["']${n}["']\\s*:`);
|
|
20
|
+
return {
|
|
21
|
+
message: b.message ?? `banned dependency: ${b.name}`,
|
|
22
|
+
re,
|
|
23
|
+
severity: b.severity === 'error' ? 'error' : 'warn',
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
exports.bannedImportsCheck = {
|
|
28
|
+
name: 'Banned Imports',
|
|
29
|
+
phase: 'informational',
|
|
30
|
+
applies(ctx) {
|
|
31
|
+
return (ctx.config.bannedImports?.length ?? 0) > 0 && Boolean(ctx.baseSha);
|
|
32
|
+
},
|
|
33
|
+
async run(ctx) {
|
|
34
|
+
var _a;
|
|
35
|
+
const bans = compileBans(ctx);
|
|
36
|
+
if (bans.length === 0)
|
|
37
|
+
return (0, types_1.pass)('no banned imports configured');
|
|
38
|
+
try {
|
|
39
|
+
const stdout = await (0, findings_1.diffAddedLines)(ctx, ['*']);
|
|
40
|
+
const errors = {};
|
|
41
|
+
const warnings = {};
|
|
42
|
+
for (const raw of stdout.split('\n')) {
|
|
43
|
+
if (!raw.startsWith('+') || raw.startsWith('+++'))
|
|
44
|
+
continue;
|
|
45
|
+
const content = raw.slice(1);
|
|
46
|
+
for (const ban of bans) {
|
|
47
|
+
if (ban.re.test(content)) {
|
|
48
|
+
const bucket = ban.severity === 'error' ? errors : warnings;
|
|
49
|
+
(bucket[_a = ban.message] ?? (bucket[_a] = [])).push(content.trim().substring(0, 100));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const totalErrors = (0, findings_1.bucketTotal)(errors);
|
|
54
|
+
const totalWarnings = (0, findings_1.bucketTotal)(warnings);
|
|
55
|
+
if (totalErrors === 0 && totalWarnings === 0)
|
|
56
|
+
return (0, types_1.pass)();
|
|
57
|
+
(0, findings_1.logFindings)(errors, { prefix: PREFIX, isError: true, sampleLimit: 3 });
|
|
58
|
+
(0, findings_1.logFindings)(warnings, { prefix: PREFIX, sampleLimit: 3 });
|
|
59
|
+
const parts = [];
|
|
60
|
+
if (totalErrors > 0)
|
|
61
|
+
parts.push(`${totalErrors} banned (error)`);
|
|
62
|
+
if (totalWarnings > 0)
|
|
63
|
+
parts.push(`${totalWarnings} banned (warn)`);
|
|
64
|
+
return totalErrors > 0 ? (0, types_1.fail)(parts.join(', ')) : (0, types_1.warn)(parts.join(', '));
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
utils_1.logger.warn(`[Banned Imports]: ⚠️ Could not analyze diff\n${err}`);
|
|
68
|
+
return (0, types_1.skip)('diff unavailable');
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const circularDepsCheck: Check;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.circularDepsCheck = void 0;
|
|
4
|
-
const config_1 = require("
|
|
5
|
-
const utils_1 = require("
|
|
6
|
-
const execa_1 = require("
|
|
7
|
-
const targets_1 = require("
|
|
8
|
-
const types_1 = require("
|
|
4
|
+
const config_1 = require("../../../config");
|
|
5
|
+
const utils_1 = require("../../../utils");
|
|
6
|
+
const execa_1 = require("../../../utils/execa");
|
|
7
|
+
const targets_1 = require("../../targets");
|
|
8
|
+
const types_1 = require("../../types");
|
|
9
9
|
exports.circularDepsCheck = {
|
|
10
10
|
name: 'Circular Deps',
|
|
11
11
|
phase: 'blocking',
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const dependencyCheck: Check;
|
|
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.dependencyCheck = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const utils_1 = require("
|
|
10
|
-
const execa_1 = require("
|
|
11
|
-
const types_1 = require("
|
|
9
|
+
const utils_1 = require("../../../utils");
|
|
10
|
+
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const types_1 = require("../../types");
|
|
12
12
|
exports.dependencyCheck = {
|
|
13
13
|
name: 'Dependencies',
|
|
14
14
|
phase: 'blocking',
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const licenseCheck: Check;
|
|
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.licenseCheck = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const utils_1 = require("
|
|
10
|
-
const execa_1 = require("
|
|
11
|
-
const types_1 = require("
|
|
9
|
+
const utils_1 = require("../../../utils");
|
|
10
|
+
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const types_1 = require("../../types");
|
|
12
12
|
const FORBIDDEN_LICENSES = ['GPL-2.0', 'GPL-3.0', 'AGPL-3.0', 'LGPL-2.0', 'LGPL-2.1', 'LGPL-3.0'];
|
|
13
13
|
/** Strip the trailing "@version" to get the bare package name (scoped-safe). */
|
|
14
14
|
function bareName(pkgKey) {
|
|
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.npmAuditCheck = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const utils_1 = require("
|
|
10
|
-
const execa_1 = require("
|
|
11
|
-
const types_1 = require("
|
|
9
|
+
const utils_1 = require("../../../utils");
|
|
10
|
+
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const types_1 = require("../../types");
|
|
12
12
|
const SEVERITY_ORDER = ['info', 'low', 'moderate', 'high', 'critical'];
|
|
13
13
|
function isSeverityAtLeast(severity, level) {
|
|
14
14
|
const sevIdx = SEVERITY_ORDER.indexOf(severity);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const outdatedDepsCheck: Check;
|
|
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.outdatedDepsCheck = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const utils_1 = require("
|
|
10
|
-
const execa_1 = require("
|
|
11
|
-
const types_1 = require("
|
|
9
|
+
const utils_1 = require("../../../utils");
|
|
10
|
+
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const types_1 = require("../../types");
|
|
12
12
|
exports.outdatedDepsCheck = {
|
|
13
13
|
name: 'Outdated Deps',
|
|
14
14
|
phase: 'informational',
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Check } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-language dependency vulnerability scan via osv-scanner, which reads
|
|
4
|
+
* lockfiles across ecosystems (npm, PyPI, Go, crates, Maven, …) against the OSV
|
|
5
|
+
* database. Runner-dependency and opt-in (`vulnScan.enabled: true`): osv-scanner
|
|
6
|
+
* must be on the runner, and the check skips gracefully when it is absent.
|
|
7
|
+
* Complements the JS-only `npm-audit` with coverage for every other ecosystem.
|
|
8
|
+
*/
|
|
9
|
+
export declare const vulnScanCheck: Check;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.vulnScanCheck = void 0;
|
|
4
|
+
const utils_1 = require("../../../utils");
|
|
5
|
+
const execa_1 = require("../../../utils/execa");
|
|
6
|
+
const types_1 = require("../../types");
|
|
7
|
+
/**
|
|
8
|
+
* Cross-language dependency vulnerability scan via osv-scanner, which reads
|
|
9
|
+
* lockfiles across ecosystems (npm, PyPI, Go, crates, Maven, …) against the OSV
|
|
10
|
+
* database. Runner-dependency and opt-in (`vulnScan.enabled: true`): osv-scanner
|
|
11
|
+
* must be on the runner, and the check skips gracefully when it is absent.
|
|
12
|
+
* Complements the JS-only `npm-audit` with coverage for every other ecosystem.
|
|
13
|
+
*/
|
|
14
|
+
exports.vulnScanCheck = {
|
|
15
|
+
name: 'Vuln Scan',
|
|
16
|
+
phase: 'informational',
|
|
17
|
+
applies(ctx) {
|
|
18
|
+
return ctx.config.vulnScan?.enabled === true;
|
|
19
|
+
},
|
|
20
|
+
async run(ctx) {
|
|
21
|
+
const result = await (0, execa_1.execa)('osv-scanner', ['-r', '.'], { cwd: ctx.cwd, reject: false });
|
|
22
|
+
// execa v9 with reject:false resolves (not throws) with `code: 'ENOENT'`
|
|
23
|
+
// when the binary is absent — detect the missing scanner on the result.
|
|
24
|
+
if (result.code === 'ENOENT')
|
|
25
|
+
return (0, types_1.skip)('osv-scanner not installed');
|
|
26
|
+
if (result.exitCode === 0) {
|
|
27
|
+
utils_1.logger.info('[Vuln Scan]: ✅ No known vulnerabilities');
|
|
28
|
+
return (0, types_1.pass)();
|
|
29
|
+
}
|
|
30
|
+
const lines = [result.stdout, result.stderr]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join('\n')
|
|
33
|
+
.split('\n')
|
|
34
|
+
.filter(l => /\b(CVE-|GHSA-|vulnerab|advisor)/i.test(l));
|
|
35
|
+
utils_1.logger.warn('[Vuln Scan]: ⚠️ known vulnerabilities found:');
|
|
36
|
+
lines.slice(0, 10).forEach(l => utils_1.logger.warn(` ${l.trim()}`));
|
|
37
|
+
if (lines.length > 10)
|
|
38
|
+
utils_1.logger.warn(` ... and ${lines.length - 10} more`);
|
|
39
|
+
return (0, types_1.warn)(`${lines.length || 'unknown'} vulnerability finding(s) (osv-scanner)`);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import { CheckContext, CheckOutcome } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Client-configurable severity for a check's findings, read from that check's
|
|
4
|
+
* config block (e.g. `mergeConflict.severity`). `error` fails the run, `warn`
|
|
5
|
+
* only advises. Every check declares a natural default; the client overrides it
|
|
6
|
+
* per repo in pr-checkmate.json — promoting an advisory check to a gate, or
|
|
7
|
+
* downgrading a blocking one, with no code change.
|
|
8
|
+
*/
|
|
9
|
+
export type Severity = 'error' | 'warn';
|
|
10
|
+
export interface SeverityEmitter {
|
|
11
|
+
/** Log at the resolved level (`logger.error` for error, `logger.warn` for warn). */
|
|
12
|
+
log: (msg: string) => void;
|
|
13
|
+
/** Prefix icon matching the level. */
|
|
14
|
+
icon: string;
|
|
15
|
+
/** Build the outcome at the resolved level (`fail` for error, `warn` for warn). */
|
|
16
|
+
emit: (details: string) => CheckOutcome;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a check's effective severity (`configured` from config, else its
|
|
20
|
+
* `fallback`) into a matched logger, icon, and outcome constructor — so a check
|
|
21
|
+
* logs and returns consistently at whatever level the client chose.
|
|
22
|
+
*/
|
|
23
|
+
export declare function severityEmitter(configured: Severity | undefined, fallback: Severity): SeverityEmitter;
|
|
2
24
|
/** Findings grouped by label → matching sample strings. */
|
|
3
25
|
export type FindingBucket = Record<string, string[]>;
|
|
4
26
|
/**
|
|
@@ -6,6 +28,14 @@ export type FindingBucket = Record<string, string[]>;
|
|
|
6
28
|
* (zero context, added/copied/modified/renamed files) for added-line scanning.
|
|
7
29
|
*/
|
|
8
30
|
export declare function diffAddedLines(ctx: CheckContext, globs: string[]): Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Strip a trailing line comment (`//` or `#`) from a line, tracking quote state
|
|
33
|
+
* so a marker inside a string literal (or a `#` in `"#fff"`, a `//` in a quoted
|
|
34
|
+
* URL) is preserved. Heuristic, not a tokenizer: block comments and regex
|
|
35
|
+
* literals aren't handled. Used by code-targeting checks so `# print(x)` or
|
|
36
|
+
* `// eval(x)` in a comment isn't flagged as real code.
|
|
37
|
+
*/
|
|
38
|
+
export declare function stripInlineComment(line: string): string;
|
|
9
39
|
/** Total number of findings across all labels in a bucket. */
|
|
10
40
|
export declare function bucketTotal(bucket: FindingBucket): number;
|
|
11
41
|
interface LogOptions {
|
|
@@ -31,10 +61,16 @@ interface SeverityReportOptions {
|
|
|
31
61
|
showCount?: boolean;
|
|
32
62
|
/** Max samples printed per label; omit to print all. */
|
|
33
63
|
sampleLimit?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Finding labels to suppress (client `<check>.ignore`). A label is dropped
|
|
66
|
+
* when it contains any ignore entry (case-insensitive), so a repo can mute a
|
|
67
|
+
* single built-in finding without disabling the whole check.
|
|
68
|
+
*/
|
|
69
|
+
ignore?: string[];
|
|
34
70
|
}
|
|
35
71
|
/**
|
|
36
72
|
* Report error/warning finding buckets: log both, then produce the outcome.
|
|
37
73
|
* Returns pass() when both buckets are empty, otherwise warn() with a summary.
|
|
38
74
|
*/
|
|
39
|
-
export declare function reportSeverityBuckets(
|
|
75
|
+
export declare function reportSeverityBuckets(errorsIn: FindingBucket, warningsIn: FindingBucket, opts: SeverityReportOptions): CheckOutcome;
|
|
40
76
|
export {};
|
|
@@ -1,19 +1,81 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.severityEmitter = severityEmitter;
|
|
3
4
|
exports.diffAddedLines = diffAddedLines;
|
|
5
|
+
exports.stripInlineComment = stripInlineComment;
|
|
4
6
|
exports.bucketTotal = bucketTotal;
|
|
5
7
|
exports.logFindings = logFindings;
|
|
6
8
|
exports.reportSeverityBuckets = reportSeverityBuckets;
|
|
7
9
|
const utils_1 = require("../../utils");
|
|
8
10
|
const execa_1 = require("../../utils/execa");
|
|
9
11
|
const types_1 = require("../types");
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a check's effective severity (`configured` from config, else its
|
|
14
|
+
* `fallback`) into a matched logger, icon, and outcome constructor — so a check
|
|
15
|
+
* logs and returns consistently at whatever level the client chose.
|
|
16
|
+
*/
|
|
17
|
+
function severityEmitter(configured, fallback) {
|
|
18
|
+
const isError = (configured ?? fallback) === 'error';
|
|
19
|
+
return {
|
|
20
|
+
log: (isError ? utils_1.logger.error : utils_1.logger.warn).bind(utils_1.logger),
|
|
21
|
+
icon: isError ? '❌' : '⚠️',
|
|
22
|
+
emit: (details) => (isError ? (0, types_1.fail)(details) : (0, types_1.warn)(details)),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
10
25
|
/**
|
|
11
26
|
* Run `git diff` over the given file globs, returning the raw unified diff
|
|
12
27
|
* (zero context, added/copied/modified/renamed files) for added-line scanning.
|
|
13
28
|
*/
|
|
14
29
|
async function diffAddedLines(ctx, globs) {
|
|
15
|
-
const
|
|
16
|
-
|
|
30
|
+
const args = [
|
|
31
|
+
'diff',
|
|
32
|
+
'--unified=0',
|
|
33
|
+
'--diff-filter=ACMR',
|
|
34
|
+
ctx.baseSha,
|
|
35
|
+
ctx.headSha ?? 'HEAD',
|
|
36
|
+
'--',
|
|
37
|
+
...globs,
|
|
38
|
+
];
|
|
39
|
+
const cacheKey = `d:${args.join('')}`;
|
|
40
|
+
let stdout = ctx.cache?.get(cacheKey);
|
|
41
|
+
if (stdout === undefined) {
|
|
42
|
+
stdout = (await (0, execa_1.execa)('git', args, { cwd: ctx.cwd })).stdout;
|
|
43
|
+
ctx.cache?.set(cacheKey, stdout);
|
|
44
|
+
}
|
|
45
|
+
// Universal inline suppression: strip added lines opted out with the
|
|
46
|
+
// `pr-checkmate-ignore` directive, so every content check that scans this
|
|
47
|
+
// diff honours it without repeating the check itself.
|
|
48
|
+
return stdout
|
|
49
|
+
.split('\n')
|
|
50
|
+
.filter(line => !(line.startsWith('+') && line.includes('pr-checkmate-ignore')))
|
|
51
|
+
.join('\n');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Strip a trailing line comment (`//` or `#`) from a line, tracking quote state
|
|
55
|
+
* so a marker inside a string literal (or a `#` in `"#fff"`, a `//` in a quoted
|
|
56
|
+
* URL) is preserved. Heuristic, not a tokenizer: block comments and regex
|
|
57
|
+
* literals aren't handled. Used by code-targeting checks so `# print(x)` or
|
|
58
|
+
* `// eval(x)` in a comment isn't flagged as real code.
|
|
59
|
+
*/
|
|
60
|
+
function stripInlineComment(line) {
|
|
61
|
+
let quote = '';
|
|
62
|
+
for (let i = 0; i < line.length; i++) {
|
|
63
|
+
const c = line[i];
|
|
64
|
+
if (quote) {
|
|
65
|
+
if (c === quote && line[i - 1] !== '\\')
|
|
66
|
+
quote = '';
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
70
|
+
quote = c;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (c === '#')
|
|
74
|
+
return line.slice(0, i);
|
|
75
|
+
if (c === '/' && line[i + 1] === '/')
|
|
76
|
+
return line.slice(0, i);
|
|
77
|
+
}
|
|
78
|
+
return line;
|
|
17
79
|
}
|
|
18
80
|
/** Total number of findings across all labels in a bucket. */
|
|
19
81
|
function bucketTotal(bucket) {
|
|
@@ -34,11 +96,26 @@ function logFindings(bucket, opts) {
|
|
|
34
96
|
}
|
|
35
97
|
}
|
|
36
98
|
}
|
|
99
|
+
/** Remove bucket entries whose label matches any ignore substring. */
|
|
100
|
+
function dropIgnored(bucket, ignore) {
|
|
101
|
+
if (ignore.length === 0)
|
|
102
|
+
return bucket;
|
|
103
|
+
const lowered = ignore.map(i => i.toLowerCase());
|
|
104
|
+
const kept = {};
|
|
105
|
+
for (const [label, items] of Object.entries(bucket)) {
|
|
106
|
+
if (!lowered.some(i => label.toLowerCase().includes(i)))
|
|
107
|
+
kept[label] = items;
|
|
108
|
+
}
|
|
109
|
+
return kept;
|
|
110
|
+
}
|
|
37
111
|
/**
|
|
38
112
|
* Report error/warning finding buckets: log both, then produce the outcome.
|
|
39
113
|
* Returns pass() when both buckets are empty, otherwise warn() with a summary.
|
|
40
114
|
*/
|
|
41
|
-
function reportSeverityBuckets(
|
|
115
|
+
function reportSeverityBuckets(errorsIn, warningsIn, opts) {
|
|
116
|
+
const ignore = opts.ignore ?? [];
|
|
117
|
+
const errors = dropIgnored(errorsIn, ignore);
|
|
118
|
+
const warnings = dropIgnored(warningsIn, ignore);
|
|
42
119
|
const totalErrors = bucketTotal(errors);
|
|
43
120
|
const totalWarnings = bucketTotal(warnings);
|
|
44
121
|
if (totalErrors === 0 && totalWarnings === 0)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.customRulesCheck = void 0;
|
|
4
|
+
const utils_1 = require("../../../utils");
|
|
5
|
+
const execa_1 = require("../../../utils/execa");
|
|
6
|
+
const types_1 = require("../../types");
|
|
7
|
+
const findings_1 = require("../findings");
|
|
8
|
+
/**
|
|
9
|
+
* Compile the user's `customRules` into ready-to-run matchers. A rule with an
|
|
10
|
+
* invalid regex is skipped with a warning rather than crashing the run. `g`/`y`
|
|
11
|
+
* flags are stripped because `.test()` on a sticky/global regex is stateful and
|
|
12
|
+
* would skip matches across lines.
|
|
13
|
+
*/
|
|
14
|
+
function compileRules(ctx) {
|
|
15
|
+
const rules = ctx.config.customRules ?? [];
|
|
16
|
+
const compiled = [];
|
|
17
|
+
for (const r of rules) {
|
|
18
|
+
if (!r?.name || !r?.pattern)
|
|
19
|
+
continue;
|
|
20
|
+
let re;
|
|
21
|
+
try {
|
|
22
|
+
re = new RegExp(r.pattern, (r.flags ?? '').replace(/[gy]/g, ''));
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
utils_1.logger.warn(`[Custom Rules]: ⚠️ invalid regex in rule "${r.name}" — skipped\n${err}`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
compiled.push({
|
|
29
|
+
message: r.message ?? r.name,
|
|
30
|
+
re,
|
|
31
|
+
severity: r.severity === 'error' ? 'error' : 'warn',
|
|
32
|
+
include: r.include,
|
|
33
|
+
exclude: r.exclude,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return compiled;
|
|
37
|
+
}
|
|
38
|
+
const PREFIX = 'Custom Rules';
|
|
39
|
+
/** Parse the diff, bucketing rule matches on added lines by severity. */
|
|
40
|
+
function scanDiff(stdout, rules) {
|
|
41
|
+
var _a;
|
|
42
|
+
const errors = {};
|
|
43
|
+
const warnings = {};
|
|
44
|
+
let file = '';
|
|
45
|
+
for (const raw of stdout.split('\n')) {
|
|
46
|
+
if (raw.startsWith('+++ ')) {
|
|
47
|
+
const p = raw.slice(4).trim();
|
|
48
|
+
file = p === '/dev/null' ? '' : p.replace(/^b\//, '');
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!raw.startsWith('+') || raw.startsWith('+++'))
|
|
52
|
+
continue;
|
|
53
|
+
if (raw.includes('pr-checkmate-ignore'))
|
|
54
|
+
continue;
|
|
55
|
+
const content = raw.slice(1);
|
|
56
|
+
for (const rule of rules) {
|
|
57
|
+
if (rule.include && !(0, utils_1.matchesAnyGlob)(file, rule.include))
|
|
58
|
+
continue;
|
|
59
|
+
if (rule.exclude && (0, utils_1.matchesAnyGlob)(file, rule.exclude))
|
|
60
|
+
continue;
|
|
61
|
+
if (rule.re.test(content)) {
|
|
62
|
+
const bucket = rule.severity === 'error' ? errors : warnings;
|
|
63
|
+
(bucket[_a = rule.message] ?? (bucket[_a] = [])).push(`${file}: ${content.trim().substring(0, 100)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { errors, warnings };
|
|
68
|
+
}
|
|
69
|
+
exports.customRulesCheck = {
|
|
70
|
+
name: 'Custom Rules',
|
|
71
|
+
phase: 'informational',
|
|
72
|
+
applies(ctx) {
|
|
73
|
+
return (ctx.config.customRules?.length ?? 0) > 0 && Boolean(ctx.baseSha);
|
|
74
|
+
},
|
|
75
|
+
async run(ctx) {
|
|
76
|
+
const rules = compileRules(ctx);
|
|
77
|
+
if (rules.length === 0)
|
|
78
|
+
return (0, types_1.pass)('no valid custom rules');
|
|
79
|
+
try {
|
|
80
|
+
const { stdout } = await (0, execa_1.execa)('git', ['diff', '--unified=0', '--diff-filter=ACMR', ctx.baseSha, ctx.headSha ?? 'HEAD'], { cwd: ctx.cwd });
|
|
81
|
+
const { errors, warnings } = scanDiff(stdout, rules);
|
|
82
|
+
const totalErrors = (0, findings_1.bucketTotal)(errors);
|
|
83
|
+
const totalWarnings = (0, findings_1.bucketTotal)(warnings);
|
|
84
|
+
if (totalErrors === 0 && totalWarnings === 0)
|
|
85
|
+
return (0, types_1.pass)();
|
|
86
|
+
(0, findings_1.logFindings)(errors, { prefix: PREFIX, isError: true, sampleLimit: 3 });
|
|
87
|
+
(0, findings_1.logFindings)(warnings, { prefix: PREFIX, sampleLimit: 3 });
|
|
88
|
+
const parts = [];
|
|
89
|
+
if (totalErrors > 0)
|
|
90
|
+
parts.push(`${totalErrors} error(s)`);
|
|
91
|
+
if (totalWarnings > 0)
|
|
92
|
+
parts.push(`${totalWarnings} warning(s)`);
|
|
93
|
+
const summary = parts.join(', ');
|
|
94
|
+
// A rule with severity "error" blocks the run (fail), regardless of phase.
|
|
95
|
+
return totalErrors > 0 ? (0, types_1.fail)(summary) : (0, types_1.warn)(summary);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
utils_1.logger.warn(`[Custom Rules]: ⚠️ Could not analyze diff\n${err}`);
|
|
99
|
+
return (0, types_1.skip)('diff unavailable');
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Check } from '
|
|
1
|
+
import { Check } from '../../types';
|
|
2
2
|
export declare const diffSmellCheck: Check;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.diffSmellCheck = void 0;
|
|
4
|
-
const utils_1 = require("
|
|
5
|
-
const types_1 = require("
|
|
6
|
-
const findings_1 = require("
|
|
4
|
+
const utils_1 = require("../../../utils");
|
|
5
|
+
const types_1 = require("../../types");
|
|
6
|
+
const findings_1 = require("../findings");
|
|
7
7
|
const SMELL_PATTERNS = [
|
|
8
8
|
{
|
|
9
9
|
pattern: /^\+(?!\+)(?:[^/]|\/[^/]).*\bconsole\.(log|error|warn|debug|info)\s*\(/,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.largeFilesCheck = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const utils_1 = require("../../../utils");
|
|
10
|
+
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const targets_1 = require("../../targets");
|
|
12
|
+
const types_1 = require("../../types");
|
|
13
|
+
const findings_1 = require("../findings");
|
|
14
|
+
const DEFAULT_MAX_KB = 500;
|
|
15
|
+
/**
|
|
16
|
+
* Parse `git diff --numstat` and collect files git reports as binary — those
|
|
17
|
+
* rows carry `-` in both the additions and deletions columns.
|
|
18
|
+
*/
|
|
19
|
+
function binaryPaths(numstat) {
|
|
20
|
+
const binary = new Set();
|
|
21
|
+
for (const line of numstat.split('\n')) {
|
|
22
|
+
if (!line)
|
|
23
|
+
continue;
|
|
24
|
+
const [add, del, ...rest] = line.split('\t');
|
|
25
|
+
if (add === '-' && del === '-')
|
|
26
|
+
binary.add(rest.join('\t'));
|
|
27
|
+
}
|
|
28
|
+
return binary;
|
|
29
|
+
}
|
|
30
|
+
exports.largeFilesCheck = {
|
|
31
|
+
name: 'Large Files',
|
|
32
|
+
phase: 'informational',
|
|
33
|
+
applies(ctx) {
|
|
34
|
+
return ctx.config.largeFiles?.enabled !== false && Boolean(ctx.baseSha);
|
|
35
|
+
},
|
|
36
|
+
async run(ctx) {
|
|
37
|
+
// resolveTargetFiles (not the scoped variant): a bloat guard should catch
|
|
38
|
+
// large files anywhere in the repo — a root-level asset is exactly the case
|
|
39
|
+
// a narrowed `sourcePath` would miss — but still skip `ignoreDirs`
|
|
40
|
+
// (dist/build/vendor), where large generated files are expected.
|
|
41
|
+
const files = await (0, targets_1.resolveTargetFiles)(ctx, ['*']);
|
|
42
|
+
if (files === null)
|
|
43
|
+
return (0, types_1.skip)('git unavailable');
|
|
44
|
+
if (files.length === 0)
|
|
45
|
+
return (0, types_1.pass)();
|
|
46
|
+
const maxKb = ctx.config.largeFiles?.maxSizeKb ?? DEFAULT_MAX_KB;
|
|
47
|
+
const maxBytes = maxKb * 1024;
|
|
48
|
+
const allow = ctx.config.largeFiles?.allow ?? [];
|
|
49
|
+
let binary;
|
|
50
|
+
try {
|
|
51
|
+
const { stdout } = await (0, execa_1.execa)('git', ['diff', '--numstat', '--diff-filter=ACM', ctx.baseSha, ctx.headSha ?? 'HEAD'], { cwd: ctx.cwd });
|
|
52
|
+
binary = binaryPaths(stdout);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
utils_1.logger.warn(`[Large Files]: ⚠️ Could not analyze diff\n${err}`);
|
|
56
|
+
return (0, types_1.skip)('diff unavailable');
|
|
57
|
+
}
|
|
58
|
+
const findings = [];
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
if (allow.length > 0 && (0, utils_1.matchesAnyGlob)(file, allow))
|
|
61
|
+
continue;
|
|
62
|
+
let size;
|
|
63
|
+
try {
|
|
64
|
+
size = node_fs_1.default.statSync(node_path_1.default.join(ctx.cwd, file)).size;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue; // renamed/removed between diff and stat — skip
|
|
68
|
+
}
|
|
69
|
+
const reasons = [];
|
|
70
|
+
if (size > maxBytes)
|
|
71
|
+
reasons.push(`${Math.round(size / 1024)} KB > ${maxKb} KB`);
|
|
72
|
+
if (binary.has(file))
|
|
73
|
+
reasons.push('binary');
|
|
74
|
+
if (reasons.length > 0)
|
|
75
|
+
findings.push(`${file} (${reasons.join(', ')})`);
|
|
76
|
+
}
|
|
77
|
+
if (findings.length === 0)
|
|
78
|
+
return (0, types_1.pass)();
|
|
79
|
+
const sev = (0, findings_1.severityEmitter)(ctx.config.largeFiles?.severity, 'warn');
|
|
80
|
+
findings.forEach(f => sev.log(`[Large Files]: ${sev.icon} ${f}`));
|
|
81
|
+
return sev.emit(`${findings.length} large/binary file(s) added`);
|
|
82
|
+
},
|
|
83
|
+
};
|