pr-checkmate 2.1.2 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +63 -268
  2. package/cspell.json +470 -447
  3. package/dist/cli.js +1 -1
  4. package/dist/config/constants.d.ts +6 -1
  5. package/dist/config/constants.js +9 -7
  6. package/dist/config/index.d.ts +23 -11
  7. package/dist/config/index.js +27 -33
  8. package/dist/core/checks/dependencies/banned-imports.js +4 -1
  9. package/dist/core/checks/dependencies/circular-deps.js +14 -5
  10. package/dist/core/checks/dependencies/dependency-check.js +1 -1
  11. package/dist/core/checks/dependencies/grype-scan.js +2 -1
  12. package/dist/core/checks/dependencies/vuln-scan.js +2 -1
  13. package/dist/core/checks/findings.d.ts +46 -2
  14. package/dist/core/checks/findings.js +140 -2
  15. package/dist/core/checks/git/custom-rules.js +2 -0
  16. package/dist/core/checks/git/diff-smell.js +6 -1
  17. package/dist/core/checks/git/leftover-debug.js +8 -2
  18. package/dist/core/checks/git/lockfile-drift.js +6 -1
  19. package/dist/core/checks/git/merge-conflict.js +1 -1
  20. package/dist/core/checks/git/missing-tests.js +1 -1
  21. package/dist/core/checks/git/todo-fixme.js +10 -2
  22. package/dist/core/checks/languages/cpp-format.js +19 -42
  23. package/dist/core/checks/languages/external-checks.d.ts +77 -0
  24. package/dist/core/checks/languages/external-checks.js +200 -0
  25. package/dist/core/checks/languages/external-runner.d.ts +119 -0
  26. package/dist/core/checks/languages/external-runner.js +143 -0
  27. package/dist/core/checks/languages/go-format.d.ts +4 -4
  28. package/dist/core/checks/languages/go-format.js +151 -30
  29. package/dist/core/checks/languages/lint.js +19 -19
  30. package/dist/core/checks/languages/prettier.js +2 -7
  31. package/dist/core/checks/languages/python-typecheck.d.ts +10 -5
  32. package/dist/core/checks/languages/python-typecheck.js +161 -37
  33. package/dist/core/checks/languages/ruff-shared.d.ts +3 -3
  34. package/dist/core/checks/languages/ruff-shared.js +27 -4
  35. package/dist/core/checks/languages/typecheck.js +10 -8
  36. package/dist/core/checks/pr/commitlint.js +7 -1
  37. package/dist/core/checks/quality/case-collision.js +6 -4
  38. package/dist/core/checks/quality/config-validation.d.ts +0 -7
  39. package/dist/core/checks/quality/config-validation.js +30 -0
  40. package/dist/core/checks/quality/coverage.js +6 -1
  41. package/dist/core/checks/quality/dead-code.js +12 -3
  42. package/dist/core/checks/quality/duplicate-check.js +54 -27
  43. package/dist/core/checks/quality/license-header.js +7 -4
  44. package/dist/core/checks/quality/markdown-lint.js +1 -6
  45. package/dist/core/checks/quality/package-size.d.ts +15 -0
  46. package/dist/core/checks/quality/package-size.js +130 -0
  47. package/dist/core/checks/quality/spellcheck.js +34 -5
  48. package/dist/core/checks/quality/symlinks.js +6 -4
  49. package/dist/core/checks/security/diff-security.js +26 -18
  50. package/dist/core/checks/security/dockerfile-security.js +7 -1
  51. package/dist/core/checks/security/migration-safety.js +32 -4
  52. package/dist/core/checks/security/security.d.ts +56 -0
  53. package/dist/core/checks/security/security.js +135 -11
  54. package/dist/core/checks/security/sensitive-file-guard.js +40 -6
  55. package/dist/core/checks/security/workflow-security.js +7 -1
  56. package/dist/core/checks/tool-install.d.ts +102 -0
  57. package/dist/core/checks/tool-install.js +69 -0
  58. package/dist/core/commands.js +1 -0
  59. package/dist/core/context.js +9 -1
  60. package/dist/core/install/cache.d.ts +20 -1
  61. package/dist/core/install/cache.js +65 -12
  62. package/dist/core/install/doctor.js +49 -15
  63. package/dist/core/install/ensure.d.ts +31 -14
  64. package/dist/core/install/ensure.js +165 -61
  65. package/dist/core/install/lock.d.ts +2 -1
  66. package/dist/core/install/merge.d.ts +47 -0
  67. package/dist/core/install/merge.js +91 -0
  68. package/dist/core/install/prepare.d.ts +76 -0
  69. package/dist/core/install/prepare.js +147 -0
  70. package/dist/core/install/profiles.d.ts +17 -3
  71. package/dist/core/install/profiles.js +27 -0
  72. package/dist/core/install/reuse.d.ts +41 -0
  73. package/dist/core/install/reuse.js +107 -0
  74. package/dist/core/registry.js +12 -18
  75. package/dist/core/run-checks.js +74 -3
  76. package/dist/core/targets.d.ts +116 -5
  77. package/dist/core/targets.js +101 -16
  78. package/dist/core/types.d.ts +8 -2
  79. package/dist/init.js +21 -1
  80. package/dist/interfaces/index.d.ts +6 -0
  81. package/dist/utils/resolve-bin.d.ts +56 -6
  82. package/dist/utils/resolve-bin.js +136 -26
  83. package/gitleaks.toml +14 -0
  84. package/install-manifests/commitlint/package-lock.json +128 -250
  85. package/install-manifests/commitlint/package.json +2 -2
  86. package/install-manifests/core/package-lock.json +76 -76
  87. package/install-manifests/core/package.json +1 -1
  88. package/install-manifests/duplicate/package-lock.json +45 -45
  89. package/install-manifests/duplicate/package.json +1 -1
  90. package/install-manifests/go/package-lock.json +24 -0
  91. package/install-manifests/go/package.json +8 -0
  92. package/install-manifests/js/package-lock.json +248 -283
  93. package/install-manifests/js/package.json +9 -9
  94. package/install-manifests/python/package-lock.json +4 -4
  95. package/install-manifests/python/package.json +1 -1
  96. package/install-manifests/pytypes/package-lock.json +45 -0
  97. package/install-manifests/pytypes/package.json +8 -0
  98. package/package.json +21 -19
  99. package/pr-checkmate.schema.json +41 -13
  100. package/src/config/cspell.json +470 -447
  101. package/src/config/pr-checkmate.schema.json +41 -13
  102. package/dist/core/checks/languages/csharp-format.d.ts +0 -13
  103. package/dist/core/checks/languages/csharp-format.js +0 -52
  104. package/dist/core/checks/languages/go-lint.d.ts +0 -8
  105. package/dist/core/checks/languages/go-lint.js +0 -48
  106. package/dist/core/checks/languages/kotlin-lint.d.ts +0 -2
  107. package/dist/core/checks/languages/kotlin-lint.js +0 -42
  108. package/dist/core/checks/languages/php-format.d.ts +0 -9
  109. package/dist/core/checks/languages/php-format.js +0 -49
  110. package/dist/core/checks/languages/ruby-lint.d.ts +0 -8
  111. package/dist/core/checks/languages/ruby-lint.js +0 -50
  112. package/dist/core/checks/languages/rust-format.d.ts +0 -9
  113. package/dist/core/checks/languages/rust-format.js +0 -45
  114. package/dist/core/checks/languages/rust-lint.d.ts +0 -8
  115. package/dist/core/checks/languages/rust-lint.js +0 -50
  116. package/dist/core/checks/languages/shellcheck.d.ts +0 -8
  117. package/dist/core/checks/languages/shellcheck.js +0 -55
  118. package/dist/core/checks/languages/swift-lint.d.ts +0 -2
  119. package/dist/core/checks/languages/swift-lint.js +0 -46
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runTool = runTool;
4
+ exports.defineExternalLint = defineExternalLint;
5
+ exports.defineExternalFormat = defineExternalFormat;
6
+ const utils_1 = require("../../../utils");
7
+ const execa_1 = require("../../../utils/execa");
8
+ const targets_1 = require("../../targets");
9
+ const types_1 = require("../../types");
10
+ const findings_1 = require("../findings");
11
+ const tool_install_1 = require("../tool-install");
12
+ /**
13
+ * Run an external tool, telling an absent binary apart from a failing one.
14
+ *
15
+ * execa v9 with `reject: false` does NOT throw on a missing binary — it resolves
16
+ * with `code: 'ENOENT'`. Detecting that on the result rather than in a
17
+ * `try`/`catch` (which never fires here) is the difference between an honest skip
18
+ * and a missing linter reporting a clean pass. This comment used to be repeated
19
+ * verbatim in eleven files, which is roughly how long it takes for one of the
20
+ * copies to lose it.
21
+ */
22
+ async function runTool(ctx, bin, args) {
23
+ const result = await (0, execa_1.execa)(bin, args, { cwd: ctx.cwd, reject: false });
24
+ if (result.code === 'ENOENT')
25
+ return { missing: true };
26
+ return {
27
+ missing: false,
28
+ exitCode: result.exitCode,
29
+ stdout: result.stdout,
30
+ stderr: result.stderr,
31
+ };
32
+ }
33
+ /** Both streams as one blob — some tools diagnose on stdout, some on stderr. */
34
+ function combinedOutput(run, stderrFirst) {
35
+ const streams = stderrFirst ? [run.stderr, run.stdout] : [run.stdout, run.stderr];
36
+ return streams.filter(Boolean).join('\n').trim();
37
+ }
38
+ /** Every non-blank line is a finding, unless a spec says otherwise. */
39
+ function isNotBlank(line) {
40
+ return line.trim().length > 0;
41
+ }
42
+ /** `N issue(s)`, unless a spec phrases its count differently. */
43
+ function countIssues(lines) {
44
+ return `${lines.length} issue(s)`;
45
+ }
46
+ /** Whether a language check is switched on for this repository. */
47
+ function isEnabled(ctx, key, language) {
48
+ if (ctx.config[key]?.enabled === false)
49
+ return false;
50
+ return language === undefined || ctx.languages.includes(language);
51
+ }
52
+ /**
53
+ * Assemble the `Check` both factories return, given the part that differs.
54
+ *
55
+ * Everything here is identical between a linter and a formatter — the name, the
56
+ * `applies` gate, and resolving the files — and only `phase` and `work` are not.
57
+ * Kept as a third function rather than merged into one factory with a flag,
58
+ * because the two specs are genuinely different shapes: a linter reads output, a
59
+ * formatter runs twice and may write. One factory would mean a spec with half its
60
+ * fields unused, which is the trade this file's header argues against.
61
+ */
62
+ function defineExternal(spec, phase, work) {
63
+ return {
64
+ name: spec.name,
65
+ phase,
66
+ applies(ctx) {
67
+ return isEnabled(ctx, spec.configKey, spec.language);
68
+ },
69
+ async run(ctx) {
70
+ const targets = await (0, targets_1.resolveTargets)(ctx, spec.globs, {
71
+ onEmpty: { pass: `no ${spec.subject} changed` },
72
+ });
73
+ if (!Array.isArray(targets))
74
+ return targets;
75
+ return work(ctx, targets);
76
+ },
77
+ };
78
+ }
79
+ /**
80
+ * Build an informational lint check from its spec.
81
+ *
82
+ * Informational for all of them, and not a field: these checks depend on a tool
83
+ * we do not ship, so on a runner without it they cannot distinguish clean code
84
+ * from unexamined code. A gate that silently stops gating is worse than no gate,
85
+ * and a client who wants one promotes the check with `severity`.
86
+ */
87
+ function defineExternalLint(spec) {
88
+ return defineExternal(spec, 'informational', async (ctx, targets) => {
89
+ const run = await runTool(ctx, spec.bin, spec.args(ctx, targets));
90
+ if (run.missing)
91
+ return (0, tool_install_1.notInstalled)(spec.tool, spec.toolLabel);
92
+ const output = combinedOutput(run, spec.stderrFirst);
93
+ // Exit 0 is the tool's verdict. Empty output on a non-zero exit is a tool
94
+ // that objected without saying to what, and there is nothing to put in a
95
+ // report either way — so both are a pass rather than a warn reading
96
+ // "0 issue(s)".
97
+ if (run.exitCode === 0 || !output) {
98
+ utils_1.logger.info(`[${spec.name}]: ✅ No issues found`);
99
+ return (0, types_1.pass)();
100
+ }
101
+ const lines = output.split('\n').filter(spec.keep ?? isNotBlank);
102
+ // The tool objected and none of its output matched what this check knows how
103
+ // to read — a build error ahead of the lint pass, usually. Naming the exit
104
+ // code is thin, but a clean pass over an objection is a lie.
105
+ const summary = lines.length > 0
106
+ ? (spec.summary ?? countIssues)(lines, targets)
107
+ : `${spec.bin} exited ${run.exitCode ?? 'without a code'} with no readable diagnostics`;
108
+ (0, findings_1.logSample)(spec.name, `${summary}:`, lines);
109
+ return (0, types_1.warn)(summary);
110
+ });
111
+ }
112
+ /**
113
+ * Build a format check from its spec: detect, then rewrite only when asked.
114
+ *
115
+ * The two passes are deliberate. A check run must never change the tree unless
116
+ * the caller said `write`, and in `write` mode CI needs the files actually
117
+ * rewritten so it can commit them — so the read-only pass is the one that decides
118
+ * the verdict, and the second only carries it out.
119
+ */
120
+ function defineExternalFormat(spec) {
121
+ return defineExternal(spec, 'format', async (ctx, targets) => {
122
+ const dry = await runTool(ctx, spec.bin, spec.checkArgs(targets));
123
+ if (dry.missing)
124
+ return (0, tool_install_1.notInstalled)(spec.tool, spec.toolLabel);
125
+ if (dry.exitCode === 0)
126
+ return (0, types_1.pass)();
127
+ if (!ctx.write) {
128
+ utils_1.logger.warn(`[${spec.name}]: ⚠️ ${spec.code} needs formatting`);
129
+ return (0, types_1.warn)(`${spec.code} needs formatting (run with write to fix)`);
130
+ }
131
+ const fix = await runTool(ctx, spec.bin, spec.writeArgs(targets));
132
+ // `missing` cannot happen here — the same binary answered a moment ago — but
133
+ // it is a case of the type, and collapsing it into the failure branch is both
134
+ // correct and shorter than asserting it away.
135
+ if (fix.missing || fix.exitCode !== 0) {
136
+ const detail = fix.missing ? '' : `\n${fix.stderr}`;
137
+ utils_1.logger.warn(`[${spec.name}]: ⚠️ ${spec.bin} failed to rewrite files${detail}`);
138
+ return (0, types_1.warn)(`${spec.bin} failed to rewrite files`);
139
+ }
140
+ utils_1.logger.info(`[${spec.name}]: ✅ Reformatted ${spec.code}`);
141
+ return (0, types_1.pass)(`reformatted ${spec.code}`);
142
+ });
143
+ }
@@ -1,8 +1,8 @@
1
1
  import { Check } from '../../types';
