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,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;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.missingTestsCheck = 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
+ /** Source-file extensions across the supported languages. */
11
+ const SOURCE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|kt|kts|java|swift|cpp|cc|cxx|c|h|hpp|go|rb)$/i;
12
+ /** Test-file conventions across languages. */
13
+ function isTestFile(file) {
14
+ return (/\.(test|spec)\.[cm]?[jt]sx?$/i.test(file) || // foo.test.ts, foo.spec.jsx
15
+ /(^|\/)__tests__\//.test(file) ||
16
+ /(^|\/)tests?\//i.test(file) || // test/ or tests/ dir
17
+ /(^|\/)test_[^/]+\.py$/.test(file) || // test_foo.py
18
+ /_test\.(py|go)$/.test(file) || // foo_test.py, foo_test.go
19
+ /(Test|Spec)s?\.(kt|java|swift)$/.test(file) // FooTest.kt, FooSpec.swift
20
+ );
21
+ }
22
+ /**
23
+ * Source files that legitimately ship without a test of their own. Setting
24
+ * `missingTests.ignore` REPLACES this list (same convention as duplicate/
25
+ * dead-code ignores).
26
+ */
27
+ const DEFAULT_IGNORE = [
28
+ '**/*.d.ts',
29
+ '**/index.ts',
30
+ '**/index.tsx',
31
+ '**/index.js',
32
+ '**/index.jsx',
33
+ '**/*.config.*',
34
+ '**/migrations/**',
35
+ '**/migrate/**',
36
+ '**/__init__.py',
37
+ ];
38
+ exports.missingTestsCheck = {
39
+ name: 'Missing Tests',
40
+ phase: 'informational',
41
+ applies(ctx) {
42
+ return ctx.config.missingTests?.enabled !== false && Boolean(ctx.baseSha);
43
+ },
44
+ async run(ctx) {
45
+ // Scope the same way lint does — a client's `sourcePath` and `ignoreDirs`
46
+ // both apply, so generated/out-of-scope dirs never count as "new source".
47
+ const scopedList = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*'], (0, config_1.getSourcePaths)());
48
+ if (scopedList === null)
49
+ return (0, types_1.skip)('git unavailable');
50
+ const scoped = new Set(scopedList);
51
+ const added = [];
52
+ const touched = [];
53
+ try {
54
+ const { stdout } = await (0, execa_1.execa)('git', [
55
+ 'diff',
56
+ '--name-status',
57
+ '--diff-filter=ACM',
58
+ ctx.baseSha,
59
+ ctx.headSha ?? 'HEAD',
60
+ ], { cwd: ctx.cwd });
61
+ for (const line of stdout.split('\n')) {
62
+ if (!line)
63
+ continue;
64
+ const [status, ...rest] = line.split('\t');
65
+ const file = rest.join('\t');
66
+ if (!file || !scoped.has(file))
67
+ continue;
68
+ touched.push(file);
69
+ if (status === 'A')
70
+ added.push(file);
71
+ }
72
+ }
73
+ catch (err) {
74
+ utils_1.logger.warn(`[Missing Tests]: ⚠️ Could not analyze diff\n${err}`);
75
+ return (0, types_1.skip)('diff unavailable');
76
+ }
77
+ const ignore = ctx.config.missingTests?.ignore ?? DEFAULT_IGNORE;
78
+ const addedSource = added.filter(f => SOURCE_EXT.test(f) && !isTestFile(f) && !(0, utils_1.matchesAnyGlob)(f, ignore));
79
+ if (addedSource.length === 0)
80
+ return (0, types_1.pass)(); // no new source that would need a test
81
+ // A test added OR modified anywhere in scope satisfies the check.
82
+ if (touched.some(isTestFile))
83
+ return (0, types_1.pass)();
84
+ const sev = (0, findings_1.severityEmitter)(ctx.config.missingTests?.severity, 'warn');
85
+ addedSource
86
+ .slice(0, 5)
87
+ .forEach(f => sev.log(`[Missing Tests]: ${sev.icon} new source without tests: ${f}`));
88
+ if (addedSource.length > 5)
89
+ sev.log(`[Missing Tests]: ${sev.icon} ... and ${addedSource.length - 5} more`);
90
+ return sev.emit(`${addedSource.length} new source file(s) added without any test changes`);
91
+ },
92
+ };
@@ -0,0 +1,7 @@
1
+ import { Check } from '../../types';
2
+ /**
3
+ * Flags leftover TODO/FIXME/XXX/HACK markers in Python and Kotlin/Java comments.
4
+ * JS/TS markers are already handled by the Diff Smells check, so this stays out
5
+ * of those globs to avoid double-reporting (same split as Leftover Debug).
6
+ */
7
+ export declare const todoFixmeCheck: Check;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.todoFixmeCheck = void 0;
4
+ const utils_1 = require("../../../utils");
5
+ const types_1 = require("../../types");
6
+ const findings_1 = require("../findings");
7
+ const DEFAULT_KEYWORDS = ['TODO', 'FIXME', 'XXX', 'HACK'];
8
+ /** A ticket reference in the comment: `#123` or JIRA-style `PROJ-456`. */
9
+ const TICKET_PATTERN = /#\d+|[A-Z][A-Z0-9]+-\d+/;
10
+ /**
11
+ * Build an added-line matcher for a comment style. `commentFragment` is a regex
12
+ * alternation of the language's comment openers (`#` for Python; `//`/`/*` for
13
+ * Kotlin/Java). Keywords are escaped and captured so the finding is labelled.
14
+ */
15
+ function buildPattern(commentFragment, keywords) {
16
+ const kw = keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
17
+ return new RegExp(`^\\+.*(?:${commentFragment})\\s*(${kw})\\b`, 'i');
18
+ }
19
+ /** Bucket matching added lines by keyword, honouring opt-out and requireTicket. */
20
+ function scan(stdout, pattern, requireTicket, findings) {
21
+ for (const line of stdout.split('\n')) {
22
+ if (line.includes('pr-checkmate-ignore'))
23
+ continue;
24
+ const match = pattern.exec(line);
25
+ if (!match)
26
+ continue;
27
+ // With requireTicket, a marker that already links a ticket is acceptable.
28
+ if (requireTicket && TICKET_PATTERN.test(line))
29
+ continue;
30
+ const keyword = match[1].toUpperCase();
31
+ (findings[keyword] ?? (findings[keyword] = [])).push(line.slice(1).trim().substring(0, 100));
32
+ }
33
+ }
34
+ /**
35
+ * Flags leftover TODO/FIXME/XXX/HACK markers in Python and Kotlin/Java comments.
36
+ * JS/TS markers are already handled by the Diff Smells check, so this stays out
37
+ * of those globs to avoid double-reporting (same split as Leftover Debug).
38
+ */
39
+ exports.todoFixmeCheck = {
40
+ name: 'TODO/FIXME',
41
+ phase: 'informational',
42
+ applies(ctx) {
43
+ return (ctx.config.todoFixme?.enabled !== false &&
44
+ Boolean(ctx.baseSha) &&
45
+ (ctx.languages.includes('python') || ctx.languages.includes('kotlin')));
46
+ },
47
+ async run(ctx) {
48
+ try {
49
+ const cfg = ctx.config.todoFixme;
50
+ const keywords = cfg?.keywords ?? DEFAULT_KEYWORDS;
51
+ const requireTicket = cfg?.requireTicket ?? false;
52
+ const findings = {};
53
+ if (ctx.languages.includes('python')) {
54
+ scan(await (0, findings_1.diffAddedLines)(ctx, ['*.py']), buildPattern('#', keywords), requireTicket, findings);
55
+ }
56
+ if (ctx.languages.includes('kotlin')) {
57
+ const ktPattern = buildPattern('//|/\\*', keywords);
58
+ scan(await (0, findings_1.diffAddedLines)(ctx, ['*.kt', '*.kts', '*.java']), ktPattern, requireTicket, findings);
59
+ }
60
+ if ((0, findings_1.bucketTotal)(findings) === 0)
61
+ return (0, types_1.pass)();
62
+ const sev = (0, findings_1.severityEmitter)(cfg?.severity, 'warn');
63
+ (0, findings_1.logFindings)(findings, { prefix: 'TODO/FIXME', sampleLimit: 3, isError: sev.icon === '❌' });
64
+ const summary = Object.entries(findings)
65
+ .map(([label, arr]) => `${arr.length}× ${label}`)
66
+ .join(', ') + (requireTicket ? ' (no ticket reference)' : '');
67
+ return sev.emit(summary);
68
+ }
69
+ catch (err) {
70
+ utils_1.logger.warn(`[TODO/FIXME]: ⚠️ Could not analyze diff\n${err}`);
71
+ return (0, types_1.skip)('diff unavailable');
72
+ }
73
+ },
74
+ };
@@ -1,4 +1,4 @@
1
- import { Check } from '../types';
1
+ import { Check } from '../../types';
2
2
  /**
3
3
  * Formats C/C++ code with clang-format, bundled as a self-contained WASM CLI
4
4
  * (`@wasm-fmt/clang-format`) so it runs on a bare Node image with no system
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.cppFormatCheck = 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
  /**
13
13
  * C/C++ source & header extensions clang-format understands. `.h` is intentionally
14
14
  * included even though it is ambiguous with C — clang-format handles both.
@@ -0,0 +1,13 @@
1
+ import { Check } from '../../types';
2
+ /**
3
+ * Formats/verifies C# code with `dotnet format` (which also runs the configured
4
+ * analyzers/whitespace/style rules). Runner-dependency: needs the .NET SDK on
5
+ * the runner, gracefully skipping when absent. Read-only in check mode
6
+ * (`--verify-no-changes`); rewrites in place with `dotnet format` when `write`,
7
+ * so CI can auto-commit — mirrors the other format checks.
8
+ *
9
+ * Note: `dotnet format` operates on the project/solution (needs a prior
10
+ * `dotnet restore`), so it runs over the whole project; the delta check only
11
+ * gates whether it runs at all.
12
+ */
13
+ export declare const csharpFormatCheck: Check;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.csharpFormatCheck = void 0;
4
+ const utils_1 = require("../../../utils");
5
+ const execa_1 = require("../../../utils/execa");
6
+ const targets_1 = require("../../targets");
7
+ const types_1 = require("../../types");
8
+ const CS_GLOBS = ['*.cs'];
9
+ /**
10
+ * Formats/verifies C# code with `dotnet format` (which also runs the configured
11
+ * analyzers/whitespace/style rules). Runner-dependency: needs the .NET SDK on
12
+ * the runner, gracefully skipping when absent. Read-only in check mode
13
+ * (`--verify-no-changes`); rewrites in place with `dotnet format` when `write`,
14
+ * so CI can auto-commit — mirrors the other format checks.
15
+ *
16
+ * Note: `dotnet format` operates on the project/solution (needs a prior
17
+ * `dotnet restore`), so it runs over the whole project; the delta check only
18
+ * gates whether it runs at all.
19
+ */
20
+ exports.csharpFormatCheck = {
21
+ name: 'C# Format',
22
+ phase: 'format',
23
+ applies(ctx) {
24
+ return ctx.config.csharp?.enabled !== false && ctx.languages.includes('csharp');
25
+ },
26
+ async run(ctx) {
27
+ const targets = await (0, targets_1.resolveTargetFiles)(ctx, CS_GLOBS);
28
+ if (targets === null)
29
+ return (0, types_1.skip)('git unavailable');
30
+ if (targets.length === 0)
31
+ return (0, types_1.pass)('no C# files changed');
32
+ const check = await (0, execa_1.execa)('dotnet', ['format', '--verify-no-changes'], {
33
+ cwd: ctx.cwd,
34
+ reject: false,
35
+ });
36
+ if (check.code === 'ENOENT')
37
+ return (0, types_1.skip)('dotnet SDK not installed');
38
+ if (check.exitCode === 0)
39
+ return (0, types_1.pass)();
40
+ if (!ctx.write) {
41
+ utils_1.logger.warn('[C# Format]: ⚠️ C# code needs formatting (dotnet format --verify-no-changes failed)');
42
+ return (0, types_1.warn)('C# code needs formatting (run with write to fix)');
43
+ }
44
+ const fix = await (0, execa_1.execa)('dotnet', ['format'], { cwd: ctx.cwd, reject: false });
45
+ if (fix.exitCode !== 0) {
46
+ utils_1.logger.warn(`[C# Format]: ⚠️ dotnet format failed\n${fix.stderr}`);
47
+ return (0, types_1.warn)('dotnet format failed to rewrite files');
48
+ }
49
+ utils_1.logger.info('[C# Format]: ✅ Reformatted C# code');
50
+ return (0, types_1.pass)('reformatted C# code');
51
+ },
52
+ };
@@ -0,0 +1,8 @@
1
+ import { Check } from '../../types';
2
+ /**
3
+ * Formats changed Go files with `gofmt`. Runner-dependency (needs `gofmt` on the
4
+ * runner), gracefully skipping when absent. Read-only in check mode (`gofmt -l`
5
+ * lists files that differ from gofmt output); rewrites in place with `gofmt -w`
6
+ * when `write`, so CI can auto-commit — mirrors the Prettier/C++/Ruff format checks.
7
+ */
8
+ export declare const goFormatCheck: Check;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.goFormatCheck = void 0;
4
+ const utils_1 = require("../../../utils");
5
+ const execa_1 = require("../../../utils/execa");
6
+ const targets_1 = require("../../targets");
7
+ const types_1 = require("../../types");
8
+ const GO_GLOBS = ['*.go'];
9
+ /**
10
+ * Formats changed Go files with `gofmt`. Runner-dependency (needs `gofmt` on the
11
+ * runner), gracefully skipping when absent. Read-only in check mode (`gofmt -l`
12
+ * lists files that differ from gofmt output); rewrites in place with `gofmt -w`
13
+ * when `write`, so CI can auto-commit — mirrors the Prettier/C++/Ruff format checks.
14
+ */
15
+ exports.goFormatCheck = {
16
+ name: 'Go Format',
17
+ phase: 'format',
18
+ applies(ctx) {
19
+ return ctx.config.go?.enabled !== false && ctx.languages.includes('go');
20
+ },
21
+ async run(ctx) {
22
+ const targets = await (0, targets_1.resolveTargetFiles)(ctx, GO_GLOBS);
23
+ if (targets === null)
24
+ return (0, types_1.skip)('git unavailable');
25
+ if (targets.length === 0)
26
+ return (0, types_1.pass)('no Go files changed');
27
+ const list = await (0, execa_1.execa)('gofmt', ['-l', ...targets], { cwd: ctx.cwd, reject: false });
28
+ // execa v9 with reject:false resolves (not throws) with `code: 'ENOENT'` when
29
+ // the binary is absent — detect the missing toolchain on the result.
30
+ if (list.code === 'ENOENT')
31
+ return (0, types_1.skip)('gofmt not installed');
32
+ const unformatted = list.stdout.split('\n').filter(Boolean);
33
+ if (unformatted.length === 0)
34
+ return (0, types_1.pass)();
35
+ if (!ctx.write) {
36
+ utils_1.logger.warn(`[Go Format]: ⚠️ ${unformatted.length} Go file(s) need formatting`);
37
+ unformatted.slice(0, 10).forEach(f => utils_1.logger.warn(` ${f}`));
38
+ return (0, types_1.warn)(`${unformatted.length} file(s) need formatting (run with write to fix)`);
39
+ }
40
+ const fix = await (0, execa_1.execa)('gofmt', ['-w', ...unformatted], { cwd: ctx.cwd, reject: false });
41
+ if (fix.exitCode !== 0) {
42
+ utils_1.logger.warn(`[Go Format]: ⚠️ gofmt -w failed\n${fix.stderr}`);
43
+ return (0, types_1.warn)('gofmt failed to rewrite files');
44
+ }
45
+ utils_1.logger.info(`[Go Format]: ✅ Reformatted ${unformatted.length} Go file(s)`);
46
+ return (0, types_1.pass)(`reformatted ${unformatted.length} file(s)`);
47
+ },
48
+ };
@@ -0,0 +1,8 @@
1
+ import { Check } from '../../types';
2
+ /**
3
+ * Vets changed Go code with `go vet ./...`. Runner-dependency: requires the Go
4
+ * toolchain on the runner (like SwiftLint/ktlint), and gracefully skips when
5
+ * `go` is absent. `go vet` operates per-package, so it runs over the module
6
+ * rather than a file list; the delta check only gates whether it runs at all.
7
+ */
8
+ export declare const goLintCheck: Check;