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.
Files changed (129) hide show
  1. package/README.md +101 -333
  2. package/cspell.json +9 -1
  3. package/dist/cli.js +32 -2
  4. package/dist/config/constants.d.ts +9 -0
  5. package/dist/config/constants.js +68 -1
  6. package/dist/core/checks/dependencies/banned-imports.d.ts +2 -0
  7. package/dist/core/checks/dependencies/banned-imports.js +71 -0
  8. package/dist/core/checks/{circular-deps.d.ts → dependencies/circular-deps.d.ts} +1 -1
  9. package/dist/core/checks/{circular-deps.js → dependencies/circular-deps.js} +5 -5
  10. package/dist/core/checks/{dependency-check.d.ts → dependencies/dependency-check.d.ts} +1 -1
  11. package/dist/core/checks/{dependency-check.js → dependencies/dependency-check.js} +3 -3
  12. package/dist/core/checks/{license-check.d.ts → dependencies/license-check.d.ts} +1 -1
  13. package/dist/core/checks/{license-check.js → dependencies/license-check.js} +3 -3
  14. package/dist/core/checks/{npm-audit.d.ts → dependencies/npm-audit.d.ts} +1 -1
  15. package/dist/core/checks/{npm-audit.js → dependencies/npm-audit.js} +3 -3
  16. package/dist/core/checks/{outdated-deps.d.ts → dependencies/outdated-deps.d.ts} +1 -1
  17. package/dist/core/checks/{outdated-deps.js → dependencies/outdated-deps.js} +3 -3
  18. package/dist/core/checks/dependencies/vuln-scan.d.ts +9 -0
  19. package/dist/core/checks/dependencies/vuln-scan.js +41 -0
  20. package/dist/core/checks/findings.d.ts +37 -1
  21. package/dist/core/checks/findings.js +80 -3
  22. package/dist/core/checks/git/custom-rules.d.ts +2 -0
  23. package/dist/core/checks/git/custom-rules.js +102 -0
  24. package/dist/core/checks/{diff-smell.d.ts → git/diff-smell.d.ts} +1 -1
  25. package/dist/core/checks/{diff-smell.js → git/diff-smell.js} +3 -3
  26. package/dist/core/checks/git/large-files.d.ts +2 -0
  27. package/dist/core/checks/git/large-files.js +83 -0
  28. package/dist/core/checks/git/leftover-debug.d.ts +2 -0
  29. package/dist/core/checks/git/leftover-debug.js +77 -0
  30. package/dist/core/checks/git/lockfile-drift.d.ts +2 -0
  31. package/dist/core/checks/git/lockfile-drift.js +114 -0
  32. package/dist/core/checks/git/merge-conflict.d.ts +2 -0
  33. package/dist/core/checks/git/merge-conflict.js +109 -0
  34. package/dist/core/checks/git/missing-tests.d.ts +2 -0
  35. package/dist/core/checks/git/missing-tests.js +92 -0
  36. package/dist/core/checks/git/todo-fixme.d.ts +7 -0
  37. package/dist/core/checks/git/todo-fixme.js +74 -0
  38. package/dist/core/checks/{cpp-format.d.ts → languages/cpp-format.d.ts} +1 -1
  39. package/dist/core/checks/{cpp-format.js → languages/cpp-format.js} +3 -3
  40. package/dist/core/checks/languages/csharp-format.d.ts +13 -0
  41. package/dist/core/checks/languages/csharp-format.js +52 -0
  42. package/dist/core/checks/languages/go-format.d.ts +8 -0
  43. package/dist/core/checks/languages/go-format.js +48 -0
  44. package/dist/core/checks/languages/go-lint.d.ts +8 -0
  45. package/dist/core/checks/languages/go-lint.js +48 -0
  46. package/dist/core/checks/{kotlin-lint.d.ts → languages/kotlin-lint.d.ts} +1 -1
  47. package/dist/core/checks/{kotlin-lint.js → languages/kotlin-lint.js} +13 -17
  48. package/dist/core/checks/{lint.d.ts → languages/lint.d.ts} +1 -1
  49. package/dist/core/checks/{lint.js → languages/lint.js} +5 -5
  50. package/dist/core/checks/languages/php-format.d.ts +9 -0
  51. package/dist/core/checks/languages/php-format.js +49 -0
  52. package/dist/core/checks/{prettier.d.ts → languages/prettier.d.ts} +1 -1
  53. package/dist/core/checks/{prettier.js → languages/prettier.js} +5 -5
  54. package/dist/core/checks/{python-format.d.ts → languages/python-format.d.ts} +1 -1
  55. package/dist/core/checks/{python-format.js → languages/python-format.js} +3 -3
  56. package/dist/core/checks/{python-lint.d.ts → languages/python-lint.d.ts} +1 -1
  57. package/dist/core/checks/{python-lint.js → languages/python-lint.js} +3 -3
  58. package/dist/core/checks/languages/python-typecheck.d.ts +9 -0
  59. package/dist/core/checks/languages/python-typecheck.js +66 -0
  60. package/dist/core/checks/languages/ruby-lint.d.ts +8 -0
  61. package/dist/core/checks/languages/ruby-lint.js +50 -0
  62. package/dist/core/checks/languages/rust-format.d.ts +9 -0
  63. package/dist/core/checks/languages/rust-format.js +45 -0
  64. package/dist/core/checks/languages/rust-lint.d.ts +8 -0
  65. package/dist/core/checks/languages/rust-lint.js +50 -0
  66. package/dist/core/checks/{swift-lint.d.ts → languages/swift-lint.d.ts} +1 -1
  67. package/dist/core/checks/{swift-lint.js → languages/swift-lint.js} +13 -17
  68. package/dist/core/checks/{typecheck.d.ts → languages/typecheck.d.ts} +1 -1
  69. package/dist/core/checks/{typecheck.js → languages/typecheck.js} +3 -3
  70. package/dist/core/checks/{commitlint.d.ts → pr/commitlint.d.ts} +1 -1
  71. package/dist/core/checks/{commitlint.js → pr/commitlint.js} +4 -4
  72. package/dist/core/checks/{pr-body.d.ts → pr/pr-body.d.ts} +1 -1
  73. package/dist/core/checks/{pr-body.js → pr/pr-body.js} +2 -2
  74. package/dist/core/checks/{pr-size-check.d.ts → pr/pr-size-check.d.ts} +1 -1
  75. package/dist/core/checks/{pr-size-check.js → pr/pr-size-check.js} +13 -11
  76. package/dist/core/checks/{pr-title-lint.d.ts → pr/pr-title-lint.d.ts} +1 -1
  77. package/dist/core/checks/{pr-title-lint.js → pr/pr-title-lint.js} +2 -2
  78. package/dist/core/checks/quality/case-collision.d.ts +8 -0
  79. package/dist/core/checks/quality/case-collision.js +40 -0
  80. package/dist/core/checks/quality/config-validation.d.ts +8 -0
  81. package/dist/core/checks/quality/config-validation.js +63 -0
  82. package/dist/core/checks/quality/coverage.d.ts +2 -0
  83. package/dist/core/checks/quality/coverage.js +63 -0
  84. package/dist/core/checks/{dead-code.d.ts → quality/dead-code.d.ts} +1 -1
  85. package/dist/core/checks/{dead-code.js → quality/dead-code.js} +4 -4
  86. package/dist/core/checks/{duplicate-check.d.ts → quality/duplicate-check.d.ts} +1 -1
  87. package/dist/core/checks/{duplicate-check.js → quality/duplicate-check.js} +6 -6
  88. package/dist/core/checks/quality/license-header.d.ts +7 -0
  89. package/dist/core/checks/quality/license-header.js +74 -0
  90. package/dist/core/checks/{markdown-lint.d.ts → quality/markdown-lint.d.ts} +1 -1
  91. package/dist/core/checks/{markdown-lint.js → quality/markdown-lint.js} +5 -5
  92. package/dist/core/checks/{spellcheck.d.ts → quality/spellcheck.d.ts} +1 -1
  93. package/dist/core/checks/{spellcheck.js → quality/spellcheck.js} +5 -5
  94. package/dist/core/checks/{diff-security.d.ts → security/diff-security.d.ts} +1 -1
  95. package/dist/core/checks/security/diff-security.js +262 -0
  96. package/dist/core/checks/security/dockerfile-security.d.ts +2 -0
  97. package/dist/core/checks/security/dockerfile-security.js +92 -0
  98. package/dist/core/checks/security/migration-safety.d.ts +2 -0
  99. package/dist/core/checks/security/migration-safety.js +110 -0
  100. package/dist/core/checks/{security.d.ts → security/security.d.ts} +1 -1
  101. package/dist/core/checks/{security.js → security/security.js} +4 -4
  102. package/dist/core/checks/{sensitive-file-guard.d.ts → security/sensitive-file-guard.d.ts} +1 -1
  103. package/dist/core/checks/{sensitive-file-guard.js → security/sensitive-file-guard.js} +4 -4
  104. package/dist/core/checks/security/workflow-security.d.ts +2 -0
  105. package/dist/core/checks/security/workflow-security.js +75 -0
  106. package/dist/core/context.js +14 -0
  107. package/dist/core/registry.js +73 -28
  108. package/dist/core/reporters.d.ts +4 -3
  109. package/dist/core/reporters.js +104 -4
  110. package/dist/core/run-checks.js +21 -3
  111. package/dist/core/targets.d.ts +4 -1
  112. package/dist/core/targets.js +16 -3
  113. package/dist/core/types.d.ts +13 -1
  114. package/dist/init.js +63 -0
  115. package/dist/interfaces/index.d.ts +99 -0
  116. package/dist/scripts/copy-configs.js +1 -0
  117. package/dist/utils/detect-language.d.ts +1 -1
  118. package/dist/utils/detect-language.js +32 -3
  119. package/dist/utils/glob.d.ts +10 -0
  120. package/dist/utils/glob.js +25 -0
  121. package/dist/utils/index.d.ts +1 -0
  122. package/dist/utils/index.js +1 -0
  123. package/package.json +2 -1
  124. package/pr-checkmate.schema.json +166 -0
  125. package/src/config/cspell.json +9 -1
  126. package/src/config/pr-checkmate.schema.json +166 -0
  127. package/dist/core/checks/diff-security.js +0 -145
  128. /package/dist/core/checks/{ruff-shared.d.ts → languages/ruff-shared.d.ts} +0 -0
  129. /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 '../types';
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("../../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");
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 '../types';
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("../../utils");
10
- const execa_1 = require("../../utils/execa");
11
- const types_1 = require("../types");
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 '../types';
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("../../utils");
10
- const execa_1 = require("../../utils/execa");
11
- const types_1 = require("../types");
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) {
@@ -1,4 +1,4 @@
1
- import { Check } from '../types';
1
+ import { Check } from '../../types';
2
2
  /**
3
3
  * npm audit — only direct-dependency vulnerabilities block. Transitive ones and
4
4
  * vulnerabilities in pr-checkmate itself are ignored (the client cannot fix those).
@@ -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("../../utils");
10
- const execa_1 = require("../../utils/execa");
11
- const types_1 = require("../types");
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 '../types';
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("../../utils");
10
- const execa_1 = require("../../utils/execa");
11
- const types_1 = require("../types");
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(errors: FindingBucket, warnings: FindingBucket, opts: SeverityReportOptions): CheckOutcome;
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 { stdout } = await (0, execa_1.execa)('git', ['diff', '--unified=0', '--diff-filter=ACMR', ctx.baseSha, 'HEAD', '--', ...globs], { cwd: ctx.cwd });
16
- return stdout;
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(errors, warnings, opts) {
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,2 @@
1
+ import { Check } from '../../types';
2
+ export declare const customRulesCheck: Check;
@@ -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 '../types';
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("../../utils");
5
- const types_1 = require("../types");
6
- const findings_1 = require("./findings");
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,2 @@
1
+ import { Check } from '../../types';
2
+ export declare const largeFilesCheck: Check;
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ import { Check } from '../../types';
2
+ export declare const leftoverDebugCheck: Check;