2
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.
3
+ * Formats changed Go files with gofmt — the runner's own when it has one, and a
4
+ * bundled WASM build otherwise, so the check has no runner dependency at all.
5
+ * Read-only in check mode; rewrites in place when `write`, so CI can auto-commit
6
+ * — mirrors the Prettier/C++/Ruff format checks.
7
7
  */
8
8
  export declare const goFormatCheck: Check;
@@ -1,16 +1,146 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.goFormatCheck = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_url_1 = require("node:url");
4
10
  const utils_1 = require("../../../utils");
5
- const execa_1 = require("../../../utils/execa");
6
11
  const targets_1 = require("../../targets");
7
12
  const types_1 = require("../../types");
13
+ const tool_install_1 = require("../tool-install");
14
+ const external_runner_1 = require("./external-runner");
8
15
  const GO_GLOBS = ['*.go'];
9
16
  /**
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.
17
+ * Load the bundled WASM gofmt, or explain why it could not be loaded.
18
+ *
19
+ * This is what stopped `⏭️ Go Format: gofmt not installed` from being the answer
20
+ * on every runner without a Go toolchain — a ~285 KB WASM build of gofmt does
21
+ * the same job with nothing to install, the same way Ruff covers Python and
22
+ * clang-format covers C++.
23
+ *
24
+ * Three details, each of which was a way to get this wrong:
25
+ *
26
+ * - **Resolved through `resolvePackageBin`, not a bare specifier.** The package
27
+ * may live in a fetched profile directory rather than in our own tree, and only
28
+ * the resolver knows about those. A bare `import('@wasm-fmt/gofmt')` resolves
29
+ * relative to this file and would miss it.
30
+ * - **`gofmt_node.js` explicitly**, because the package's `exports` map routes
31
+ * `.` per runtime and the node entry is the one that reads the `.wasm` off disk.
32
+ * - **A dynamic `import()`.** The package is ESM and we emit CommonJS;
33
+ * `require` cannot load it, and TypeScript preserves dynamic `import()` in CJS
34
+ * output under `module: NodeNext` precisely so this works.
35
+ */
36
+ async function loadBundledGofmt() {
37
+ try {
38
+ const entry = (0, utils_1.resolvePackageBin)('@wasm-fmt/gofmt', 'gofmt_node.js');
39
+ const mod = (await import((0, node_url_1.pathToFileURL)(entry).href));
40
+ return mod.format;
41
+ }
42
+ catch (err) {
43
+ utils_1.logger.warn(`[Go Format]: ⚠️ bundled gofmt could not be loaded\n${err}`);
44
+ return null;
45
+ }
46
+ }
47
+ /**
48
+ * The verdict, given the files that turned out to need formatting.
49
+ *
50
+ * Shared by both routes on purpose. It was written out twice — once after
51
+ * `gofmt -l` and once after the in-memory comparison — and the two copies are how
52
+ * the wording of a verdict comes to depend on whether Go happens to be installed
53
+ * on the runner (ticket 079). What a client is told about their code must not be a
54
+ * function of our own resolution order.
55
+ *
56
+ * Called only once the rewriting, if any, is done — so reaching the last branch
57
+ * means the files really were rewritten and CI has something to commit. The
58
+ * routes differ in when that happens (the system route shells out to `gofmt -w`,
59
+ * the bundled one wrote each file as it went) and in nothing this function can
60
+ * see.
61
+ */
62
+ function verdictFor(ctx, unformatted) {
63
+ if (unformatted.length === 0)
64
+ return (0, types_1.pass)();
65
+ if (!ctx.write) {
66
+ utils_1.logger.warn(`[Go Format]: ⚠️ ${unformatted.length} Go file(s) need formatting`);
67
+ unformatted.slice(0, 10).forEach(file => utils_1.logger.warn(` ${file}`));
68
+ return (0, types_1.warn)(`${unformatted.length} file(s) need formatting (run with write to fix)`);
69
+ }
70
+ utils_1.logger.info(`[Go Format]: ✅ Reformatted ${unformatted.length} Go file(s)`);
71
+ return (0, types_1.pass)(`reformatted ${unformatted.length} file(s)`);
72
+ }
73
+ /**
74
+ * Format with the runner's own `gofmt`, when it has one.
75
+ *
76
+ * Tried ahead of the bundled copy on purpose, and it is the same policy as
77
+ * preferring a repo's `.clang-format` or its own ESLint: gofmt's output is a
78
+ * function of the Go release, and the one release we know a repository agrees
79
+ * with is the one installed beside it. Reaching for ours first would mean
80
+ * rewriting a Go 1.18 tree by current rules in `write` mode — us and their CI
81
+ * reformatting the same files in opposite directions.
82
+ *
83
+ * @returns The outcome, or `null` when there is no system `gofmt` to ask.
84
+ */
85
+ async function formatWithSystemGofmt(ctx, targets) {
86
+ const list = await (0, external_runner_1.runTool)(ctx, 'gofmt', ['-l', ...targets]);
87
+ if (list.missing)
88
+ return null;
89
+ const unformatted = list.stdout.split('\n').filter(Boolean);
90
+ // Decided before the rewrite, so a read-only run answers without `gofmt -w`
91
+ // having been considered at all.
92
+ if (unformatted.length === 0 || !ctx.write)
93
+ return verdictFor(ctx, unformatted);
94
+ const fix = await (0, external_runner_1.runTool)(ctx, 'gofmt', ['-w', ...unformatted]);
95
+ if (fix.missing || fix.exitCode !== 0) {
96
+ utils_1.logger.warn(`[Go Format]: ⚠️ gofmt -w failed\n${fix.missing ? '' : fix.stderr}`);
97
+ return (0, types_1.warn)('gofmt failed to rewrite files');
98
+ }
99
+ return verdictFor(ctx, unformatted);
100
+ }
101
+ /**
102
+ * Format in-process with the bundled WASM gofmt — the route taken on a runner
103
+ * with no Go toolchain, which is most of them.
104
+ *
105
+ * Compares rather than trusting an exit code, because the WASM build is a
106
+ * library and not a CLI: it formats a string and says nothing about whether it
107
+ * changed anything. Mirrors the Ruff format check for that reason.
108
+ */
109
+ function formatWithBundledGofmt(ctx, targets, format) {
110
+ const unformatted = [];
111
+ for (const rel of targets) {
112
+ const abs = node_path_1.default.join(ctx.cwd, rel);
113
+ let content;
114
+ try {
115
+ content = node_fs_1.default.readFileSync(abs, 'utf-8');
116
+ }
117
+ catch {
118
+ continue; // renamed or removed between the diff and the read
119
+ }
120
+ let formatted;
121
+ try {
122
+ formatted = format(content);
123
+ }
124
+ catch (err) {
125
+ // gofmt parses before it formats, so a throw here is a syntax error. That
126
+ // is Go Vet's finding to report, not a formatting verdict — and calling it
127
+ // one sends people looking for a style problem that does not exist.
128
+ utils_1.logger.warn(`[Go Format]: ⚠️ could not parse ${rel} (syntax error?)\n${err}`);
129
+ continue;
130
+ }
131
+ if (formatted === content)
132
+ continue;
133
+ unformatted.push(rel);
134
+ if (ctx.write)
135
+ node_fs_1.default.writeFileSync(abs, formatted);
136
+ }
137
+ return verdictFor(ctx, unformatted);
138
+ }
139
+ /**
140
+ * Formats changed Go files with gofmt — the runner's own when it has one, and a
141
+ * bundled WASM build otherwise, so the check has no runner dependency at all.
142
+ * Read-only in check mode; rewrites in place when `write`, so CI can auto-commit
143
+ * — mirrors the Prettier/C++/Ruff format checks.
14
144
  */
