pr-checkmate 1.22.7 → 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 (140) 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 +25 -1
  5. package/dist/config/constants.js +157 -38
  6. package/dist/config/index.d.ts +15 -2
  7. package/dist/config/index.js +28 -7
  8. package/dist/config/loader.js +5 -1
  9. package/dist/core/checks/dependencies/banned-imports.d.ts +2 -0
  10. package/dist/core/checks/dependencies/banned-imports.js +71 -0
  11. package/dist/core/checks/{circular-deps.d.ts → dependencies/circular-deps.d.ts} +1 -1
  12. package/dist/core/checks/{circular-deps.js → dependencies/circular-deps.js} +9 -10
  13. package/dist/core/checks/{dependency-check.d.ts → dependencies/dependency-check.d.ts} +1 -1
  14. package/dist/core/checks/{dependency-check.js → dependencies/dependency-check.js} +3 -3
  15. package/dist/core/checks/{license-check.d.ts → dependencies/license-check.d.ts} +1 -1
  16. package/dist/core/checks/{license-check.js → dependencies/license-check.js} +3 -3
  17. package/dist/core/checks/{npm-audit.d.ts → dependencies/npm-audit.d.ts} +1 -1
  18. package/dist/core/checks/{npm-audit.js → dependencies/npm-audit.js} +3 -3
  19. package/dist/core/checks/{outdated-deps.d.ts → dependencies/outdated-deps.d.ts} +1 -1
  20. package/dist/core/checks/{outdated-deps.js → dependencies/outdated-deps.js} +3 -3
  21. package/dist/core/checks/dependencies/vuln-scan.d.ts +9 -0
  22. package/dist/core/checks/dependencies/vuln-scan.js +41 -0
  23. package/dist/core/checks/findings.d.ts +37 -1
  24. package/dist/core/checks/findings.js +80 -3
  25. package/dist/core/checks/git/custom-rules.d.ts +2 -0
  26. package/dist/core/checks/git/custom-rules.js +102 -0
  27. package/dist/core/checks/{diff-smell.d.ts → git/diff-smell.d.ts} +1 -1
  28. package/dist/core/checks/{diff-smell.js → git/diff-smell.js} +3 -3
  29. package/dist/core/checks/git/large-files.d.ts +2 -0
  30. package/dist/core/checks/git/large-files.js +83 -0
  31. package/dist/core/checks/git/leftover-debug.d.ts +2 -0
  32. package/dist/core/checks/git/leftover-debug.js +77 -0
  33. package/dist/core/checks/git/lockfile-drift.d.ts +2 -0
  34. package/dist/core/checks/git/lockfile-drift.js +114 -0
  35. package/dist/core/checks/git/merge-conflict.d.ts +2 -0
  36. package/dist/core/checks/git/merge-conflict.js +109 -0
  37. package/dist/core/checks/git/missing-tests.d.ts +2 -0
  38. package/dist/core/checks/git/missing-tests.js +92 -0
  39. package/dist/core/checks/git/todo-fixme.d.ts +7 -0
  40. package/dist/core/checks/git/todo-fixme.js +74 -0
  41. package/dist/core/checks/{cpp-format.d.ts → languages/cpp-format.d.ts} +1 -1
  42. package/dist/core/checks/{cpp-format.js → languages/cpp-format.js} +3 -3
  43. package/dist/core/checks/languages/csharp-format.d.ts +13 -0
  44. package/dist/core/checks/languages/csharp-format.js +52 -0
  45. package/dist/core/checks/languages/go-format.d.ts +8 -0
  46. package/dist/core/checks/languages/go-format.js +48 -0
  47. package/dist/core/checks/languages/go-lint.d.ts +8 -0
  48. package/dist/core/checks/languages/go-lint.js +48 -0
  49. package/dist/core/checks/{kotlin-lint.d.ts → languages/kotlin-lint.d.ts} +1 -1
  50. package/dist/core/checks/{kotlin-lint.js → languages/kotlin-lint.js} +13 -17
  51. package/dist/core/checks/{lint.d.ts → languages/lint.d.ts} +1 -1
  52. package/dist/core/checks/{lint.js → languages/lint.js} +25 -14
  53. package/dist/core/checks/languages/php-format.d.ts +9 -0
  54. package/dist/core/checks/languages/php-format.js +49 -0
  55. package/dist/core/checks/{prettier.d.ts → languages/prettier.d.ts} +1 -1
  56. package/dist/core/checks/{prettier.js → languages/prettier.js} +10 -10
  57. package/dist/core/checks/{python-format.d.ts → languages/python-format.d.ts} +1 -1
  58. package/dist/core/checks/{python-format.js → languages/python-format.js} +3 -3
  59. package/dist/core/checks/{python-lint.d.ts → languages/python-lint.d.ts} +1 -1
  60. package/dist/core/checks/{python-lint.js → languages/python-lint.js} +3 -3
  61. package/dist/core/checks/languages/python-typecheck.d.ts +9 -0
  62. package/dist/core/checks/languages/python-typecheck.js +66 -0
  63. package/dist/core/checks/languages/ruby-lint.d.ts +8 -0
  64. package/dist/core/checks/languages/ruby-lint.js +50 -0
  65. package/dist/core/checks/languages/rust-format.d.ts +9 -0
  66. package/dist/core/checks/languages/rust-format.js +45 -0
  67. package/dist/core/checks/languages/rust-lint.d.ts +8 -0
  68. package/dist/core/checks/languages/rust-lint.js +50 -0
  69. package/dist/core/checks/{swift-lint.d.ts → languages/swift-lint.d.ts} +1 -1
  70. package/dist/core/checks/{swift-lint.js → languages/swift-lint.js} +13 -17
  71. package/dist/core/checks/{typecheck.d.ts → languages/typecheck.d.ts} +1 -1
  72. package/dist/core/checks/{typecheck.js → languages/typecheck.js} +3 -3
  73. package/dist/core/checks/{commitlint.d.ts → pr/commitlint.d.ts} +1 -1
  74. package/dist/core/checks/{commitlint.js → pr/commitlint.js} +4 -4
  75. package/dist/core/checks/{pr-body.d.ts → pr/pr-body.d.ts} +1 -1
  76. package/dist/core/checks/{pr-body.js → pr/pr-body.js} +2 -2
  77. package/dist/core/checks/{pr-size-check.d.ts → pr/pr-size-check.d.ts} +1 -1
  78. package/dist/core/checks/{pr-size-check.js → pr/pr-size-check.js} +13 -11
  79. package/dist/core/checks/{pr-title-lint.d.ts → pr/pr-title-lint.d.ts} +1 -1
  80. package/dist/core/checks/{pr-title-lint.js → pr/pr-title-lint.js} +2 -2
  81. package/dist/core/checks/quality/case-collision.d.ts +8 -0
  82. package/dist/core/checks/quality/case-collision.js +40 -0
  83. package/dist/core/checks/quality/config-validation.d.ts +8 -0
  84. package/dist/core/checks/quality/config-validation.js +63 -0
  85. package/dist/core/checks/quality/coverage.d.ts +2 -0
  86. package/dist/core/checks/quality/coverage.js +63 -0
  87. package/dist/core/checks/{dead-code.d.ts → quality/dead-code.d.ts} +1 -1
  88. package/dist/core/checks/{dead-code.js → quality/dead-code.js} +4 -4
  89. package/dist/core/checks/{duplicate-check.d.ts → quality/duplicate-check.d.ts} +1 -1
  90. package/dist/core/checks/{duplicate-check.js → quality/duplicate-check.js} +11 -15
  91. package/dist/core/checks/quality/license-header.d.ts +7 -0
  92. package/dist/core/checks/quality/license-header.js +74 -0
  93. package/dist/core/checks/{markdown-lint.d.ts → quality/markdown-lint.d.ts} +1 -1
  94. package/dist/core/checks/{markdown-lint.js → quality/markdown-lint.js} +8 -21
  95. package/dist/core/checks/{spellcheck.d.ts → quality/spellcheck.d.ts} +1 -1
  96. package/dist/core/checks/{spellcheck.js → quality/spellcheck.js} +11 -19
  97. package/dist/core/checks/{diff-security.d.ts → security/diff-security.d.ts} +1 -1
  98. package/dist/core/checks/security/diff-security.js +262 -0
  99. package/dist/core/checks/security/dockerfile-security.d.ts +2 -0
  100. package/dist/core/checks/security/dockerfile-security.js +92 -0
  101. package/dist/core/checks/security/migration-safety.d.ts +2 -0
  102. package/dist/core/checks/security/migration-safety.js +110 -0
  103. package/dist/core/checks/{security.d.ts → security/security.d.ts} +1 -1
  104. package/dist/core/checks/{security.js → security/security.js} +4 -4
  105. package/dist/core/checks/{sensitive-file-guard.d.ts → security/sensitive-file-guard.d.ts} +1 -1
  106. package/dist/core/checks/{sensitive-file-guard.js → security/sensitive-file-guard.js} +4 -4
  107. package/dist/core/checks/security/workflow-security.d.ts +2 -0
  108. package/dist/core/checks/security/workflow-security.js +75 -0
  109. package/dist/core/context.js +54 -10
  110. package/dist/core/registry.js +73 -28
  111. package/dist/core/reporters.d.ts +4 -3
  112. package/dist/core/reporters.js +104 -4
  113. package/dist/core/run-checks.js +21 -3
  114. package/dist/core/targets.d.ts +17 -1
  115. package/dist/core/targets.js +74 -4
  116. package/dist/core/types.d.ts +13 -1
  117. package/dist/index.d.ts +1 -1
  118. package/dist/index.js +3 -1
  119. package/dist/init.js +86 -8
  120. package/dist/interfaces/index.d.ts +101 -1
  121. package/dist/scripts/copy-configs.js +6 -1
  122. package/dist/utils/detect-language.d.ts +1 -1
  123. package/dist/utils/detect-language.js +32 -3
  124. package/dist/utils/glob.d.ts +10 -0
  125. package/dist/utils/glob.js +25 -0
  126. package/dist/utils/index.d.ts +1 -1
  127. package/dist/utils/index.js +1 -1
  128. package/eslint-default-rules.json +62 -0
  129. package/eslint.config.mjs +6 -66
  130. package/package.json +3 -1
  131. package/pr-checkmate.schema.json +166 -0
  132. package/src/config/cspell.json +9 -1
  133. package/src/config/eslint-default-rules.json +62 -0
  134. package/src/config/pr-checkmate.schema.json +166 -0
  135. package/dist/core/checks/diff-security.js +0 -145
  136. package/dist/utils/delta.d.ts +0 -5
  137. package/dist/utils/delta.js +0 -53
  138. package/src/config/eslint.config.mjs +0 -180
  139. /package/dist/core/checks/{ruff-shared.d.ts → languages/ruff-shared.d.ts} +0 -0
  140. /package/dist/core/checks/{ruff-shared.js → languages/ruff-shared.js} +0 -0
