pr-checkmate 2.1.2 → 3.0.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.
- package/README.md +63 -268
- package/cspell.json +155 -133
- package/dist/config/constants.d.ts +6 -1
- package/dist/config/constants.js +8 -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/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/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/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/context.js +9 -1
- package/dist/core/install/cache.js +42 -11
- package/dist/core/install/doctor.js +9 -0
- package/dist/core/install/ensure.js +22 -3
- package/dist/core/install/lock.d.ts +2 -1
- package/dist/core/install/prepare.d.ts +75 -0
- package/dist/core/install/prepare.js +151 -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 +10 -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 +1 -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/go/package-lock.json +24 -0
- package/install-manifests/go/package.json +8 -0
- package/install-manifests/pytypes/package-lock.json +45 -0
- package/install-manifests/pytypes/package.json +8 -0
- package/package.json +4 -2
- package/pr-checkmate.schema.json +8 -2
- package/src/config/cspell.json +155 -133
- package/src/config/pr-checkmate.schema.json +8 -2
- 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
|
@@ -52,6 +52,12 @@ export declare const DIFF_SECURITY_IGNORE_DEFAULTS: string[];
|
|
|
52
52
|
* committed reappears in it — 39 of 42 spellcheck findings in one real repository
|
|
53
53
|
* came from that one file, none of them actionable: the text cannot be corrected
|
|
54
54
|
* without rewriting git history, and the next release regenerates it anyway.
|
|
55
|
+
*
|
|
56
|
+
* Matched at ANY depth, not just at the repository root. The root-only form was
|
|
57
|
+
* the original, and it left every nested changelog unhandled: a component-config
|
|
58
|
+
* repository with one per component produced 157 of 157 spelling findings and 240
|
|
59
|
+
* of 246 markdown findings from files nobody writes by hand. A generated file is
|
|
60
|
+
* generated wherever it sits.
|
|
55
61
|
*/
|
|
56
62
|
export declare const GENERATED_DOC_FILES: string[];
|
|
57
63
|
/**
|
|
@@ -136,7 +142,6 @@ export declare const documentation: {
|
|
|
136
142
|
prettier: string;
|
|
137
143
|
cspell: string;
|
|
138
144
|
};
|
|
139
|
-
export declare const CONFIG_FILE: string;
|
|
140
145
|
/**
|
|
141
146
|
* Every top-level key PR CheckMate recognises in pr-checkmate.json — the union
|
|
142
147
|
* of the `PRCheckMateConfig` interface fields and the keys the `init` generator
|
package/dist/config/constants.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.KNOWN_CONFIG_KEYS = exports.
|
|
7
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
3
|
+
exports.KNOWN_CONFIG_KEYS = exports.documentation = exports.FULL_CONFIG_URL = exports.DOCS_URL = exports.DEFAULT_ALLOW_PACKAGES = exports.DEFAULT_COMMANDS = exports.SOURCE_EXTENSIONS = exports.DUPLICATE_CODE_FORMATS = exports.CONFIG_FILE_PATTERNS = exports.DEAD_CODE_IGNORE_DEFAULTS = exports.GENERATED_DOC_FILES = exports.DIFF_SECURITY_IGNORE_DEFAULTS = exports.DUPLICATE_IGNORE_DEFAULTS = exports.TEST_PATH_GLOBS = exports.IGNORE_PATTERNS = exports.IGNORED_DIRS = void 0;
|
|
8
4
|
/**
|
|
9
5
|
* Build output, dependency, tooling, and report directories that are never
|
|
10
6
|
* product code. Every check excludes them by directory-name segment match (see
|
|
@@ -156,8 +152,14 @@ exports.DIFF_SECURITY_IGNORE_DEFAULTS = [...exports.TEST_PATH_GLOBS];
|
|
|
156
152
|
* committed reappears in it — 39 of 42 spellcheck findings in one real repository
|
|
157
153
|
* came from that one file, none of them actionable: the text cannot be corrected
|
|
158
154
|
* without rewriting git history, and the next release regenerates it anyway.
|
|
155
|
+
*
|
|
156
|
+
* Matched at ANY depth, not just at the repository root. The root-only form was
|
|
157
|
+
* the original, and it left every nested changelog unhandled: a component-config
|
|
158
|
+
* repository with one per component produced 157 of 157 spelling findings and 240
|
|
159
|
+
* of 246 markdown findings from files nobody writes by hand. A generated file is
|
|
160
|
+
* generated wherever it sits.
|
|
159
161
|
*/
|
|
160
|
-
exports.GENERATED_DOC_FILES = ['CHANGELOG.md'];
|
|
162
|
+
exports.GENERATED_DOC_FILES = ['**/CHANGELOG.md'];
|
|
161
163
|
/**
|
|
162
164
|
* Files excluded from the dead-code (unused-exports) report by default. These are
|
|
163
165
|
* REGEX sources (matched against each reported file path), not globs — dead-code's
|
|
@@ -279,7 +281,6 @@ exports.documentation = {
|
|
|
279
281
|
prettier: 'https://prettier.io/docs/en/options.html',
|
|
280
282
|
cspell: 'https://cspell.org/configuration/',
|
|
281
283
|
};
|
|
282
|
-
exports.CONFIG_FILE = node_path_1.default.resolve(process.cwd(), 'pr-checkmate.json');
|
|
283
284
|
/**
|
|
284
285
|
* Every top-level key PR CheckMate recognises in pr-checkmate.json — the union
|
|
285
286
|
* of the `PRCheckMateConfig` interface fields and the keys the `init` generator
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
export * from './constants';
|
|
2
2
|
export { loadProjectConfig } from '../config/loader';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* values fall back to `['.']
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The configured source path(s), normalised to a list.
|
|
5
|
+
*
|
|
6
|
+
* Accepts a string or an array; empty and invalid values fall back to `['.']`,
|
|
7
|
+
* the whole repository. Clients narrow scanning to a subdirectory by setting
|
|
8
|
+
* `sourcePath`.
|
|
9
|
+
*
|
|
10
|
+
* `cwd` is a parameter rather than `process.cwd()` baked in, and that is the
|
|
11
|
+
* whole of ticket 074. This used to read a `CONFIG_FILE` constant resolved at
|
|
12
|
+
* *module load*, so the answer was fixed to whatever directory the process
|
|
13
|
+
* started in — `chdir` could not change it and neither could a caller. For the
|
|
14
|
+
* CLI that is harmless, since its working directory never moves; for
|
|
15
|
+
* `runChecks({ cwd })` it meant the SDK consumer's own `sourcePath` was applied
|
|
16
|
+
* to somebody else's repository, silently scoping every scoped check to a
|
|
17
|
+
* directory that may not exist there.
|
|
18
|
+
*
|
|
19
|
+
* It also read the file itself, duplicating `loadProjectConfig` with its own
|
|
20
|
+
* defaults. One reader is now the only reader.
|
|
9
21
|
*/
|
|
10
|
-
export declare function getSourcePaths(): string[];
|
|
22
|
+
export declare function getSourcePaths(cwd?: string): string[];
|
|
11
23
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
24
|
+
* The first configured source path, for callers that only support one directory.
|
|
25
|
+
* Prefer {@link getSourcePaths} for scan targets.
|
|
14
26
|
*/
|
|
15
|
-
export declare function getSourcePath(): string;
|
|
27
|
+
export declare function getSourcePath(cwd?: string): string;
|
|
16
28
|
/**
|
|
17
29
|
* Get the effective list of directory names excluded from every check
|
|
18
30
|
* (client `ignoreDirs` in pr-checkmate.json, or the curated default).
|
|
19
31
|
*/
|
|
20
|
-
export declare function getIgnoredDirs(): string[];
|
|
32
|
+
export declare function getIgnoredDirs(cwd?: string): string[];
|
|
21
33
|
/**
|
|
22
34
|
* Get path to package config files
|
|
23
35
|
*/
|
|
@@ -25,4 +37,4 @@ export declare function getPackageConfigPath(fileName: string): string;
|
|
|
25
37
|
/**
|
|
26
38
|
* Check if project has local config
|
|
27
39
|
*/
|
|
28
|
-
export declare function hasLocalConfig(configNames: string[]): boolean;
|
|
40
|
+
export declare function hasLocalConfig(configNames: string[], cwd?: string): boolean;
|
package/dist/config/index.js
CHANGED
|
@@ -26,51 +26,46 @@ exports.hasLocalConfig = hasLocalConfig;
|
|
|
26
26
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
27
27
|
const node_path_1 = __importDefault(require("node:path"));
|
|
28
28
|
const loader_1 = require("../config/loader");
|
|
29
|
-
const utils_1 = require("../utils");
|
|
30
|
-
const constants_1 = require("./constants");
|
|
31
29
|
__exportStar(require("./constants"), exports);
|
|
32
30
|
var loader_2 = require("../config/loader");
|
|
33
31
|
Object.defineProperty(exports, "loadProjectConfig", { enumerable: true, get: function () { return loader_2.loadProjectConfig; } });
|
|
34
32
|
/**
|
|
35
|
-
*
|
|
33
|
+
* The configured source path(s), normalised to a list.
|
|
34
|
+
*
|
|
35
|
+
* Accepts a string or an array; empty and invalid values fall back to `['.']`,
|
|
36
|
+
* the whole repository. Clients narrow scanning to a subdirectory by setting
|
|
37
|
+
* `sourcePath`.
|
|
38
|
+
*
|
|
39
|
+
* `cwd` is a parameter rather than `process.cwd()` baked in, and that is the
|
|
40
|
+
* whole of ticket 074. This used to read a `CONFIG_FILE` constant resolved at
|
|
41
|
+
* *module load*, so the answer was fixed to whatever directory the process
|
|
42
|
+
* started in — `chdir` could not change it and neither could a caller. For the
|
|
43
|
+
* CLI that is harmless, since its working directory never moves; for
|
|
44
|
+
* `runChecks({ cwd })` it meant the SDK consumer's own `sourcePath` was applied
|
|
45
|
+
* to somebody else's repository, silently scoping every scoped check to a
|
|
46
|
+
* directory that may not exist there.
|
|
47
|
+
*
|
|
48
|
+
* It also read the file itself, duplicating `loadProjectConfig` with its own
|
|
49
|
+
* defaults. One reader is now the only reader.
|
|
36
50
|
*/
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
if (node_fs_1.default.existsSync(constants_1.CONFIG_FILE)) {
|
|
40
|
-
const configContent = node_fs_1.default.readFileSync(constants_1.CONFIG_FILE, 'utf-8');
|
|
41
|
-
return JSON.parse(configContent);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
catch (err) {
|
|
45
|
-
utils_1.logger.warn(`⚠️ Failed to read config from ${constants_1.CONFIG_FILE}, using defaults${err}`);
|
|
46
|
-
}
|
|
47
|
-
return { sourcePath: '.' };
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Get the configured source path(s) dynamically as a normalized list.
|
|
51
|
-
* Accepts a single string or an array in pr-checkmate.json; empty/invalid
|
|
52
|
-
* values fall back to `['.']` (the whole repo). Clients narrow scanning to a
|
|
53
|
-
* subdirectory by setting `sourcePath` in their pr-checkmate.json. Always reads
|
|
54
|
-
* fresh config.
|
|
55
|
-
*/
|
|
56
|
-
function getSourcePaths() {
|
|
57
|
-
const { sourcePath } = loadConfig();
|
|
51
|
+
function getSourcePaths(cwd = process.cwd()) {
|
|
52
|
+
const { sourcePath } = (0, loader_1.loadProjectConfig)(cwd);
|
|
58
53
|
const paths = (Array.isArray(sourcePath) ? sourcePath : [sourcePath]).filter((p) => typeof p === 'string' && p.trim().length > 0);
|
|
59
54
|
return paths.length > 0 ? paths : ['.'];
|
|
60
55
|
}
|
|
61
56
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
57
|
+
* The first configured source path, for callers that only support one directory.
|
|
58
|
+
* Prefer {@link getSourcePaths} for scan targets.
|
|
64
59
|
*/
|
|
65
|
-
function getSourcePath() {
|
|
66
|
-
return getSourcePaths()[0];
|
|
60
|
+
function getSourcePath(cwd) {
|
|
61
|
+
return getSourcePaths(cwd)[0];
|
|
67
62
|
}
|
|
68
63
|
/**
|
|
69
64
|
* Get the effective list of directory names excluded from every check
|
|
70
65
|
* (client `ignoreDirs` in pr-checkmate.json, or the curated default).
|
|
71
66
|
*/
|
|
72
|
-
function getIgnoredDirs() {
|
|
73
|
-
return (0, loader_1.loadProjectConfig)().ignoreDirs ?? [];
|
|
67
|
+
function getIgnoredDirs(cwd) {
|
|
68
|
+
return (0, loader_1.loadProjectConfig)(cwd).ignoreDirs ?? [];
|
|
74
69
|
}
|
|
75
70
|
/**
|
|
76
71
|
* Get path to package config files
|
|
@@ -82,7 +77,6 @@ function getPackageConfigPath(fileName) {
|
|
|
82
77
|
/**
|
|
83
78
|
* Check if project has local config
|
|
84
79
|
*/
|
|
85
|
-
function hasLocalConfig(configNames) {
|
|
86
|
-
|
|
87
|
-
return configNames.some(name => node_fs_1.default.existsSync(node_path_1.default.join(projectRoot, name)));
|
|
80
|
+
function hasLocalConfig(configNames, cwd = process.cwd()) {
|
|
81
|
+
return configNames.some(name => node_fs_1.default.existsSync(node_path_1.default.join(cwd, name)));
|
|
88
82
|
}
|
|
@@ -39,7 +39,7 @@ exports.bannedImportsCheck = {
|
|
|
39
39
|
try {
|
|
40
40
|
const errors = {};
|
|
41
41
|
const warnings = {};
|
|
42
|
-
await (0, findings_1.reviewLines)(ctx, ['*'], stdout => {
|
|
42
|
+
const reviewed = await (0, findings_1.reviewLines)(ctx, ['*'], stdout => {
|
|
43
43
|
var _a;
|
|
44
44
|
for (const raw of stdout.split('\n')) {
|
|
45
45
|
if (!raw.startsWith('+') || raw.startsWith('+++'))
|
|
@@ -53,6 +53,9 @@ exports.bannedImportsCheck = {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
|
+
// An unreadable file list is not "no banned imports" (ticket 072).
|
|
57
|
+
if (!reviewed)
|
|
58
|
+
return (0, types_1.skip)('git unavailable');
|
|
56
59
|
// A ban configured as "error" blocks the run, regardless of phase — that
|
|
57
60
|
// is the default `failOn`, and the client can still relax it per repo.
|
|
58
61
|
return (0, findings_1.reportSeverityBuckets)(errors, warnings, {
|
|
@@ -9,6 +9,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
9
9
|
const config_1 = require("../../../config");
|
|
10
10
|
const utils_1 = require("../../../utils");
|
|
11
11
|
const execa_1 = require("../../../utils/execa");
|
|
12
|
+
const reuse_1 = require("../../install/reuse");
|
|
12
13
|
const targets_1 = require("../../targets");
|
|
13
14
|
const types_1 = require("../../types");
|
|
14
15
|
const findings_1 = require("../findings");
|
|
@@ -36,8 +37,10 @@ const SOURCE_GLOBS = ['*.ts', '*.tsx', '*.js', '*.jsx'];
|
|
|
36
37
|
* dpdm from the registry on first use — and when that fetch could not happen
|
|
37
38
|
* (offline or locked-down CI, private registry, exit 127) nothing was analyzed.
|
|
38
39
|
*/
|
|
39
|
-
function resolveDpdm(
|
|
40
|
-
|
|
40
|
+
function resolveDpdm(ctx) {
|
|
41
|
+
// No configuration of its own — dpdm reads the module graph — so the version
|
|
42
|
+
// check is the right question here and `clientOwnsConfig` stays off.
|
|
43
|
+
for (const options of [{ paths: (0, reuse_1.clientToolPaths)('dpdm', ctx) }, undefined]) {
|
|
41
44
|
try {
|
|
42
45
|
return (0, utils_1.resolvePackageBin)('dpdm', 'lib/bin/dpdm.js', options);
|
|
43
46
|
}
|
|
@@ -80,7 +83,13 @@ exports.circularDepsCheck = {
|
|
|
80
83
|
return ctx.languages.includes('typescript') || ctx.languages.includes('javascript');
|
|
81
84
|
},
|
|
82
85
|
async run(ctx) {
|
|
83
|
-
const allFiles = await (0, targets_1.listTrackedFiles)(ctx, SOURCE_GLOBS, (0, config_1.getSourcePaths)());
|
|
86
|
+
const allFiles = await (0, targets_1.listTrackedFiles)(ctx, SOURCE_GLOBS, (0, config_1.getSourcePaths)(ctx.cwd));
|
|
87
|
+
// A cycle detector needs the WHOLE graph, so an unreadable file list is the
|
|
88
|
+
// one case where passing would be worst: half a graph produces confident
|
|
89
|
+
// nonsense, and no graph reported as `pass` says the repository is acyclic
|
|
90
|
+
// when nobody looked (ticket 072).
|
|
91
|
+
if (allFiles === null)
|
|
92
|
+
return (0, types_1.skip)('git unavailable');
|
|
84
93
|
if (allFiles.length === 0)
|
|
85
94
|
return (0, types_1.pass)();
|
|
86
95
|
// Files the client exempted (generated code, vendored bundles) are dropped
|
|
@@ -90,7 +99,7 @@ exports.circularDepsCheck = {
|
|
|
90
99
|
const files = ignore.length > 0 ? allFiles.filter(f => !(0, utils_1.matchesAnyGlob)(f, ignore)) : allFiles;
|
|
91
100
|
if (files.length === 0)
|
|
92
101
|
return (0, types_1.skip)('all files ignored');
|
|
93
|
-
const dpdmBin = resolveDpdm(ctx
|
|
102
|
+
const dpdmBin = resolveDpdm(ctx);
|
|
94
103
|
if (!dpdmBin) {
|
|
95
104
|
utils_1.logger.warn('[Circular Deps]: ⚠️ dpdm could not be resolved — skipping');
|
|
96
105
|
return (0, types_1.skip)('dpdm unavailable');
|
|
@@ -114,7 +123,7 @@ exports.circularDepsCheck = {
|
|
|
114
123
|
'circular:1',
|
|
115
124
|
'-o',
|
|
116
125
|
reportPath,
|
|
117
|
-
...toFileArgs(files, (0, config_1.getSourcePaths)()),
|
|
126
|
+
...toFileArgs(files, (0, config_1.getSourcePaths)(ctx.cwd)),
|
|
118
127
|
], { cwd: ctx.cwd, reject: false, timeout });
|
|
119
128
|
if (result.timedOut) {
|
|
120
129
|
utils_1.logger.warn(`[Circular Deps]: ⚠️ dpdm timed out after ${timeout}ms — skipping`);
|
|
@@ -122,7 +122,7 @@ exports.dependencyCheck = {
|
|
|
122
122
|
// from a config file only, so they go into a temp one — but a client's own knip
|
|
123
123
|
// config wins outright, exactly as it does for cspell.
|
|
124
124
|
const ignore = ctx.config.dependency?.ignore ?? [];
|
|
125
|
-
const clientOwnsConfig = (0, config_1.hasLocalConfig)(KNIP_CONFIG_FILES) || hasPackageJsonConfig(ctx.cwd);
|
|
125
|
+
const clientOwnsConfig = (0, config_1.hasLocalConfig)(KNIP_CONFIG_FILES, ctx.cwd) || hasPackageJsonConfig(ctx.cwd);
|
|
126
126
|
let tempConfigPath = null;
|
|
127
127
|
if (ignore.length > 0 && clientOwnsConfig) {
|
|
128
128
|
// Say so out loud: a silently ignored `ignore` list looks like a broken check.
|
|
@@ -4,6 +4,7 @@ exports.grypeScanCheck = void 0;
|
|
|
4
4
|
const utils_1 = require("../../../utils");
|
|
5
5
|
const execa_1 = require("../../../utils/execa");
|
|
6
6
|
const types_1 = require("../../types");
|
|
7
|
+
const tool_install_1 = require("../tool-install");
|
|
7
8
|
const SEVERITY_ORDER = ['negligible', 'low', 'medium', 'high', 'critical'];
|
|
8
9
|
/** grype artifact `type` → the ecosystem we can resolve direct deps for. */
|
|
9
10
|
const TYPE_TO_ECOSYSTEM = {
|
|
@@ -69,7 +70,7 @@ exports.grypeScanCheck = {
|
|
|
69
70
|
// execa v9 with reject:false resolves (not throws) with `code: 'ENOENT'`
|
|
70
71
|
// when the binary is absent — detect the missing scanner on the result.
|
|
71
72
|
if (result.code === 'ENOENT')
|
|
72
|
-
return (0,
|
|
73
|
+
return (0, tool_install_1.notInstalled)('grype');
|
|
73
74
|
if (result.timedOut) {
|
|
74
75
|
utils_1.logger.warn(`[Grype Scan]: ⚠️ grype timed out after ${timeout}ms — skipping`);
|
|
75
76
|
return (0, types_1.skip)('grype timed out');
|
|
@@ -4,6 +4,7 @@ exports.vulnScanCheck = void 0;
|
|
|
4
4
|
const utils_1 = require("../../../utils");
|
|
5
5
|
const execa_1 = require("../../../utils/execa");
|
|
6
6
|
const types_1 = require("../../types");
|
|
7
|
+
const tool_install_1 = require("../tool-install");
|
|
7
8
|
/**
|
|
8
9
|
* Cross-language dependency vulnerability scan via osv-scanner, which reads
|
|
9
10
|
* lockfiles across ecosystems (npm, PyPI, Go, crates, Maven, …) against the OSV
|
|
@@ -33,7 +34,7 @@ exports.vulnScanCheck = {
|
|
|
33
34
|
// execa v9 with reject:false resolves (not throws) with `code: 'ENOENT'`
|
|
34
35
|
// when the binary is absent — detect the missing scanner on the result.
|
|
35
36
|
if (result.code === 'ENOENT')
|
|
36
|
-
return (0,
|
|
37
|
+
return (0, tool_install_1.notInstalled)('osv-scanner');
|
|
37
38
|
if (result.timedOut) {
|
|
38
39
|
utils_1.logger.warn(`[Vuln Scan]: ⚠️ osv-scanner timed out after ${timeout}ms — skipping`);
|
|
39
40
|
return (0, types_1.skip)('osv-scanner timed out');
|
|
@@ -74,8 +74,15 @@ export declare function scanTrackedFiles(ctx: CheckContext, files: string[], onF
|
|
|
74
74
|
*
|
|
75
75
|
* `onChunk` is called once per diff in delta mode and once per file in full mode,
|
|
76
76
|
* which keeps a whole monorepo from being concatenated before matching starts.
|
|
77
|
+
*
|
|
78
|
+
* Returns whether the review actually happened. `false` means git could not
|
|
79
|
+
* produce a file list, and the caller has to say so rather than report a pass:
|
|
80
|
+
* a check that reviewed nothing looks exactly like a check that found nothing,
|
|
81
|
+
* and that is the whole of ticket 072. `fullScanBehaviour: "skip"` is the one
|
|
82
|
+
* case where `true` is returned having reviewed nothing on purpose — the client
|
|
83
|
+
* asked for that, so it is not a hole in coverage.
|
|
77
84
|
*/
|
|
78
|
-
export declare function reviewLines(ctx: CheckContext, globs: string[], onChunk: (stdout: string) => void): Promise<
|
|
85
|
+
export declare function reviewLines(ctx: CheckContext, globs: string[], onChunk: (stdout: string) => void): Promise<boolean>;
|
|
79
86
|
/**
|
|
80
87
|
* Which comment markers apply to a file, since they are not universal:
|
|
81
88
|
* - `c-like` — `//` only. `#` is NOT a comment in C-family syntax, where it
|
|
@@ -129,6 +136,17 @@ export interface DiffPattern {
|
|
|
129
136
|
*/
|
|
130
137
|
view?: 'raw' | 'code';
|
|
131
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* How many added lines a taint survives, for the one check that asks for it.
|
|
141
|
+
*
|
|
142
|
+
* Exported because the default belongs to Diff Security, not to this function:
|
|
143
|
+
* `scanDiffPatterns` is shared by Diff Smells, Workflow Security and Dockerfile
|
|
144
|
+
* Security too, and defaulting the heuristic ON here leaked a finding labelled
|
|
145
|
+
* "SQL injection risk" into a code-smell report — under the wrong check, three
|
|
146
|
+
* times per run, off a config key (`diffSecurity.dataflowWindow`) that has no
|
|
147
|
+
* meaning for any of them. Caught by our own Diff Smells row.
|
|
148
|
+
*/
|
|
149
|
+
export declare const DEFAULT_DATAFLOW_WINDOW = 10;
|
|
132
150
|
/**
|
|
133
151
|
* Bucket added diff lines by the {@link DiffPattern}s they match, split by
|
|
134
152
|
* severity. Patterns are expected to anchor on the leading `+` themselves
|
|
@@ -150,8 +168,34 @@ export declare function scanDiffPatterns(stdout: string, patterns: readonly Diff
|
|
|
150
168
|
comments?: 'language' | 'legacy' | 'off';
|
|
151
169
|
/** Globs whose files are skipped entirely (client `<check>.ignorePaths`). */
|
|
152
170
|
ignorePaths?: string[];
|
|
171
|
+
/**
|
|
172
|
+
* Added lines a taint survives for the cross-line SQL heuristic.
|
|
173
|
+
*
|
|
174
|
+
* Off unless the caller asks. Only Diff Security owns this behaviour — it is
|
|
175
|
+
* configured by `diffSecurity.dataflowWindow` and reports a security label —
|
|
176
|
+
* so every other caller of this function gets a plain pattern scan.
|
|
177
|
+
*/
|
|
178
|
+
dataflowWindow?: number;
|
|
153
179
|
}): SeverityBuckets;
|
|
154
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Log a capped sample of raw output lines under a one-line summary.
|
|
182
|
+
*
|
|
183
|
+
* The cap is the point: a linter with 400 offences must not push the rest of the
|
|
184
|
+
* report out of a CI log's scrollback, and the count that follows the sample is
|
|
185
|
+
* what keeps the truncation from reading as the whole story.
|
|
186
|
+
*
|
|
187
|
+
* Deliberately not the private `logFindings` below, which this nearly collided
|
|
188
|
+
* with. That one takes findings already grouped into labelled buckets and pairs
|
|
189
|
+
* the log with a severity; this one takes lines a tool printed and does nothing
|
|
190
|
+
* but show them, which is what a check wrapping somebody else's output needs and
|
|
191
|
+
* the only thing it can honestly claim about them.
|
|
192
|
+
*
|
|
193
|
+
* Here rather than beside its first caller because three categories want it now —
|
|
194
|
+
* the language runner, the Python type check, and Duplicate Code — and a
|
|
195
|
+
* `quality/` check importing from `languages/` to get a logger was the wrong
|
|
196
|
+
* shape (ticket 079).
|
|
197
|
+
*/
|
|
198
|
+
export declare function logSample(name: string, summary: string, lines: string[]): void;
|
|
155
199
|
export declare function bucketTotal(bucket: FindingBucket): number;
|
|
156
200
|
/**
|
|
157
201
|
* Report a bucket at the check's configured level, summarized as
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_DATAFLOW_WINDOW = void 0;
|
|
6
7
|
exports.severityEmitter = severityEmitter;
|
|
7
8
|
exports.mergeSeverityBuckets = mergeSeverityBuckets;
|
|
8
9
|
exports.emptyBuckets = emptyBuckets;
|
|
@@ -13,6 +14,7 @@ exports.commentSyntaxFor = commentSyntaxFor;
|
|
|
13
14
|
exports.stripInlineComment = stripInlineComment;
|
|
14
15
|
exports.stripStringLiterals = stripStringLiterals;
|
|
15
16
|
exports.scanDiffPatterns = scanDiffPatterns;
|
|
17
|
+
exports.logSample = logSample;
|
|
16
18
|
exports.bucketTotal = bucketTotal;
|
|
17
19
|
exports.reportCountedFindings = reportCountedFindings;
|
|
18
20
|
exports.clientFindingOptions = clientFindingOptions;
|
|
@@ -142,16 +144,26 @@ async function scanTrackedFiles(ctx, files, onFile) {
|
|
|
142
144
|
*
|
|
143
145
|
* `onChunk` is called once per diff in delta mode and once per file in full mode,
|
|
144
146
|
* which keeps a whole monorepo from being concatenated before matching starts.
|
|
147
|
+
*
|
|
148
|
+
* Returns whether the review actually happened. `false` means git could not
|
|
149
|
+
* produce a file list, and the caller has to say so rather than report a pass:
|
|
150
|
+
* a check that reviewed nothing looks exactly like a check that found nothing,
|
|
151
|
+
* and that is the whole of ticket 072. `fullScanBehaviour: "skip"` is the one
|
|
152
|
+
* case where `true` is returned having reviewed nothing on purpose — the client
|
|
153
|
+
* asked for that, so it is not a hole in coverage.
|
|
145
154
|
*/
|
|
146
155
|
async function reviewLines(ctx, globs, onChunk) {
|
|
147
156
|
if (ctx.baseSha) {
|
|
148
157
|
onChunk(await diffAddedLines(ctx, globs));
|
|
149
|
-
return;
|
|
158
|
+
return true;
|
|
150
159
|
}
|
|
151
160
|
if (ctx.config.run?.fullScanBehaviour === 'skip')
|
|
152
|
-
return;
|
|
161
|
+
return true;
|
|
153
162
|
const files = await (0, targets_1.listTrackedFiles)(ctx, globs, ['.']);
|
|
163
|
+
if (files === null)
|
|
164
|
+
return false;
|
|
154
165
|
await scanTrackedFiles(ctx, files, onChunk);
|
|
166
|
+
return true;
|
|
155
167
|
}
|
|
156
168
|
const C_LIKE_EXTENSIONS = new Set([
|
|
157
169
|
'ts',
|
|
@@ -340,6 +352,94 @@ function stripStringLiterals(line) {
|
|
|
340
352
|
}
|
|
341
353
|
/** Characters kept per logged sample, so one minified added line can't flood the log. */
|
|
342
354
|
const SAMPLE_MAX_LENGTH = 120;
|
|
355
|
+
/**
|
|
356
|
+
* Cross-line taint tracking: the cheapest useful step beyond a per-line regex.
|
|
357
|
+
*
|
|
358
|
+
* A per-line pattern cannot see the shape that actually ships:
|
|
359
|
+
*
|
|
360
|
+
* ```ts
|
|
361
|
+
* const q = `SELECT * FROM u WHERE id=${id}`;
|
|
362
|
+
* db.query(q);
|
|
363
|
+
* ```
|
|
364
|
+
*
|
|
365
|
+
* Both halves are innocent alone — a template literal is not a query, and
|
|
366
|
+
* `query(q)` is not an interpolation — so the built-in rule, which requires the
|
|
367
|
+
* backtick *inside* the call, misses it entirely. That is the most ordinary way
|
|
368
|
+
* anyone writes an injection.
|
|
369
|
+
*
|
|
370
|
+
* This is deliberately not dataflow analysis. It remembers one thing (a variable
|
|
371
|
+
* assigned from an interpolated template literal), for a bounded number of added
|
|
372
|
+
* lines, inside one hunk, and reports when that name is handed to a query sink.
|
|
373
|
+
* No scopes, no reassignment, no function boundaries. Ticket 058 calls the
|
|
374
|
+
* ceiling out on purpose: an honest boundary beats an inflated promise, and the
|
|
375
|
+
* finding is rated `warn` because a heuristic must not block a build.
|
|
376
|
+
*/
|
|
377
|
+
const TAINT_DECLARATION = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*`[^`]*\$\{/;
|
|
378
|
+
/** Sinks that turn a tainted string into a query. */
|
|
379
|
+
const TAINT_SINKS = 'query|execute|exec|raw';
|
|
380
|
+
/**
|
|
381
|
+
* How many added lines a taint survives, for the one check that asks for it.
|
|
382
|
+
*
|
|
383
|
+
* Exported because the default belongs to Diff Security, not to this function:
|
|
384
|
+
* `scanDiffPatterns` is shared by Diff Smells, Workflow Security and Dockerfile
|
|
385
|
+
* Security too, and defaulting the heuristic ON here leaked a finding labelled
|
|
386
|
+
* "SQL injection risk" into a code-smell report — under the wrong check, three
|
|
387
|
+
* times per run, off a config key (`diffSecurity.dataflowWindow`) that has no
|
|
388
|
+
* meaning for any of them. Caught by our own Diff Smells row.
|
|
389
|
+
*/
|
|
390
|
+
exports.DEFAULT_DATAFLOW_WINDOW = 10;
|
|
391
|
+
const CROSS_LINE_SQL_LABEL = 'Interpolated value reaches a query (SQL injection risk, cross-line)';
|
|
392
|
+
/** `@@ ... @@` — a new hunk, and the boundary taint never crosses. */
|
|
393
|
+
const DIFF_HUNK_HEADER = /^@@ /;
|
|
394
|
+
/**
|
|
395
|
+
* Whether this line hands `name` to a query sink.
|
|
396
|
+
*
|
|
397
|
+
* Built per lookup rather than cached: the set of tracked names is at most a
|
|
398
|
+
* handful per hunk, and a regex assembled from a captured identifier must not
|
|
399
|
+
* outlive the hunk that captured it.
|
|
400
|
+
*/
|
|
401
|
+
function reachesSink(code, name) {
|
|
402
|
+
return new RegExp(`\\b(?:${TAINT_SINKS})\\s*\\(\\s*${name}\\s*[,)]`).test(code);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The per-hunk taint state, kept out of {@link scanDiffPatterns} so the scan loop
|
|
406
|
+
* stays one thing: match patterns, collect findings. A tracker built with a
|
|
407
|
+
* window of `0` answers `false` to everything and records nothing, which is how
|
|
408
|
+
* the heuristic switches off without a branch at the call site.
|
|
409
|
+
*/
|
|
410
|
+
function taintTracker(window) {
|
|
411
|
+
let tainted = new Map();
|
|
412
|
+
let ordinal = 0;
|
|
413
|
+
return {
|
|
414
|
+
reset: () => {
|
|
415
|
+
tainted = new Map();
|
|
416
|
+
ordinal = 0;
|
|
417
|
+
},
|
|
418
|
+
inspect: (rawText, asCode) => {
|
|
419
|
+
if (window <= 0)
|
|
420
|
+
return false;
|
|
421
|
+
ordinal += 1;
|
|
422
|
+
// Sinks are looked for before new taints are recorded, so a line that both
|
|
423
|
+
// declares and consumes cannot report against itself — that shape is
|
|
424
|
+
// already covered by the built-in same-line rule.
|
|
425
|
+
let hit = false;
|
|
426
|
+
for (const [name, declaredAt] of tainted) {
|
|
427
|
+
if (ordinal - declaredAt > window) {
|
|
428
|
+
tainted.delete(name);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (reachesSink(asCode, name))
|
|
432
|
+
hit = true;
|
|
433
|
+
}
|
|
434
|
+
// Matched on the raw text: `stripStringLiterals` blanks the `${...}` that
|
|
435
|
+
// is the entire reason the value is suspect.
|
|
436
|
+
const declared = TAINT_DECLARATION.exec(rawText);
|
|
437
|
+
if (declared)
|
|
438
|
+
tainted.set(declared[1], ordinal);
|
|
439
|
+
return hit;
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
343
443
|
/** `+++ b/path/to/file.ts` — the header that says which file follows. */
|
|
344
444
|
const DIFF_FILE_HEADER = /^\+\+\+ b\/(.+)$/;
|
|
345
445
|
/**
|
|
@@ -368,12 +468,21 @@ function scanDiffPatterns(stdout, patterns, opts = {}) {
|
|
|
368
468
|
let inBlockComment = false;
|
|
369
469
|
// Whole files can be excluded by glob; the flag persists until the next header.
|
|
370
470
|
let skippingFile = false;
|
|
471
|
+
// Cleared at every file and hunk boundary: two unrelated hunks are not two
|
|
472
|
+
// halves of one flow.
|
|
473
|
+
const taint = taintTracker(Math.max(0, opts.dataflowWindow ?? 0));
|
|
371
474
|
for (const line of stdout.split('\n')) {
|
|
372
475
|
const header = DIFF_FILE_HEADER.exec(line);
|
|
373
476
|
if (header) {
|
|
374
477
|
syntax = mode === 'language' ? commentSyntaxFor(header[1]) : 'both';
|
|
375
478
|
inBlockComment = false;
|
|
376
479
|
skippingFile = ignorePaths.length > 0 && (0, utils_1.matchesAnyGlob)(header[1], ignorePaths);
|
|
480
|
+
taint.reset();
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
// Checked before the `+` filter below, which a hunk header does not satisfy.
|
|
484
|
+
if (DIFF_HUNK_HEADER.test(line)) {
|
|
485
|
+
taint.reset();
|
|
377
486
|
continue;
|
|
378
487
|
}
|
|
379
488
|
if (skippingFile)
|
|
@@ -393,6 +502,9 @@ function scanDiffPatterns(stdout, patterns, opts = {}) {
|
|
|
393
502
|
(bucket[label] ?? (bucket[label] = [])).push(line.slice(1).trim().substring(0, SAMPLE_MAX_LENGTH));
|
|
394
503
|
}
|
|
395
504
|
}
|
|
505
|
+
if (taint.inspect(prepared.text, asCode)) {
|
|
506
|
+
(warnings[CROSS_LINE_SQL_LABEL] ?? (warnings[CROSS_LINE_SQL_LABEL] = [])).push(line.slice(1).trim().substring(0, SAMPLE_MAX_LENGTH));
|
|
507
|
+
}
|
|
396
508
|
}
|
|
397
509
|
return { errors, warnings };
|
|
398
510
|
}
|
|
@@ -445,6 +557,32 @@ function stripBlockComments(line, open) {
|
|
|
445
557
|
return { text: out, open: inside };
|
|
446
558
|
}
|
|
447
559
|
/** Total number of findings across all labels in a bucket. */
|
|
560
|
+
/** How many findings reach the log before the rest are counted instead. */
|
|
561
|
+
const MAX_LOGGED = 10;
|
|
562
|
+
/**
|
|
563
|
+
* Log a capped sample of raw output lines under a one-line summary.
|
|
564
|
+
*
|
|
565
|
+
* The cap is the point: a linter with 400 offences must not push the rest of the
|
|
566
|
+
* report out of a CI log's scrollback, and the count that follows the sample is
|
|
567
|
+
* what keeps the truncation from reading as the whole story.
|
|
568
|
+
*
|
|
569
|
+
* Deliberately not the private `logFindings` below, which this nearly collided
|
|
570
|
+
* with. That one takes findings already grouped into labelled buckets and pairs
|
|
571
|
+
* the log with a severity; this one takes lines a tool printed and does nothing
|
|
572
|
+
* but show them, which is what a check wrapping somebody else's output needs and
|
|
573
|
+
* the only thing it can honestly claim about them.
|
|
574
|
+
*
|
|
575
|
+
* Here rather than beside its first caller because three categories want it now —
|
|
576
|
+
* the language runner, the Python type check, and Duplicate Code — and a
|
|
577
|
+
* `quality/` check importing from `languages/` to get a logger was the wrong
|
|
578
|
+
* shape (ticket 079).
|
|
579
|
+
*/
|
|
580
|
+
function logSample(name, summary, lines) {
|
|
581
|
+
utils_1.logger.warn(`[${name}]: ⚠️ ${summary}`);
|
|
582
|
+
lines.slice(0, MAX_LOGGED).forEach(line => utils_1.logger.warn(` ${line}`));
|
|
583
|
+
if (lines.length > MAX_LOGGED)
|
|
584
|
+
utils_1.logger.warn(` ... and ${lines.length - MAX_LOGGED} more`);
|
|
585
|
+
}
|
|
448
586
|
function bucketTotal(bucket) {
|
|
449
587
|
return Object.values(bucket).reduce((sum, arr) => sum + arr.length, 0);
|
|
450
588
|
}
|
|
@@ -89,6 +89,8 @@ exports.customRulesCheck = {
|
|
|
89
89
|
}
|
|
90
90
|
else if (ctx.config.run?.fullScanBehaviour !== 'skip') {
|
|
91
91
|
const files = await (0, targets_1.listTrackedFiles)(ctx, ['*'], ['.']);
|
|
92
|
+
if (files === null)
|
|
93
|
+
return (0, types_1.skip)('git unavailable');
|
|
92
94
|
await (0, findings_1.scanTrackedFiles)(ctx, files, collect);
|
|
93
95
|
}
|
|
94
96
|
// A rule with severity "error" blocks the run (fail), regardless of phase.
|
|
@@ -50,10 +50,15 @@ exports.diffSmellCheck = {
|
|
|
50
50
|
try {
|
|
51
51
|
const { ignorePaths } = ctx.config.diffSmell ?? {};
|
|
52
52
|
const found = (0, findings_1.emptyBuckets)();
|
|
53
|
-
await (0, findings_1.reviewLines)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx'], stdout => {
|
|
53
|
+
const reviewed = await (0, findings_1.reviewLines)(ctx, ['*.ts', '*.tsx', '*.js', '*.jsx'], stdout => {
|
|
54
54
|
(0, findings_1.mergeSeverityBuckets)(found, (0, findings_1.scanDiffPatterns)(stdout, exports.CODE_SMELLS, { ignorePaths }));
|
|
55
55
|
(0, findings_1.mergeSeverityBuckets)(found, (0, findings_1.scanDiffPatterns)(stdout, exports.COMMENT_SMELLS, { comments: 'off', ignorePaths }));
|
|
56
56
|
});
|
|
57
|
+
// An unreadable file list is not "no smells" (ticket 072). Checked before
|
|
58
|
+
// the empty-bucket branch below, because that branch is what turned it into
|
|
59
|
+
// a green row.
|
|
60
|
+
if (!reviewed)
|
|
61
|
+
return (0, types_1.skip)('git unavailable');
|
|
57
62
|
// Every smell here is declared `warn`, so nothing reaches the error bucket.
|
|
58
63
|
if ((0, findings_1.bucketTotal)(found.warnings) === 0)
|
|
59
64
|
return (0, types_1.pass)();
|
|
@@ -63,12 +63,18 @@ exports.leftoverDebugCheck = {
|
|
|
63
63
|
try {
|
|
64
64
|
const findings = {};
|
|
65
65
|
const comments = ctx.config.leftoverDebug?.commentHandling ?? 'language';
|
|
66
|
+
// One failed language set fails the check — see todo-fixme, same reason.
|
|
67
|
+
let reviewed = true;
|
|
66
68
|
if (ctx.languages.includes('python')) {
|
|
67
|
-
|
|
69
|
+
reviewed =
|
|
70
|
+
(await (0, findings_1.reviewLines)(ctx, ['*.py'], out => collect(out, PYTHON_PATTERNS, findings, comments))) && reviewed;
|
|
68
71
|
}
|
|
69
72
|
if (ctx.languages.includes('kotlin')) {
|
|
70
|
-
|
|
73
|
+
reviewed =
|
|
74
|
+
(await (0, findings_1.reviewLines)(ctx, ['*.kt', '*.kts', '*.java'], out => collect(out, KOTLIN_PATTERNS, findings, comments))) && reviewed;
|
|
71
75
|
}
|
|
76
|
+
if (!reviewed)
|
|
77
|
+
return (0, types_1.skip)('git unavailable');
|
|
72
78
|
if ((0, findings_1.bucketTotal)(findings) === 0)
|
|
73
79
|
return (0, types_1.pass)();
|
|
74
80
|
// Advisory by default; a repo may promote to a gate via
|