15
145
  exports.goFormatCheck = {
16
146
  name: 'Go Format',
@@ -19,30 +149,21 @@ exports.goFormatCheck = {
19
149
  return ctx.config.go?.enabled !== false && ctx.languages.includes('go');
20
150
  },
21
151
  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)`);
152
+ const targets = await (0, targets_1.resolveTargets)(ctx, GO_GLOBS, {
153
+ onEmpty: { pass: 'no Go files changed' },
154
+ });
155
+ if (!Array.isArray(targets))
156
+ return targets;
157
+ const viaSystem = await formatWithSystemGofmt(ctx, targets);
158
+ if (viaSystem)
159
+ return viaSystem;
160
+ const format = await loadBundledGofmt();
161
+ // Neither route: no Go toolchain on the runner AND our own copy could not be
162
+ // loaded, which means a partial install of this package. Installing Go is
163
+ // the fix a client can actually act on.
164
+ if (!format)
165
+ return (0, tool_install_1.notInstalled)('gofmt', 'gofmt');
166
+ utils_1.logger.debug('[Go Format]: no system gofmt — using the bundled WASM formatter');
167
+ return formatWithBundledGofmt(ctx, targets, format);
47
168
  },
48
169
  };
@@ -4,20 +4,23 @@ exports.lintCheck = void 0;
4
4
  const config_1 = require("../../../config");
5
5
  const utils_1 = require("../../../utils");
6
6
  const execa_1 = require("../../../utils/execa");
7
+ const reuse_1 = require("../../install/reuse");
7
8
  const targets_1 = require("../../targets");
8
9
  const types_1 = require("../../types");
9
10
  /**
10
- * Resolve the ESLint binary, preferring the consumer project's own copy so a
11
- * repo pinned to ESLint 8 (+ legacy `.eslintrc.*`) keeps working. Falls back to
12
- * the version bundled with pr-checkmate when the project has no local install.
11
+ * Resolve the ESLint binary under the client's `install.reuseLocal` policy.
12
+ *
13
+ * A repo pinned to ESLint 8 with a legacy `.eslintrc.*` must run ITS ESLint —
14
+ * ours does not read eslintrc — so when the client owns the config the version is
15
+ * not asked about. When the config is the flat one WE generate, it is: ESLint 8
16
+ * cannot read it, and reusing their copy there produced a failure about our
17
+ * configuration rather than about their code. That asymmetry is the whole reason
18
+ * `clientOwnsConfig` exists (ticket 064, lever A).
13
19
  */
14
- function resolveESLintBin(cwd) {
15
- try {
16
- return (0, utils_1.resolvePackageBin)('eslint', 'bin/eslint.js', { paths: [cwd] });
17
- }
18
- catch {
19
- return (0, utils_1.resolvePackageBin)('eslint', 'bin/eslint.js');
20
- }
20
+ function resolveESLintBin(ctx, clientOwnsConfig) {
21
+ return (0, utils_1.resolvePackageBin)('eslint', 'bin/eslint.js', {
22
+ paths: (0, reuse_1.clientToolPaths)('eslint', ctx, { clientOwnsConfig }),
23
+ });
21
24
  }
22
25
  async function getESLintMajorVersion(eslintBin) {
23
26
  try {
@@ -36,19 +39,16 @@ exports.lintCheck = {
36
39
  return ctx.languages.includes('typescript') || ctx.languages.includes('javascript');
37
40
  },
38
41
  async run(ctx) {
39
- const eslintBin = resolveESLintBin(ctx.cwd);
40
- const targets = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx'], (0, config_1.getSourcePaths)());
42
+ const targets = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx'], (0, config_1.getSourcePaths)(ctx.cwd));
41
43
  if (targets === null)
42
44
  return (0, types_1.skip)('git unavailable');
43
45
  if (targets.length === 0)
44
46
  return (0, types_1.pass)('no TypeScript/JavaScript files to lint');
45
- const hasLocal = (0, config_1.hasLocalConfig)([
46
- 'eslint.config.mjs',
47
- 'eslint.config.js',
48
- '.eslintrc.js',
49
- '.eslintrc.json',
50
- '.eslintrc',
51
- ]);
47
+ // Whose config decides whose binary, so this is read BEFORE resolving one.
48
+ // It used to be the other way round, which is how a client's ESLint 8 ended
49
+ // up pointed at the flat config we generate.
50
+ const hasLocal = (0, config_1.hasLocalConfig)(['eslint.config.mjs', 'eslint.config.js', '.eslintrc.js', '.eslintrc.json', '.eslintrc'], ctx.cwd);
51
+ const eslintBin = resolveESLintBin(ctx, hasLocal);
52
52
  const version = await getESLintMajorVersion(eslintBin);
53
53
  const args = [...targets];
54
54
  // `--ext` is an eslintrc-only flag. Under flat config (ESLint 9+) it is
@@ -42,7 +42,7 @@ exports.prettierCheck = {
42
42
  return ctx.languages.includes('typescript') || ctx.languages.includes('javascript');
43
43
  },
44
44
  async run(ctx) {
45
- const targets = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx', '*.json', '*.md'], (0, config_1.getSourcePaths)());
45
+ const targets = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx', '*.json', '*.md'], (0, config_1.getSourcePaths)(ctx.cwd));
46
46
  if (targets === null)
47
47
  return (0, types_1.skip)('git unavailable');
48
48
  if (targets.length === 0)
@@ -57,12 +57,7 @@ exports.prettierCheck = {
57
57
  ignorePath = gitignore;
58
58
  else
59
59
  ignorePath = tempIgnore = createTempIgnoreFile(ctx.cwd);
60
- const hasLocal = (0, config_1.hasLocalConfig)([
61
- '.prettierrc',
62
- '.prettierrc.json',
63
- '.prettierrc.js',
64
- 'prettier.config.js',
65
- ]);
60
+ const hasLocal = (0, config_1.hasLocalConfig)(['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js'], ctx.cwd);
66
61
  // write=true → format in place; write=false → report-only (`--check`, never mutates files).
67
62
  const mode = ctx.write ? '--write' : '--check';
68
63
  let tempConfig = null;
@@ -1,9 +1,14 @@
1
1
  import { Check } from '../../types';
2
2
  /**
3
- * Type-checks changed Python files — the Python analogue of the TypeScript
4
- * `tsc --noEmit` check. Runner-dependency (needs mypy or pyright on the runner),
5
- * gracefully skipping when neither is present. Informational by design (like
6
- * Ruff lint) so teams can adopt gradually; force a checker with
7
- * `python.typeChecker`, else mypy is tried first, then pyright.
3
+ * Type-checks changed Python files — the Python analogue of `tsc --noEmit`.
4
+ *
5
+ * Two checkers, and the order is the policy. **mypy first**, because a repository
6
+ * that installed mypy configured it too (`mypy.ini`, `[tool.mypy]`), and its
7
+ * settled opinion about its own code beats ours. **Our bundled pyright
8
+ * otherwise**, which is what stopped `⏭️ mypy/pyright not installed` from being
9
+ * the answer on every runner: pyright is a Node bundle carrying its own typeshed,
10
+ * so it needs neither Python nor pip. Force either with `python.typeChecker`.
11
+ *
12
+ * Informational by design, like Ruff lint, so teams can adopt gradually.
8
13
  */
9
14
  export declare const pythonTypecheckCheck: Check;