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.
- package/README.md +63 -268
- package/cspell.json +470 -447
- package/dist/cli.js +1 -1
- package/dist/config/constants.d.ts +6 -1
- package/dist/config/constants.js +9 -7
- package/dist/config/index.d.ts +23 -11
- package/dist/config/index.js +27 -33
- package/dist/core/checks/dependencies/banned-imports.js +4 -1
- package/dist/core/checks/dependencies/circular-deps.js +14 -5
- package/dist/core/checks/dependencies/dependency-check.js +1 -1
- package/dist/core/checks/dependencies/grype-scan.js +2 -1
- package/dist/core/checks/dependencies/vuln-scan.js +2 -1
- package/dist/core/checks/findings.d.ts +46 -2
- package/dist/core/checks/findings.js +140 -2
- package/dist/core/checks/git/custom-rules.js +2 -0
- package/dist/core/checks/git/diff-smell.js +6 -1
- package/dist/core/checks/git/leftover-debug.js +8 -2
- package/dist/core/checks/git/lockfile-drift.js +6 -1
- package/dist/core/checks/git/merge-conflict.js +1 -1
- package/dist/core/checks/git/missing-tests.js +1 -1
- package/dist/core/checks/git/todo-fixme.js +10 -2
- package/dist/core/checks/languages/cpp-format.js +19 -42
- package/dist/core/checks/languages/external-checks.d.ts +77 -0
- package/dist/core/checks/languages/external-checks.js +200 -0
- package/dist/core/checks/languages/external-runner.d.ts +119 -0
- package/dist/core/checks/languages/external-runner.js +143 -0
- package/dist/core/checks/languages/go-format.d.ts +4 -4
- package/dist/core/checks/languages/go-format.js +151 -30
- package/dist/core/checks/languages/lint.js +19 -19
- package/dist/core/checks/languages/prettier.js +2 -7
- package/dist/core/checks/languages/python-typecheck.d.ts +10 -5
- package/dist/core/checks/languages/python-typecheck.js +161 -37
- package/dist/core/checks/languages/ruff-shared.d.ts +3 -3
- package/dist/core/checks/languages/ruff-shared.js +27 -4
- package/dist/core/checks/languages/typecheck.js +10 -8
- package/dist/core/checks/pr/commitlint.js +7 -1
- package/dist/core/checks/quality/case-collision.js +6 -4
- package/dist/core/checks/quality/config-validation.d.ts +0 -7
- package/dist/core/checks/quality/config-validation.js +30 -0
- package/dist/core/checks/quality/coverage.js +6 -1
- package/dist/core/checks/quality/dead-code.js +12 -3
- package/dist/core/checks/quality/duplicate-check.js +54 -27
- package/dist/core/checks/quality/license-header.js +7 -4
- package/dist/core/checks/quality/markdown-lint.js +1 -6
- package/dist/core/checks/quality/package-size.d.ts +15 -0
- package/dist/core/checks/quality/package-size.js +130 -0
- package/dist/core/checks/quality/spellcheck.js +34 -5
- package/dist/core/checks/quality/symlinks.js +6 -4
- package/dist/core/checks/security/diff-security.js +26 -18
- package/dist/core/checks/security/dockerfile-security.js +7 -1
- package/dist/core/checks/security/migration-safety.js +32 -4
- package/dist/core/checks/security/security.d.ts +56 -0
- package/dist/core/checks/security/security.js +135 -11
- package/dist/core/checks/security/sensitive-file-guard.js +40 -6
- package/dist/core/checks/security/workflow-security.js +7 -1
- package/dist/core/checks/tool-install.d.ts +102 -0
- package/dist/core/checks/tool-install.js +69 -0
- package/dist/core/commands.js +1 -0
- package/dist/core/context.js +9 -1
- package/dist/core/install/cache.d.ts +20 -1
- package/dist/core/install/cache.js +65 -12
- package/dist/core/install/doctor.js +49 -15
- package/dist/core/install/ensure.d.ts +31 -14
- package/dist/core/install/ensure.js +165 -61
- package/dist/core/install/lock.d.ts +2 -1
- package/dist/core/install/merge.d.ts +47 -0
- package/dist/core/install/merge.js +91 -0
- package/dist/core/install/prepare.d.ts +76 -0
- package/dist/core/install/prepare.js +147 -0
- package/dist/core/install/profiles.d.ts +17 -3
- package/dist/core/install/profiles.js +27 -0
- package/dist/core/install/reuse.d.ts +41 -0
- package/dist/core/install/reuse.js +107 -0
- package/dist/core/registry.js +12 -18
- package/dist/core/run-checks.js +74 -3
- package/dist/core/targets.d.ts +116 -5
- package/dist/core/targets.js +101 -16
- package/dist/core/types.d.ts +8 -2
- package/dist/init.js +21 -1
- package/dist/interfaces/index.d.ts +6 -0
- package/dist/utils/resolve-bin.d.ts +56 -6
- package/dist/utils/resolve-bin.js +136 -26
- package/gitleaks.toml +14 -0
- package/install-manifests/commitlint/package-lock.json +128 -250
- package/install-manifests/commitlint/package.json +2 -2
- package/install-manifests/core/package-lock.json +76 -76
- package/install-manifests/core/package.json +1 -1
- package/install-manifests/duplicate/package-lock.json +45 -45
- package/install-manifests/duplicate/package.json +1 -1
- package/install-manifests/go/package-lock.json +24 -0
- package/install-manifests/go/package.json +8 -0
- package/install-manifests/js/package-lock.json +248 -283
- package/install-manifests/js/package.json +9 -9
- package/install-manifests/python/package-lock.json +4 -4
- package/install-manifests/python/package.json +1 -1
- package/install-manifests/pytypes/package-lock.json +45 -0
- package/install-manifests/pytypes/package.json +8 -0
- package/package.json +21 -19
- package/pr-checkmate.schema.json +41 -13
- package/src/config/cspell.json +470 -447
- package/src/config/pr-checkmate.schema.json +41 -13
- package/dist/core/checks/languages/csharp-format.d.ts +0 -13
- package/dist/core/checks/languages/csharp-format.js +0 -52
- package/dist/core/checks/languages/go-lint.d.ts +0 -8
- package/dist/core/checks/languages/go-lint.js +0 -48
- package/dist/core/checks/languages/kotlin-lint.d.ts +0 -2
- package/dist/core/checks/languages/kotlin-lint.js +0 -42
- package/dist/core/checks/languages/php-format.d.ts +0 -9
- package/dist/core/checks/languages/php-format.js +0 -49
- package/dist/core/checks/languages/ruby-lint.d.ts +0 -8
- package/dist/core/checks/languages/ruby-lint.js +0 -50
- package/dist/core/checks/languages/rust-format.d.ts +0 -9
- package/dist/core/checks/languages/rust-format.js +0 -45
- package/dist/core/checks/languages/rust-lint.d.ts +0 -8
- package/dist/core/checks/languages/rust-lint.js +0 -50
- package/dist/core/checks/languages/shellcheck.d.ts +0 -8
- package/dist/core/checks/languages/shellcheck.js +0 -55
- package/dist/core/checks/languages/swift-lint.d.ts +0 -2
- package/dist/core/checks/languages/swift-lint.js +0 -46
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.profileSatisfiedLocally = profileSatisfiedLocally;
|
|
4
|
+
exports.prepareTooling = prepareTooling;
|
|
5
|
+
exports.noTooling = noTooling;
|
|
6
|
+
const logger_1 = require("../../utils/logger");
|
|
7
|
+
const resolve_bin_1 = require("../../utils/resolve-bin");
|
|
8
|
+
const ensure_1 = require("./ensure");
|
|
9
|
+
const profiles_1 = require("./profiles");
|
|
10
|
+
/**
|
|
11
|
+
* Whether a profile needs fetching at all, or already ships inside the package.
|
|
12
|
+
*
|
|
13
|
+
* This is the hinge the whole adaptive-install rollout turns on. Nearly every
|
|
14
|
+
* profile's packages sit in our `dependencies`, so it answers `true`, nothing is
|
|
15
|
+
* fetched, and a run is byte-for-byte the run it was before any of this existed.
|
|
16
|
+
* `pytypes` is the exception that already proves the other half works: `pyright`
|
|
17
|
+
* is a devDependency, so it reaches no client and is fetched on demand. When a
|
|
18
|
+
* package is moved out of `dependencies` it stops being present, its profile
|
|
19
|
+
* starts answering `false`, and the fetch turns itself on — no flag, no second
|
|
20
|
+
* code path, nothing to remember to flip.
|
|
21
|
+
*
|
|
22
|
+
* Which is what makes releases 2 and 3 of ticket 064 an edit to `package.json`
|
|
23
|
+
* rather than another change to this logic.
|
|
24
|
+
*/
|
|
25
|
+
function profileSatisfiedLocally(profile) {
|
|
26
|
+
return profile.packages.every(pkg => (0, resolve_bin_1.packagePresent)(pkg));
|
|
27
|
+
}
|
|
28
|
+
/** Profiles blocked outright by `install.deny`, with a line saying so. */
|
|
29
|
+
function denied(profile, deny) {
|
|
30
|
+
const blocked = profile.packages.filter(pkg => deny.has(pkg));
|
|
31
|
+
if (blocked.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
return `install.deny lists ${blocked.join(', ')}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Make sure the tooling this run needs is present, and say what is missing.
|
|
37
|
+
*
|
|
38
|
+
* Runs once, before the first check, and answers two questions the run cannot
|
|
39
|
+
* proceed honestly without: which directories tool resolution should search, and
|
|
40
|
+
* which checks have no tool to run. The second is the one ticket 057 is about —
|
|
41
|
+
* a check whose tooling never arrived must appear in the report saying so, not
|
|
42
|
+
* vanish from it.
|
|
43
|
+
*
|
|
44
|
+
* Only profiles backing checks that are actually selected are considered, so
|
|
45
|
+
* `severity: { "Spellcheck": "off" }` stops costing a download rather than only a
|
|
46
|
+
* row in the report (lever B of ticket 064).
|
|
47
|
+
*/
|
|
48
|
+
async function prepareTooling(opts) {
|
|
49
|
+
// Every run starts from no registered roots. The SDK can call `runChecks` more
|
|
50
|
+
// than once in one process, and a directory registered by a previous run — for
|
|
51
|
+
// a different repository, with a different profile set — must not still be on
|
|
52
|
+
// the search path for this one.
|
|
53
|
+
(0, resolve_bin_1.clearToolPaths)();
|
|
54
|
+
const install = opts.config.install ?? {};
|
|
55
|
+
const strategy = process.env.PR_CHECKMATE_INSTALL_STRATEGY ?? install.strategy ?? 'adaptive';
|
|
56
|
+
const onMissing = install.onMissingTool ?? 'auto';
|
|
57
|
+
const deny = new Set(install.deny ?? []);
|
|
58
|
+
// Which profiles this repository needs: languages by file count, universal
|
|
59
|
+
// profiles by whether their check survived config. `install.profiles` and
|
|
60
|
+
// `excludeProfiles` have already had their say inside `planProfiles`.
|
|
61
|
+
const required = new Set((0, profiles_1.planProfiles)(opts.usage, opts.config)
|
|
62
|
+
.filter(decision => decision.required)
|
|
63
|
+
.map(decision => decision.profile));
|
|
64
|
+
// A profile nobody in this run would use is not worth a thought, let alone a
|
|
65
|
+
// download. `eager` opts out of that reasoning by definition: it exists so a
|
|
66
|
+
// container build or an air-gapped mirror can hold everything up front.
|
|
67
|
+
const chosen = opts.selected.length > 0 ? new Set(opts.selected) : null;
|
|
68
|
+
const wanted = (opts.profiles ?? profiles_1.PROFILES).filter(profile => {
|
|
69
|
+
if (strategy === 'eager')
|
|
70
|
+
return true;
|
|
71
|
+
if (!required.has(profile.name))
|
|
72
|
+
return false;
|
|
73
|
+
return !chosen || profile.checks.some(check => chosen.has(check));
|
|
74
|
+
});
|
|
75
|
+
const unavailable = new Map();
|
|
76
|
+
const dirs = [];
|
|
77
|
+
const miss = (profile, reason) => {
|
|
78
|
+
for (const check of profile.checks)
|
|
79
|
+
unavailable.set(check, reason);
|
|
80
|
+
};
|
|
81
|
+
const toFetch = [];
|
|
82
|
+
for (const profile of wanted) {
|
|
83
|
+
// Bundled tooling is the answer to "where is it" for most profiles, and
|
|
84
|
+
// asking first is what keeps this whole path free of network calls.
|
|
85
|
+
if (profileSatisfiedLocally(profile))
|
|
86
|
+
continue;
|
|
87
|
+
const block = denied(profile, deny);
|
|
88
|
+
if (block) {
|
|
89
|
+
miss(profile, block);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// No early exit for `install.strategy: "manual"`. It means "never fetch",
|
|
93
|
+
// and `ensureTooling` enforces that — but only after checking the cache, so
|
|
94
|
+
// pre-warmed tooling is still used. Returning here instead meant `manual`
|
|
95
|
+
// also refused tooling that was already downloaded.
|
|
96
|
+
if (onMissing === 'skip') {
|
|
97
|
+
miss(profile, 'install.onMissingTool is "skip"');
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
toFetch.push(profile);
|
|
101
|
+
}
|
|
102
|
+
// One call, one directory, one lock — for the whole run rather than per profile.
|
|
103
|
+
// Profiles overlap heavily, and a tree each made the client download the shared
|
|
104
|
+
// half once per profile that named it (ticket 087).
|
|
105
|
+
//
|
|
106
|
+
// Reached only when something actually needs fetching, which is also what keeps
|
|
107
|
+
// the cache directory untouched otherwise: resolving it is not a read — it
|
|
108
|
+
// creates the directory and warns when the configured path is unwritable, and a
|
|
109
|
+
// container with a read-only home would print that on every single run.
|
|
110
|
+
if (toFetch.length > 0) {
|
|
111
|
+
const result = await (0, ensure_1.ensureTooling)(toFetch.map(profile => profile.name), {
|
|
112
|
+
cwd: opts.cwd,
|
|
113
|
+
version: opts.version,
|
|
114
|
+
install,
|
|
115
|
+
...(opts.installer ? { installer: opts.installer } : {}),
|
|
116
|
+
});
|
|
117
|
+
if (result.status === 'failed' || result.status === 'skipped') {
|
|
118
|
+
for (const profile of toFetch)
|
|
119
|
+
miss(profile, result.reason);
|
|
120
|
+
}
|
|
121
|
+
else if (result.dir) {
|
|
122
|
+
dirs.push(result.dir);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (unavailable.size > 0) {
|
|
126
|
+
// Always logged, never behind --debug. A check that did not run is the
|
|
127
|
+
// failure mode this whole layer exists to make loud.
|
|
128
|
+
const names = [...new Set(unavailable.keys())].join(', ');
|
|
129
|
+
logger_1.logger.warn(`[install]: ⚠️ tooling unavailable — these checks cannot run: ${names}`);
|
|
130
|
+
if (onMissing === 'error') {
|
|
131
|
+
logger_1.logger.error('[install]: ❌ install.onMissingTool is "error" — the affected checks fail');
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
logger_1.logger.info('[install]: Pre-warm the cache with: npx pr-checkmate install --all');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
(0, resolve_bin_1.registerToolPaths)(dirs);
|
|
138
|
+
return { unavailable, dirs, fatal: onMissing === 'error' && unavailable.size > 0 };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The plan for a run where preparation did not happen — nothing missing, nothing
|
|
142
|
+
* registered. Built fresh each time rather than shared: a caller holding a plan
|
|
143
|
+
* must not be able to see another run's.
|
|
144
|
+
*/
|
|
145
|
+
function noTooling() {
|
|
146
|
+
return { unavailable: new Map(), dirs: [], fatal: false };
|
|
147
|
+
}
|
|
@@ -12,7 +12,7 @@ import type { Language, LanguageUsage } from '../../utils/detect-language';
|
|
|
12
12
|
* honest report: when a profile is missing, exactly those checks skip, with the
|
|
13
13
|
* reason, instead of quietly disappearing (the mistake of ticket 057).
|
|
14
14
|
*/
|
|
15
|
-
interface Profile {
|
|
15
|
+
export interface Profile {
|
|
16
16
|
name: ProfileName;
|
|
17
17
|
/** Top-level packages installed for this profile. */
|
|
18
18
|
packages: string[];
|
|
@@ -23,8 +23,23 @@ interface Profile {
|
|
|
23
23
|
* enabled checks rather than by what the repository is written in.
|
|
24
24
|
*/
|
|
25
25
|
languages?: Language[];
|
|
26
|
+
/**
|
|
27
|
+
* An extra condition on the client's config, for a language profile whose
|
|
28
|
+
* checks can be switched off by a key of their own.
|
|
29
|
+
*
|
|
30
|
+
* `severity: { "X": "off" }` is already honoured for every profile, but it is
|
|
31
|
+
* not the only off switch: `python.typecheck: false` and `go.enabled: false`
|
|
32
|
+
* are the ones a client actually writes, and a language profile decided on
|
|
33
|
+
* file counts alone downloads the tooling regardless. For `pytypes` that is
|
|
34
|
+
* 19 MB fetched to run nothing.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately a predicate over the config rather than a list of keys: the
|
|
37
|
+
* condition it mirrors lives in the check's own `applies`, and expressing it
|
|
38
|
+
* the same way is what makes the two readable side by side.
|
|
39
|
+
*/
|
|
40
|
+
enabledBy?(config: PRCheckMateConfig): boolean;
|
|
26
41
|
}
|
|
27
|
-
export type ProfileName = 'core' | 'js' | 'python' | 'cpp' | 'spellcheck' | 'markdown' | 'commitlint' | 'duplicate';
|
|
42
|
+
export type ProfileName = 'core' | 'js' | 'python' | 'pytypes' | 'cpp' | 'go' | 'spellcheck' | 'markdown' | 'commitlint' | 'duplicate';
|
|
28
43
|
/**
|
|
29
44
|
* The catalogue.
|
|
30
45
|
*
|
|
@@ -61,4 +76,3 @@ export interface ProfileDecision {
|
|
|
61
76
|
* @returns One decision per profile, in catalogue order.
|
|
62
77
|
*/
|
|
63
78
|
export declare function planProfiles(usage: LanguageUsage[], config: PRCheckMateConfig): ProfileDecision[];
|
|
64
|
-
export {};
|
|
@@ -54,12 +54,33 @@ exports.PROFILES = [
|
|
|
54
54
|
checks: ['Ruff Lint', 'Ruff Format'],
|
|
55
55
|
languages: ['python'],
|
|
56
56
|
},
|
|
57
|
+
{
|
|
58
|
+
// Separate from `python` because it is 19 MB against ruff's 12, and because
|
|
59
|
+
// the two answer different questions: plenty of repositories want the linter
|
|
60
|
+
// and have their own opinion about type checking. Bundling them would make
|
|
61
|
+
// `python.typecheck: false` cost the download anyway, which is the feature
|
|
62
|
+
// defeating itself.
|
|
63
|
+
name: 'pytypes',
|
|
64
|
+
packages: ['pyright'],
|
|
65
|
+
checks: ['Python Types'],
|
|
66
|
+
languages: ['python'],
|
|
67
|
+
enabledBy: config => config.python?.typecheck !== false && config.python?.enabled !== false,
|
|
68
|
+
},
|
|
57
69
|
{
|
|
58
70
|
name: 'cpp',
|
|
59
71
|
packages: ['@wasm-fmt/clang-format'],
|
|
60
72
|
checks: ['C++ Format'],
|
|
61
73
|
languages: ['cpp'],
|
|
62
74
|
},
|
|
75
|
+
{
|
|
76
|
+
// Go Format only. `go vet` needs the toolchain itself, which is not ours to
|
|
77
|
+
// ship, so it stays a runner dependency and is deliberately absent here.
|
|
78
|
+
name: 'go',
|
|
79
|
+
packages: ['@wasm-fmt/gofmt'],
|
|
80
|
+
checks: ['Go Format'],
|
|
81
|
+
languages: ['go'],
|
|
82
|
+
enabledBy: config => config.go?.enabled !== false,
|
|
83
|
+
},
|
|
63
84
|
{
|
|
64
85
|
// The four below are not languages. They are single checks with heavy
|
|
65
86
|
// tooling behind them, which is exactly why they are separate: switching
|
|
@@ -145,6 +166,12 @@ function decide(profile, usage, config, minFiles) {
|
|
|
145
166
|
if (profile.name === 'core')
|
|
146
167
|
return { required: true, reason: 'always installed' };
|
|
147
168
|
if (profile.languages) {
|
|
169
|
+
// Asked before the file counts, because it is the cheaper and the more
|
|
170
|
+
// definite answer: a client who wrote `python.typecheck: false` has settled
|
|
171
|
+
// the question, and counting `.py` files can only override them.
|
|
172
|
+
if (profile.enabledBy && !profile.enabledBy(config)) {
|
|
173
|
+
return { required: false, reason: `${profile.checks.join(', ')} switched off` };
|
|
174
|
+
}
|
|
148
175
|
const found = profile.languages
|
|
149
176
|
.map(lang => usage.find(u => u.lang === lang))
|
|
150
177
|
.filter((u) => Boolean(u) && u.files >= minFiles);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CheckContext } from '../types';
|
|
2
|
+
/** One reuse decision, with the sentence that explains it. */
|
|
3
|
+
interface ReuseDecision {
|
|
4
|
+
/** Search roots to prefer, or `undefined` to use our own copy. */
|
|
5
|
+
paths?: string[];
|
|
6
|
+
reason: string;
|
|
7
|
+
}
|
|
8
|
+
interface ReuseOptions {
|
|
9
|
+
/**
|
|
10
|
+
* The tool will read the client's OWN configuration file.
|
|
11
|
+
*
|
|
12
|
+
* When it will, the version check is the wrong question and must not be asked:
|
|
13
|
+
* a repository with ESLint 8 and a legacy `.eslintrc` can only be linted by
|
|
14
|
+
* ESLint 8, because ESLint 10 does not read eslintrc — and falling back to ours
|
|
15
|
+
* on the strength of a major mismatch would break precisely the case the reuse
|
|
16
|
+
* was there to serve. TypeScript is the same shape permanently: the check only
|
|
17
|
+
* applies when the repository has a `tsconfig.json`, so their compiler is
|
|
18
|
+
* always the one their config was written for.
|
|
19
|
+
*
|
|
20
|
+
* The version check exists for the opposite case — the client's old tool being
|
|
21
|
+
* handed configuration WE generated.
|
|
22
|
+
*/
|
|
23
|
+
clientOwnsConfig?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Decide whether this run should prefer the client's copy of `pkgName`.
|
|
27
|
+
*
|
|
28
|
+
* `compatible` (the default) reuses when the client's major matches ours, or when
|
|
29
|
+
* the client's own configuration is what the tool will read. A mismatch is not an
|
|
30
|
+
* error — we have our own copy — so it downgrades to ours with a line saying why.
|
|
31
|
+
*/
|
|
32
|
+
export declare function reuseDecision(pkgName: string, ctx: CheckContext, opts?: ReuseOptions): ReuseDecision;
|
|
33
|
+
/**
|
|
34
|
+
* The search roots a check should prefer for `pkgName`, logging the reason once.
|
|
35
|
+
*
|
|
36
|
+
* The reason is logged rather than returned to each caller because it answers the
|
|
37
|
+
* question every confusing lint result starts with — "whose ESLint ran" — and no
|
|
38
|
+
* check should have to remember to say so.
|
|
39
|
+
*/
|
|
40
|
+
export declare function clientToolPaths(pkgName: string, ctx: CheckContext, opts?: ReuseOptions): string[] | undefined;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
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.reuseDecision = reuseDecision;
|
|
7
|
+
exports.clientToolPaths = clientToolPaths;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const logger_1 = require("../../utils/logger");
|
|
11
|
+
const resolve_bin_1 = require("../../utils/resolve-bin");
|
|
12
|
+
/**
|
|
13
|
+
* The major version we declare for a tool, from our own manifest.
|
|
14
|
+
*
|
|
15
|
+
* Read from `dependencies` rather than from the installed tree: what matters is
|
|
16
|
+
* the version our configuration was written against, and a hoisted install can
|
|
17
|
+
* resolve to something else entirely.
|
|
18
|
+
*/
|
|
19
|
+
function ownMajor(pkgName) {
|
|
20
|
+
try {
|
|
21
|
+
const manifest = node_path_1.default.join((0, resolve_bin_1.ownPackageRoot)(), 'package.json');
|
|
22
|
+
const deps = JSON.parse(node_fs_1.default.readFileSync(manifest, 'utf-8')).dependencies;
|
|
23
|
+
const range = deps?.[pkgName];
|
|
24
|
+
if (!range)
|
|
25
|
+
return null;
|
|
26
|
+
// `^10.6.0`, `>=3.0.0`, `~5.1` — the leading number is the major in every
|
|
27
|
+
// form we declare. Deliberately not a semver parser: this package ships zero
|
|
28
|
+
// runtime dependencies on purpose, and a major comparison is the only
|
|
29
|
+
// question being asked.
|
|
30
|
+
const major = /(\d+)/.exec(range)?.[1];
|
|
31
|
+
return major ? Number(major) : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** The major version of a package as the client actually has it installed. */
|
|
38
|
+
function installedMajor(pkgName, from) {
|
|
39
|
+
let dir = from;
|
|
40
|
+
while (dir !== node_path_1.default.dirname(dir)) {
|
|
41
|
+
const manifest = node_path_1.default.join(dir, 'node_modules', pkgName, 'package.json');
|
|
42
|
+
try {
|
|
43
|
+
if (node_fs_1.default.existsSync(manifest)) {
|
|
44
|
+
const version = JSON.parse(node_fs_1.default.readFileSync(manifest, 'utf-8'))
|
|
45
|
+
.version;
|
|
46
|
+
const major = version ? /(\d+)/.exec(version)?.[1] : undefined;
|
|
47
|
+
return major ? Number(major) : null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
dir = node_path_1.default.dirname(dir);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Decide whether this run should prefer the client's copy of `pkgName`.
|
|
59
|
+
*
|
|
60
|
+
* `compatible` (the default) reuses when the client's major matches ours, or when
|
|
61
|
+
* the client's own configuration is what the tool will read. A mismatch is not an
|
|
62
|
+
* error — we have our own copy — so it downgrades to ours with a line saying why.
|
|
63
|
+
*/
|
|
64
|
+
function reuseDecision(pkgName, ctx, opts = {}) {
|
|
65
|
+
const policy = ctx.config.install?.reuseLocal ?? 'compatible';
|
|
66
|
+
if (policy === 'off') {
|
|
67
|
+
return { reason: 'install.reuseLocal is "off" — using our own copy for reproducibility' };
|
|
68
|
+
}
|
|
69
|
+
const clientMajor = installedMajor(pkgName, ctx.cwd);
|
|
70
|
+
if (clientMajor === null) {
|
|
71
|
+
return { reason: `${pkgName} is not installed in the repository — using our own copy` };
|
|
72
|
+
}
|
|
73
|
+
if (policy === 'always') {
|
|
74
|
+
return {
|
|
75
|
+
paths: [ctx.cwd],
|
|
76
|
+
reason: `install.reuseLocal is "always" — using ${pkgName}@${clientMajor} from the repository`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (opts.clientOwnsConfig) {
|
|
80
|
+
return {
|
|
81
|
+
paths: [ctx.cwd],
|
|
82
|
+
reason: `using ${pkgName}@${clientMajor} from the repository — it reads the repository's own config`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const ourMajor = ownMajor(pkgName);
|
|
86
|
+
if (ourMajor === null || clientMajor === ourMajor) {
|
|
87
|
+
return {
|
|
88
|
+
paths: [ctx.cwd],
|
|
89
|
+
reason: `using ${pkgName}@${clientMajor} from the repository (matches ours)`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
reason: `the repository has ${pkgName}@${clientMajor} and our config is written for @${ourMajor} — using our own copy`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The search roots a check should prefer for `pkgName`, logging the reason once.
|
|
98
|
+
*
|
|
99
|
+
* The reason is logged rather than returned to each caller because it answers the
|
|
100
|
+
* question every confusing lint result starts with — "whose ESLint ran" — and no
|
|
101
|
+
* check should have to remember to say so.
|
|
102
|
+
*/
|
|
103
|
+
function clientToolPaths(pkgName, ctx, opts = {}) {
|
|
104
|
+
const decision = reuseDecision(pkgName, ctx, opts);
|
|
105
|
+
logger_1.logger.debug(`[reuseLocal]: ${decision.reason}`);
|
|
106
|
+
return decision.paths;
|
|
107
|
+
}
|
package/dist/core/registry.js
CHANGED
|
@@ -18,21 +18,13 @@ const merge_conflict_1 = require("./checks/git/merge-conflict");
|
|
|
18
18
|
const missing_tests_1 = require("./checks/git/missing-tests");
|
|
19
19
|
const todo_fixme_1 = require("./checks/git/todo-fixme");
|
|
20
20
|
const cpp_format_1 = require("./checks/languages/cpp-format");
|
|
21
|
-
const
|
|
21
|
+
const external_checks_1 = require("./checks/languages/external-checks");
|
|
22
22
|
const go_format_1 = require("./checks/languages/go-format");
|
|
23
|
-
const go_lint_1 = require("./checks/languages/go-lint");
|
|
24
|
-
const kotlin_lint_1 = require("./checks/languages/kotlin-lint");
|
|
25
23
|
const lint_1 = require("./checks/languages/lint");
|
|
26
|
-
const php_format_1 = require("./checks/languages/php-format");
|
|
27
24
|
const prettier_1 = require("./checks/languages/prettier");
|
|
28
25
|
const python_format_1 = require("./checks/languages/python-format");
|
|
29
26
|
const python_lint_1 = require("./checks/languages/python-lint");
|
|
30
27
|
const python_typecheck_1 = require("./checks/languages/python-typecheck");
|
|
31
|
-
const ruby_lint_1 = require("./checks/languages/ruby-lint");
|
|
32
|
-
const rust_format_1 = require("./checks/languages/rust-format");
|
|
33
|
-
const rust_lint_1 = require("./checks/languages/rust-lint");
|
|
34
|
-
const shellcheck_1 = require("./checks/languages/shellcheck");
|
|
35
|
-
const swift_lint_1 = require("./checks/languages/swift-lint");
|
|
36
28
|
const typecheck_1 = require("./checks/languages/typecheck");
|
|
37
29
|
const commitlint_1 = require("./checks/pr/commitlint");
|
|
38
30
|
const pr_body_1 = require("./checks/pr/pr-body");
|
|
@@ -45,6 +37,7 @@ const dead_code_1 = require("./checks/quality/dead-code");
|
|
|
45
37
|
const duplicate_check_1 = require("./checks/quality/duplicate-check");
|
|
46
38
|
const license_header_1 = require("./checks/quality/license-header");
|
|
47
39
|
const markdown_lint_1 = require("./checks/quality/markdown-lint");
|
|
40
|
+
const package_size_1 = require("./checks/quality/package-size");
|
|
48
41
|
const spellcheck_1 = require("./checks/quality/spellcheck");
|
|
49
42
|
const symlinks_1 = require("./checks/quality/symlinks");
|
|
50
43
|
const yaml_lint_1 = require("./checks/quality/yaml-lint");
|
|
@@ -98,21 +91,22 @@ exports.REGISTRY = [
|
|
|
98
91
|
spellcheck_1.spellcheckCheck,
|
|
99
92
|
yaml_lint_1.yamlLintCheck,
|
|
100
93
|
symlinks_1.symlinksCheck,
|
|
94
|
+
package_size_1.packageSizeCheck,
|
|
101
95
|
python_lint_1.pythonLintCheck,
|
|
102
96
|
python_typecheck_1.pythonTypecheckCheck,
|
|
103
97
|
dead_code_1.deadCodeCheck,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
98
|
+
external_checks_1.swiftLintCheck,
|
|
99
|
+
external_checks_1.kotlinLintCheck,
|
|
100
|
+
external_checks_1.goLintCheck,
|
|
101
|
+
external_checks_1.rustLintCheck,
|
|
102
|
+
external_checks_1.rubyLintCheck,
|
|
103
|
+
external_checks_1.shellcheckCheck,
|
|
110
104
|
// format
|
|
111
105
|
prettier_1.prettierCheck,
|
|
112
106
|
cpp_format_1.cppFormatCheck,
|
|
113
107
|
python_format_1.pythonFormatCheck,
|
|
114
108
|
go_format_1.goFormatCheck,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
109
|
+
external_checks_1.rustFormatCheck,
|
|
110
|
+
external_checks_1.csharpFormatCheck,
|
|
111
|
+
external_checks_1.phpFormatCheck,
|
|
118
112
|
];
|
package/dist/core/run-checks.js
CHANGED
|
@@ -4,9 +4,12 @@ exports.applySeverity = applySeverity;
|
|
|
4
4
|
exports.shouldReportDiffSkip = shouldReportDiffSkip;
|
|
5
5
|
exports.runChecks = runChecks;
|
|
6
6
|
const utils_1 = require("../utils");
|
|
7
|
+
const package_version_1 = require("../utils/package-version");
|
|
7
8
|
const pool_1 = require("../utils/pool");
|
|
9
|
+
const resolve_bin_1 = require("../utils/resolve-bin");
|
|
8
10
|
const result_collector_1 = require("../utils/result-collector");
|
|
9
11
|
const context_1 = require("./context");
|
|
12
|
+
const prepare_1 = require("./install/prepare");
|
|
10
13
|
const registry_1 = require("./registry");
|
|
11
14
|
const reporters_1 = require("./reporters");
|
|
12
15
|
const types_1 = require("./types");
|
|
@@ -107,13 +110,27 @@ function noDiffReason(ctx) {
|
|
|
107
110
|
return `no diff range (${scope.reason}) — set fetch-depth: 0`;
|
|
108
111
|
return 'full scan — this check reviews a diff';
|
|
109
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Whether a skip came from the missing diff range, as opposed to any other reason
|
|
115
|
+
* a check can decline to run.
|
|
116
|
+
*
|
|
117
|
+
* `reportDegradedScope` counts these to say how much coverage a fallback to a full
|
|
118
|
+
* scan cost. It used to count every skip in the report, which already included
|
|
119
|
+
* unrelated ones ("mypy not installed"), and tooling skips would have added more —
|
|
120
|
+
* turning a number that means "checks you lost to a shallow clone" into "skips,
|
|
121
|
+
* roughly". Matched on the phrasing {@link noDiffReason} produces, which is the
|
|
122
|
+
* only thing that distinguishes them once a skip is just a status and a string.
|
|
123
|
+
*/
|
|
124
|
+
function isDiffSkip(details) {
|
|
125
|
+
return Boolean(details && /^no diff range|^full scan — this check reviews a diff/.test(details));
|
|
126
|
+
}
|
|
110
127
|
/**
|
|
111
128
|
* Run one check: gate on `applies`, execute, translate throws into `fail`.
|
|
112
129
|
*
|
|
113
130
|
* `heavy` admits only a couple of expensive checks at a time, and the per-check
|
|
114
131
|
* timeout means one that never returns costs a warning rather than the run.
|
|
115
132
|
*/
|
|
116
|
-
async function runOne(check, ctx, collector, heavy) {
|
|
133
|
+
async function runOne(check, ctx, collector, heavy, tooling) {
|
|
117
134
|
try {
|
|
118
135
|
// A diff-only check without a range is usually a hole in coverage rather
|
|
119
136
|
// than an irrelevance, so it is reported as a skip and the gap stays visible
|
|
@@ -141,6 +158,20 @@ async function runOne(check, ctx, collector, heavy) {
|
|
|
141
158
|
// from run() (e.g. "no relevant files changed") are still reported.
|
|
142
159
|
return;
|
|
143
160
|
}
|
|
161
|
+
// Tooling is checked AFTER `applies`, so the row only appears for a check
|
|
162
|
+
// that would otherwise have run. A Ruff row in a TypeScript repository would
|
|
163
|
+
// be noise; a Ruff row in a Python repository whose toolchain never arrived
|
|
164
|
+
// is the whole point — ticket 057, applied to tooling instead of diffs.
|
|
165
|
+
const missing = tooling.unavailable.get(check.name);
|
|
166
|
+
if (missing) {
|
|
167
|
+
// Severity still has the last word, as it does for every other outcome: a
|
|
168
|
+
// client who made this check advisory does not get a hard failure out of
|
|
169
|
+
// `onMissingTool: "error"`, and one who promoted it does.
|
|
170
|
+
const status = applySeverity(check.name, tooling.fatal ? 'fail' : 'skip', ctx);
|
|
171
|
+
utils_1.logger.warn(`[runChecks]: ${status === 'fail' ? '❌' : '⏭️'} ${check.name}: ${missing}`);
|
|
172
|
+
collector.register(check.name, status, `tooling unavailable — ${missing}`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
144
175
|
const started = Date.now();
|
|
145
176
|
const timeoutMs = ctx.config.run?.checkTimeoutMs ?? 300000;
|
|
146
177
|
const execute = () => (0, pool_1.withTimeout)(check.run(ctx), timeoutMs, () => {
|
|
@@ -180,12 +211,16 @@ async function runChecks(opts = {}) {
|
|
|
180
211
|
// the list from here rather than importing the registry, which would make
|
|
181
212
|
// registry → check → registry a cycle.
|
|
182
213
|
ctx.checkNames = resolveChecks(opts).map(c => c.name);
|
|
214
|
+
// Before any check starts: make sure the tools they need are reachable, and
|
|
215
|
+
// learn which ones are not. Doing this per-check would fetch the same profile
|
|
216
|
+
// from several checks at once and leave each of them guessing on failure.
|
|
217
|
+
const tooling = await prepareToolingFor(ctx, selected);
|
|
183
218
|
const limit = (0, pool_1.resolveConcurrency)(ctx.config.run?.concurrency);
|
|
184
219
|
const heavyLimit = Math.max(1, ctx.config.run?.heavyConcurrency ?? 1);
|
|
185
220
|
const heavy = new pool_1.Semaphore(heavyLimit);
|
|
186
221
|
for (const phase of types_1.PHASES) {
|
|
187
222
|
const inPhase = selected.filter(c => c.phase === phase);
|
|
188
|
-
await (0, pool_1.runPool)(inPhase.map(c => () => runOne(c, ctx, collector, heavy)), limit);
|
|
223
|
+
await (0, pool_1.runPool)(inPhase.map(c => () => runOne(c, ctx, collector, heavy, tooling)), limit);
|
|
189
224
|
}
|
|
190
225
|
reportDegradedScope(ctx, collector);
|
|
191
226
|
const build = () => {
|
|
@@ -204,6 +239,40 @@ async function runChecks(opts = {}) {
|
|
|
204
239
|
}
|
|
205
240
|
return result;
|
|
206
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Prepare the tooling the selected checks need, or explain what is missing.
|
|
244
|
+
*
|
|
245
|
+
* Separate from `prepareTooling` itself so the failure of the *preparation* is
|
|
246
|
+
* never the failure of the *run*: fetching tooling is best-effort by nature —
|
|
247
|
+
* offline runners, read-only caches, a registry having a bad day — and a throw
|
|
248
|
+
* here would take down a run that could still have done most of its job. The
|
|
249
|
+
* checks that needed the tooling report it themselves, one row each.
|
|
250
|
+
*
|
|
251
|
+
* `install.strategy: "manual"` and a caller that supplied its own checks both
|
|
252
|
+
* arrive here; neither is special-cased, because a profile whose packages ship
|
|
253
|
+
* with us needs no preparation either way and the loop exits having done nothing.
|
|
254
|
+
*/
|
|
255
|
+
async function prepareToolingFor(ctx, selected) {
|
|
256
|
+
try {
|
|
257
|
+
return await (0, prepare_1.prepareTooling)({
|
|
258
|
+
cwd: ctx.cwd,
|
|
259
|
+
config: ctx.config,
|
|
260
|
+
// A hand-built context (the SDK, a test) carries no counts. Treating its
|
|
261
|
+
// languages as real rather than marker-only is the honest reading: a caller
|
|
262
|
+
// that names a language is asserting it, not guessing.
|
|
263
|
+
usage: ctx.usage ?? ctx.languages.map(lang => ({ lang, files: 1, marker: true })),
|
|
264
|
+
selected: selected.map(c => c.name),
|
|
265
|
+
version: (0, package_version_1.packageVersion)(),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
utils_1.logger.warn(`[install]: ⚠️ could not prepare tooling: ${errMessage(err)}`);
|
|
270
|
+
// Preparation may have registered roots before it threw. Leaving them behind
|
|
271
|
+
// would point resolution at a half-installed directory for the whole run.
|
|
272
|
+
(0, resolve_bin_1.clearToolPaths)();
|
|
273
|
+
return (0, prepare_1.noTooling)();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
207
276
|
/**
|
|
208
277
|
* Surface a scope that silently narrowed the run.
|
|
209
278
|
*
|
|
@@ -220,7 +289,9 @@ function reportDegradedScope(ctx, collector) {
|
|
|
220
289
|
const mode = ctx.config.run?.onDegradedScope ?? 'warn';
|
|
221
290
|
if (mode === 'off' || !ctx.scope?.degraded || !(0, utils_1.isCiEnvironment)())
|
|
222
291
|
return;
|
|
223
|
-
const dropped = collector
|
|
292
|
+
const dropped = collector
|
|
293
|
+
.getResults()
|
|
294
|
+
.filter(r => r.status === 'skip' && isDiffSkip(r.details)).length;
|
|
224
295
|
const detail = `${ctx.scope.reason}; ${dropped} diff-only check(s) did not run — set fetch-depth: 0`;
|
|
225
296
|
utils_1.logger.error(`[runChecks]: ${mode === 'error' ? '❌' : '⚠️'} Degraded scope: ${detail}`);
|
|
226
297
|
collector.register('Scan Scope', mode === 'error' ? 'fail' : 'warn', detail);
|