@@ -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;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.leftoverDebugCheck = void 0;
4
+ const utils_1 = require("../../../utils");
5
+ const types_1 = require("../../types");
6
+ const findings_1 = require("../findings");
7
+ /**
8
+ * Debug statements for languages NOT already covered by the Diff Smells check
9
+ * (which handles `console.*`/`debugger` for JS/TS). Kept separate so the two
10
+ * checks never double-report the same finding in a PR comment.
11
+ */
12
+ const PYTHON_PATTERNS = [
13
+ { pattern: /^\+(?!\+).*\bprint\s*\(/, label: 'print()' },
14
+ { pattern: /^\+(?!\+).*\bpprint\s*\(/, label: 'pprint()' },
15
+ { pattern: /^\+(?!\+).*\bbreakpoint\s*\(/, label: 'breakpoint()' },
16
+ { pattern: /^\+(?!\+).*\bpdb\.set_trace\s*\(/, label: 'pdb.set_trace()' },
17
+ ];
18
+ const KOTLIN_PATTERNS = [
19
+ { pattern: /^\+(?!\+).*\bSystem\.(out|err)\.println\s*\(/, label: 'System.out/err.println()' },
20
+ // Bare println(...), but not the qualified System.out.println above — the
21
+ // negative lookbehind rejects a preceding `.` or identifier char.
22
+ { pattern: /^\+(?!\+).*(?<![.\w])println\s*\(/, label: 'println()' },
23
+ ];
24
+ /** Bucket added-line matches by label, skipping lines opted out via directive. */
25
+ function collect(stdout, patterns, findings) {
26
+ for (const line of stdout.split('\n')) {
27
+ // Honour an inline opt-out in any comment style (`// pr-checkmate-ignore`,
28
+ // `# pr-checkmate-ignore`).
29
+ if (line.includes('pr-checkmate-ignore'))
30
+ continue;
31
+ // Match on the comment-stripped line so `# print(x)` in a comment isn't flagged.
32
+ const scanned = (0, findings_1.stripInlineComment)(line);
33
+ for (const { pattern, label } of patterns) {
34
+ if (pattern.test(scanned)) {
35
+ (findings[label] ?? (findings[label] = [])).push(line.slice(1).trim().substring(0, 100));
36
+ }
37
+ }
38
+ }
39
+ }
40
+ exports.leftoverDebugCheck = {
41
+ name: 'Leftover Debug',
42
+ phase: 'informational',
43
+ applies(ctx) {
44
+ return (ctx.config.leftoverDebug?.enabled !== false &&
45
+ Boolean(ctx.baseSha) &&
46
+ (ctx.languages.includes('python') || ctx.languages.includes('kotlin')));
47
+ },
48
+ async run(ctx) {
49
+ try {
50
+ const findings = {};
51
+ if (ctx.languages.includes('python')) {
52
+ collect(await (0, findings_1.diffAddedLines)(ctx, ['*.py']), PYTHON_PATTERNS, findings);
53
+ }
54
+ if (ctx.languages.includes('kotlin')) {
55
+ collect(await (0, findings_1.diffAddedLines)(ctx, ['*.kt', '*.kts', '*.java']), KOTLIN_PATTERNS, findings);
56
+ }
57
+ if ((0, findings_1.bucketTotal)(findings) === 0)
58
+ return (0, types_1.pass)();
59
+ // Advisory by default; a repo may promote to a gate via
60
+ // `leftoverDebug.severity: 'error'`.
61
+ const sev = (0, findings_1.severityEmitter)(ctx.config.leftoverDebug?.severity, 'warn');
62
+ (0, findings_1.logFindings)(findings, {
63
+ prefix: 'Leftover Debug',
64
+ sampleLimit: 3,
65
+ isError: sev.icon === '❌',
66
+ });
67
+ const summary = Object.entries(findings)
68
+ .map(([label, arr]) => `${arr.length}× ${label}`)
69
+ .join(', ');
70
+ return sev.emit(summary);
71
+ }
72
+ catch (err) {
73
+ utils_1.logger.warn(`[Leftover Debug]: ⚠️ Could not analyze diff\n${err}`);
74
+ return (0, types_1.skip)('diff unavailable');
75
+ }
76
+ },
77
+ };
@@ -0,0 +1,2 @@
1
+ import { Check } from '../../types';
2
+ export declare const lockfileDriftCheck: Check;
@@ -0,0 +1,114 @@
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.lockfileDriftCheck = 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 types_1 = require("../../types");
12
+ const findings_1 = require("../findings");
13
+ /**
14
+ * Manifest → its lockfile(s). A change to the manifest's dependency set without
15
+ * a corresponding lockfile update is drift: CI resolves versions the author
16
+ * never locked. Matched by basename so monorepo sub-packages work too.
17
+ */
18
+ const LOCK_PAIRS = [
19
+ { manifest: 'package.json', locks: ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'] },
20
+ { manifest: 'pyproject.toml', locks: ['poetry.lock', 'uv.lock'] },
21
+ { manifest: 'requirements.in', locks: ['requirements.txt'] },
22
+ { manifest: 'go.mod', locks: ['go.sum'] },
23
+ { manifest: 'Cargo.toml', locks: ['Cargo.lock'] },
24
+ { manifest: 'Gemfile', locks: ['Gemfile.lock'] },
25
+ ];
26
+ const DEP_KEYS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
27
+ /** Extract only the dependency sections of a parsed package.json, or `{}`. */
28
+ function extractDeps(raw) {
29
+ try {
30
+ const json = JSON.parse(raw);
31
+ const deps = {};
32
+ for (const key of DEP_KEYS)
33
+ if (json[key])
34
+ deps[key] = json[key];
35
+ return deps;
36
+ }
37
+ catch {
38
+ return {};
39
+ }
40
+ }
41
+ /**
42
+ * True when a package.json's dependency sections differ between base and head.
43
+ * Guards against false positives from `version`/`scripts`-only edits that don't
44
+ * need a lockfile refresh. A newly added manifest (no base version) counts as
45
+ * changed. Other ecosystems have no zero-dependency parser here, so they trigger
46
+ * on any manifest change.
47
+ */
48
+ async function packageDepsChanged(ctx, manifestPath, base) {
49
+ let headDeps = {};
50
+ try {
51
+ headDeps = extractDeps(node_fs_1.default.readFileSync(node_path_1.default.join(ctx.cwd, manifestPath), 'utf-8'));
52
+ }
53
+ catch {
54
+ // Head file unreadable (e.g. deleted) — treat as changed so it surfaces.
55
+ return true;
56
+ }
57
+ const { stdout, exitCode } = await (0, execa_1.execa)('git', ['show', `${base}:${manifestPath}`], {
58
+ cwd: ctx.cwd,
59
+ reject: false,
60
+ });
61
+ const baseDeps = exitCode === 0 ? extractDeps(stdout) : {};
62
+ return JSON.stringify(headDeps) !== JSON.stringify(baseDeps);
63
+ }
64
+ exports.lockfileDriftCheck = {
65
+ name: 'Lockfile Drift',
66
+ phase: 'informational',
67
+ applies(ctx) {
68
+ return ctx.config.lockfileDrift?.enabled !== false && Boolean(ctx.baseSha);
69
+ },
70
+ async run(ctx) {
71
+ try {
72
+ // Raw name-only diff — manifests/locks live at the repo root, outside any
73
+ // configured `sourcePath`, so this deliberately does NOT scope (same as
74
+ // the Sensitive Files guard). Scoping would hide the very files we check.
75
+ const { stdout } = await (0, execa_1.execa)('git', [
76
+ 'diff',
77
+ '--name-only',
78
+ '--diff-filter=ACMRD',
79
+ ctx.baseSha,
80
+ ctx.headSha ?? 'HEAD',
81
+ ], { cwd: ctx.cwd });
82
+ const changed = new Set(stdout.split('\n').filter(Boolean));
83
+ const warnings = [];
84
+ for (const changedPath of changed) {
85
+ const pair = LOCK_PAIRS.find(p => p.manifest === node_path_1.default.basename(changedPath));
86
+ if (!pair)
87
+ continue;
88
+ // For package.json, only flag when the dependency set actually changed.
89
+ if (pair.manifest === 'package.json') {
90
+ if (!(await packageDepsChanged(ctx, changedPath, ctx.baseSha)))
91
+ continue;
92
+ }
93
+ const dir = node_path_1.default.dirname(changedPath);
94
+ const lockPath = (lock) => (dir === '.' ? lock : `${dir}/${lock}`);
95
+ const presentLocks = pair.locks.filter(lock => node_fs_1.default.existsSync(node_path_1.default.join(ctx.cwd, lockPath(lock))));
96
+ if (presentLocks.length === 0)
97
+ continue; // no lockfile committed → nothing to drift
98
+ const anyLockChanged = presentLocks.some(lock => changed.has(lockPath(lock)));
99
+ if (!anyLockChanged) {
100
+ warnings.push(`${changedPath} changed but ${presentLocks.join(' / ')} not updated`);
101
+ }
102
+ }
103
+ if (warnings.length === 0)
104
+ return (0, types_1.pass)();
105
+ const sev = (0, findings_1.severityEmitter)(ctx.config.lockfileDrift?.severity, 'warn');
106
+ warnings.forEach(w => sev.log(`[Lockfile Drift]: ${sev.icon} ${w}`));
107
+ return sev.emit(`${warnings.length} manifest(s) changed without a lockfile update`);
108
+ }
109
+ catch (err) {
110
+ utils_1.logger.warn(`[Lockfile Drift]: ⚠️ Could not analyze diff\n${err}`);
111
+ return (0, types_1.skip)('diff unavailable');
112
+ }
113
+ },
114
+ };
@@ -0,0 +1,2 @@
1
+ import { Check } from '../../types';
2
+ export declare const mergeConflictCheck: Check;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeConflictCheck = 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");
9
+ const findings_1 = require("../findings");
10
+ /**
11
+ * Git conflict markers, matched against the content of an added diff line
12
+ * (the leading `+` already stripped). Each is exactly seven characters at the
13
+ * start of the line, optionally followed by a space + branch/ref label
14
+ * (`=======` stands alone). `|||||||` is the diff3 base-section marker.
15
+ */
16
+ const MARKER_START = /^<{7}(?: |$)/;
17
+ const MARKER_BASE = /^\|{7}(?: |$)/;
18
+ const MARKER_SEP = /^={7}$/;
19
+ const MARKER_END = /^>{7}(?: |$)/;
20
+ /** An angle marker (`<<<<<<<` / `>>>>>>>`) is unambiguous on its own. */
21
+ function isAngleMarker(content) {
22
+ return MARKER_START.test(content) || MARKER_END.test(content);
23
+ }
24
+ /**
25
+ * Parse a `git diff --unified=0` and return every added line that looks like a
26
+ * conflict marker, tracking the file and new-file line number for reporting.
27
+ */
28
+ function scanDiff(diff) {
29
+ const hits = [];
30
+ let file = '';
31
+ let newLine = 0;
32
+ for (const raw of diff.split('\n')) {
33
+ if (raw.startsWith('+++ ')) {
34
+ // `+++ b/path` (or `+++ /dev/null` for deletions). Strip the `b/` prefix.
35
+ const path = raw.slice(4).trim();
36
+ file = path === '/dev/null' ? '' : path.replace(/^b\//, '');
37
+ continue;
38
+ }
39
+ if (raw.startsWith('---'))
40
+ continue; // old-file header, never a content line
41
+ if (raw.startsWith('@@')) {
42
+ // @@ -oldStart,oldLen +newStart,newLen @@ — reset the new-file counter.
43
+ const m = /\+(\d+)/.exec(raw);
44
+ newLine = m ? Number(m[1]) : 0;
45
+ continue;
46
+ }
47
+ if (raw.startsWith('+')) {
48
+ const content = raw.slice(1);
49
+ if (file &&
50
+ (isAngleMarker(content) || MARKER_BASE.test(content) || MARKER_SEP.test(content))) {
51
+ hits.push({ file, line: newLine, angle: isAngleMarker(content) });
52
+ }
53
+ newLine += 1;
54
+ }
55
+ // Removed (`-`) lines don't advance the new-file counter; with --unified=0
56
+ // there are no context lines to account for.
57
+ }
58
+ return hits;
59
+ }
60
+ /**
61
+ * Keep every angle/base marker, but a bare `=======` only counts when the same
62
+ * file also carries an angle marker. A standalone `=======` is a legitimate
63
+ * markdown setext-heading underline; requiring a bracketing `<<<<<<<`/`>>>>>>>`
64
+ * in the file avoids that false positive while still catching real conflicts.
65
+ */
66
+ function filterHits(hits) {
67
+ const filesWithAngle = new Set(hits.filter(h => h.angle).map(h => h.file));
68
+ return hits.filter(h => h.angle || filesWithAngle.has(h.file));
69
+ }
70
+ exports.mergeConflictCheck = {
71
+ name: 'Merge Conflict',
72
+ phase: 'blocking',
73
+ applies(ctx) {
74
+ return ctx.config.mergeConflict?.enabled !== false && Boolean(ctx.baseSha);
75
+ },
76
+ async run(ctx) {
77
+ // Resolve the scoped file set the canonical way (delta-aware, honouring the
78
+ // client's `ignoreDirs` and `sourcePath`), then diff only those files for
79
+ // content. `['*']` matches every path since conflicts are language-agnostic;
80
+ // scoping still keeps build/vendor/report dirs out, like every other check.
81
+ const files = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*'], (0, config_1.getSourcePaths)());
82
+ if (files === null)
83
+ return (0, types_1.skip)('git unavailable');
84
+ if (files.length === 0)
85
+ return (0, types_1.pass)();
86
+ try {
87
+ const { stdout } = await (0, execa_1.execa)('git', ['diff', '--unified=0', ctx.baseSha, ctx.headSha ?? 'HEAD', '--', ...files], { cwd: ctx.cwd });
88
+ const hits = filterHits(scanDiff(stdout));
89
+ if (hits.length === 0)
90
+ return (0, types_1.pass)();
91
+ // Conflict markers are a hard error by default; a repo may downgrade to
92
+ // `warn` via `mergeConflict.severity`.
93
+ const sev = (0, findings_1.severityEmitter)(ctx.config.mergeConflict?.severity, 'error');
94
+ for (const h of hits) {
95
+ sev.log(`[Merge Conflict]: ${sev.icon} ${h.file}:${h.line}`);
96
+ }
97
+ const sample = hits
98
+ .slice(0, 5)
99
+ .map(h => `${h.file}:${h.line}`)
100
+ .join(', ');
101
+ const more = hits.length > 5 ? ` (+${hits.length - 5} more)` : '';
102
+ return sev.emit(`${hits.length} unresolved conflict marker(s): ${sample}${more}`);
103
+ }
104
+ catch (err) {
105
+ utils_1.logger.warn(`[Merge Conflict]: ⚠️ Could not analyze diff\n${err}`);
106
+ return (0, types_1.skip)('diff unavailable');
107
+ }
108
+ },
109
+ };
@@ -0,0 +1,2 @@
1
+ import { Check } from '../../types';
2
+ export declare const missingTestsCheck: Check;