pr-checkmate 2.1.0 → 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 -267
- package/cspell.json +155 -133
- package/dist/config/constants.d.ts +13 -1
- package/dist/config/constants.js +20 -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 +2 -2
- package/dist/core/checks/dependencies/grype-scan.js +2 -1
- package/dist/core/checks/dependencies/license-check.js +1 -6
- package/dist/core/checks/dependencies/npm-audit.js +1 -1
- package/dist/core/checks/dependencies/outdated-deps.js +1 -6
- 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 +86 -28
- 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.d.ts +2 -2
- package/dist/core/install/doctor.js +17 -16
- 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/reporters.js +8 -2
- 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 +26 -2
- package/dist/core/types.js +28 -0
- package/dist/init.js +21 -1
- package/dist/interfaces/index.d.ts +1 -0
- package/dist/utils/detect-language.js +11 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/package-version.d.ts +4 -0
- package/dist/utils/package-version.js +41 -0
- package/dist/utils/resolve-bin.d.ts +56 -6
- package/dist/utils/resolve-bin.js +136 -26
- package/dist/utils/update-notifier.js +5 -7
- 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
|
@@ -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
|
|
@@ -8,6 +8,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const utils_1 = require("../../../utils");
|
|
10
10
|
const execa_1 = require("../../../utils/execa");
|
|
11
|
+
const targets_1 = require("../../targets");
|
|
11
12
|
const types_1 = require("../../types");
|
|
12
13
|
const findings_1 = require("../findings");
|
|
13
14
|
/**
|
|
@@ -73,6 +74,10 @@ exports.lockfileDriftCheck = {
|
|
|
73
74
|
// Raw name-only diff — manifests/locks live at the repo root, outside any
|
|
74
75
|
// configured `sourcePath`, so this deliberately does NOT scope (same as
|
|
75
76
|
// the Sensitive Files guard). Scoping would hide the very files we check.
|
|
77
|
+
//
|
|
78
|
+
// `excludePaths` is still applied, below: not scoping is an argument about
|
|
79
|
+
// WHERE these files live, not a licence to ignore a client who named one
|
|
80
|
+
// (ticket 080). A vendored sub-project's lockfile is a real case.
|
|
76
81
|
const { stdout } = await (0, execa_1.execa)('git', [
|
|
77
82
|
'diff',
|
|
78
83
|
'--name-only',
|
|
@@ -80,7 +85,7 @@ exports.lockfileDriftCheck = {
|
|
|
80
85
|
ctx.baseSha,
|
|
81
86
|
ctx.headSha ?? 'HEAD',
|
|
82
87
|
], { cwd: ctx.cwd });
|
|
83
|
-
const changed = new Set(stdout.split('\n').filter(Boolean));
|
|
88
|
+
const changed = new Set((0, targets_1.excludeClientPaths)(stdout.split('\n').filter(Boolean), ctx.cwd));
|
|
84
89
|
const warnings = [];
|
|
85
90
|
for (const changedPath of changed) {
|
|
86
91
|
const pair = LOCK_PAIRS.find(p => p.manifest === node_path_1.default.basename(changedPath));
|
|
@@ -82,7 +82,7 @@ exports.mergeConflictCheck = {
|
|
|
82
82
|
// client's `ignoreDirs` and `sourcePath`), then diff only those files for
|
|
83
83
|
// content. `['*']` matches every path since conflicts are language-agnostic;
|
|
84
84
|
// scoping still keeps build/vendor/report dirs out, like every other check.
|
|
85
|
-
const files = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*'], (0, config_1.getSourcePaths)());
|
|
85
|
+
const files = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*'], (0, config_1.getSourcePaths)(ctx.cwd));
|
|
86
86
|
if (files === null)
|
|
87
87
|
return (0, types_1.skip)('git unavailable');
|
|
88
88
|
if (files.length === 0)
|
|
@@ -45,7 +45,7 @@ exports.missingTestsCheck = {
|
|
|
45
45
|
async run(ctx) {
|
|
46
46
|
// Scope the same way lint does — a client's `sourcePath` and `ignoreDirs`
|
|
47
47
|
// both apply, so generated/out-of-scope dirs never count as "new source".
|
|
48
|
-
const scopedList = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*'], (0, config_1.getSourcePaths)());
|
|
48
|
+
const scopedList = await (0, targets_1.resolveScopedTargetFiles)(ctx, ['*'], (0, config_1.getSourcePaths)(ctx.cwd));
|
|
49
49
|
if (scopedList === null)
|
|
50
50
|
return (0, types_1.skip)('git unavailable');
|
|
51
51
|
const scoped = new Set(scopedList);
|
|
@@ -51,14 +51,22 @@ exports.todoFixmeCheck = {
|
|
|
51
51
|
const keywords = cfg?.keywords ?? DEFAULT_KEYWORDS;
|
|
52
52
|
const requireTicket = cfg?.requireTicket ?? false;
|
|
53
53
|
const findings = {};
|
|
54
|
+
// One failed language set fails the check: a partial answer reported as a
|
|
55
|
+
// pass says the repository is clean when half of it was never read (072).
|
|
56
|
+
let reviewed = true;
|
|
54
57
|
if (ctx.languages.includes('python')) {
|
|
55
58
|
const pattern = buildPattern('#', keywords);
|
|
56
|
-
|
|
59
|
+
reviewed =
|
|
60
|
+
(await (0, findings_1.reviewLines)(ctx, ['*.py'], out => scan(out, pattern, requireTicket, findings))) &&
|
|
61
|
+
reviewed;
|
|
57
62
|
}
|
|
58
63
|
if (ctx.languages.includes('kotlin')) {
|
|
59
64
|
const ktPattern = buildPattern('//|/\\*', keywords);
|
|
60
|
-
|
|
65
|
+
reviewed =
|
|
66
|
+
(await (0, findings_1.reviewLines)(ctx, ['*.kt', '*.kts', '*.java'], out => scan(out, ktPattern, requireTicket, findings))) && reviewed;
|
|
61
67
|
}
|
|
68
|
+
if (!reviewed)
|
|
69
|
+
return (0, types_1.skip)('git unavailable');
|
|
62
70
|
if ((0, findings_1.bucketTotal)(findings) === 0)
|
|
63
71
|
return (0, types_1.pass)();
|
|
64
72
|
return (0, findings_1.reportCountedFindings)(findings, ctx, {
|
|
@@ -6,8 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.cppFormatCheck = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const config_1 = require("../../../config");
|
|
9
10
|
const utils_1 = require("../../../utils");
|
|
10
11
|
const execa_1 = require("../../../utils/execa");
|
|
12
|
+
const targets_1 = require("../../targets");
|
|
11
13
|
const types_1 = require("../../types");
|
|
12
14
|
/**
|
|
13
15
|
* C/C++ source & header extensions clang-format understands. `.h` is intentionally
|
|
@@ -27,46 +29,6 @@ function resolveStyleArg(ctx) {
|
|
|
27
29
|
const style = ctx.config.cpp?.clangFormat?.style ?? DEFAULT_STYLE;
|
|
28
30
|
return `--style=${style}`;
|
|
29
31
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Returns the C/C++ files to check. Uses the PR diff range when available
|
|
32
|
-
* (delta mode), else scans all tracked C/C++ files. Returns an empty array
|
|
33
|
-
* when there is nothing to check, `null` when git is unusable.
|
|
34
|
-
*/
|
|
35
|
-
async function resolveTargets(ctx) {
|
|
36
|
-
try {
|
|
37
|
-
if (ctx.baseSha) {
|
|
38
|
-
const { stdout } = await (0, execa_1.execa)('git', [
|
|
39
|
-
'diff',
|
|
40
|
-
'--name-only',
|
|
41
|
-
'--diff-filter=ACMR',
|
|
42
|
-
ctx.baseSha,
|
|
43
|
-
ctx.headSha ?? 'HEAD',
|
|
44
|
-
'--',
|
|
45
|
-
...CPP_GLOBS,
|
|
46
|
-
], { cwd: ctx.cwd, reject: false });
|
|
47
|
-
const files = stdout
|
|
48
|
-
.split('\n')
|
|
49
|
-
.map(f => f.trim())
|
|
50
|
-
.filter(Boolean);
|
|
51
|
-
utils_1.logger.info(`[C++ Format]: 📂 Delta mode: ${files.length} changed C/C++ file(s)`);
|
|
52
|
-
return files;
|
|
53
|
-
}
|
|
54
|
-
const { stdout } = await (0, execa_1.execa)('git', ['ls-files', ...CPP_GLOBS], {
|
|
55
|
-
cwd: ctx.cwd,
|
|
56
|
-
reject: false,
|
|
57
|
-
});
|
|
58
|
-
const files = stdout
|
|
59
|
-
.split('\n')
|
|
60
|
-
.map(f => f.trim())
|
|
61
|
-
.filter(Boolean);
|
|
62
|
-
utils_1.logger.info(`[C++ Format]: Full scan: ${files.length} tracked C/C++ file(s)`);
|
|
63
|
-
return files;
|
|
64
|
-
}
|
|
65
|
-
catch (err) {
|
|
66
|
-
utils_1.logger.warn(`[C++ Format]: ⚠️ Could not list C/C++ files\n${err}`);
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
32
|
/** Counts distinct files flagged in clang-format's `--dry-run --Werror` diagnostics. */
|
|
71
33
|
function countOffendingFiles(stderr) {
|
|
72
34
|
const files = new Set();
|
|
@@ -89,7 +51,14 @@ exports.cppFormatCheck = {
|
|
|
89
51
|
return ctx.config.cpp?.enabled !== false && ctx.languages.includes('cpp');
|
|
90
52
|
},
|
|
91
53
|
async run(ctx) {
|
|
92
|
-
|
|
54
|
+
// Through `targets.ts`, not `git` directly. This check used to run its own
|
|
55
|
+
// `git ls-files`, which is the one mistake the authoring guide exists to
|
|
56
|
+
// prevent: it inherits neither `ignoreDirs` nor `sourcePath`. On a real C++
|
|
57
|
+
// repository that meant 403 files including a vendored copy of OpenCV — and
|
|
58
|
+
// in `write` mode clang-format was rewriting third-party sources the client
|
|
59
|
+
// does not own. It also crashed the WASM formatter, which is how this was
|
|
60
|
+
// found.
|
|
61
|
+
const targets = await (0, targets_1.resolveScopedTargetFiles)(ctx, CPP_GLOBS, (0, config_1.getSourcePaths)(ctx.cwd));
|
|
93
62
|
if (targets === null)
|
|
94
63
|
return (0, types_1.skip)('git unavailable');
|
|
95
64
|
if (targets.length === 0)
|
|
@@ -112,7 +81,15 @@ exports.cppFormatCheck = {
|
|
|
112
81
|
});
|
|
113
82
|
if (fix.exitCode !== 0) {
|
|
114
83
|
utils_1.logger.warn(`[C++ Format]: ⚠️ clang-format -i failed\n${fix.stderr}`);
|
|
115
|
-
|
|
84
|
+
// A WASM runtime error is the formatter giving up on a file, not a verdict
|
|
85
|
+
// about the code — and `-i` means whatever it managed before the crash is
|
|
86
|
+
// already written, so the tree is half-formatted. Saying which it was
|
|
87
|
+
// matters: "failed to rewrite files" sent people looking for a style
|
|
88
|
+
// problem that does not exist.
|
|
89
|
+
const crashed = /RuntimeError|memory access out of bounds|abort\(/.test(fix.stderr);
|
|
90
|
+
return (0, types_1.warn)(crashed
|
|
91
|
+
? 'clang-format crashed on one of these files — some may already have been rewritten'
|
|
92
|
+
: `clang-format could not rewrite the files (exit ${fix.exitCode ?? 'killed'})`);
|
|
116
93
|
}
|
|
117
94
|
utils_1.logger.info(`[C++ Format]: ✅ Reformatted ${offending} C/C++ file(s)`);
|
|
118
95
|
return (0, types_1.pass)(`reformatted ${offending} file(s)`);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every check that runs a linter or formatter we do not ship, as a table.
|
|
3
|
+
*
|
|
4
|
+
* The engine is in `external-runner.ts`; this file is only the data. What is
|
|
5
|
+
* genuinely per-language turns out to be four things — the binary, its
|
|
6
|
+
* arguments, the files it looks at, and how to read its output — and everything
|
|
7
|
+
* else was the same in all eleven of the modules this replaces.
|
|
8
|
+
*
|
|
9
|
+
* Keeping the entries together rather than one per file is the point: it is now
|
|
10
|
+
* possible to read what nine checks do at once, and a divergence between two of
|
|
11
|
+
* them is visible instead of two directories apart. Adding a tenth is an entry,
|
|
12
|
+
* not a file.
|
|
13
|
+
*
|
|
14
|
+
* The user-facing documentation stays per check, in
|
|
15
|
+
* `documentation/checks/languages/`, because that is organised around the reader's
|
|
16
|
+
* question ("how do I configure ktlint") rather than around ours.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Lints changed Kotlin files with ktlint (style and formatting both, so it
|
|
20
|
+
* doubles as the Kotlin format signal). Uses the repo's own `.editorconfig`
|
|
21
|
+
* when present.
|
|
22
|
+
*/
|
|
23
|
+
export declare const kotlinLintCheck: import("../..").Check;
|
|
24
|
+
/**
|
|
25
|
+
* Lints Swift with SwiftLint, which needs SourceKit and therefore a macOS
|
|
26
|
+
* runner. Runs over the directory rather than a file list — SwiftLint resolves
|
|
27
|
+
* its own `.swiftlint.yml` scope, and a file list would bypass the excludes a
|
|
28
|
+
* repo wrote there — so the resolved targets only decide whether it runs at all.
|
|
29
|
+
*/
|
|
30
|
+
export declare const swiftLintCheck: import("../..").Check;
|
|
31
|
+
/**
|
|
32
|
+
* Lints changed Ruby files with RuboCop, which covers layout as well as style,
|
|
33
|
+
* so it doubles as the Ruby format signal. Uses the repo's own `.rubocop.yml`
|
|
34
|
+
* when present.
|
|
35
|
+
*/
|
|
36
|
+
export declare const rubyLintCheck: import("../..").Check;
|
|
37
|
+
/**
|
|
38
|
+
* Lints changed shell scripts with ShellCheck. `-f gcc` gives one line per
|
|
39
|
+
* finding ending in an `[SC…]` code, which both a human and the count below can
|
|
40
|
+
* read directly.
|
|
41
|
+
*
|
|
42
|
+
* No language gate: shell is not something detection reports, and a `deploy.sh`
|
|
43
|
+
* in a Python repository is exactly the file this check exists for.
|
|
44
|
+
*/
|
|
45
|
+
export declare const shellcheckCheck: import("../..").Check;
|
|
46
|
+
/**
|
|
47
|
+
* Vets changed Go code with `go vet ./...`, which needs the Go toolchain (the
|
|
48
|
+
* formatter next door no longer does — see `go-format.ts`). `go vet` operates
|
|
49
|
+
* per package, so it runs over the module rather than a file list; the delta
|
|
50
|
+
* only decides whether it runs.
|
|
51
|
+
*/
|
|
52
|
+
export declare const goLintCheck: import("../..").Check;
|
|
53
|
+
/**
|
|
54
|
+
* Lints changed Rust with Clippy. It compiles the crate, so it runs over the
|
|
55
|
+
* crate rather than a file list; the delta only decides whether it runs.
|
|
56
|
+
*/
|
|
57
|
+
export declare const rustLintCheck: import("../..").Check;
|
|
58
|
+
/**
|
|
59
|
+
* Formats changed Rust with `cargo fmt`, not a bare `rustfmt`: cargo reads the
|
|
60
|
+
* crate's edition from `Cargo.toml`, and rustfmt invoked directly does not —
|
|
61
|
+
* which silently formats 2015-edition code by 2024 rules.
|
|
62
|
+
*/
|
|
63
|
+
export declare const rustFormatCheck: import("../..").Check;
|
|
64
|
+
/**
|
|
65
|
+
* Verifies C# formatting with `dotnet format`, which also runs the project's
|
|
66
|
+
* configured analyzers and style rules. It operates on the project or solution
|
|
67
|
+
* (and expects a prior `dotnet restore`), so it runs over the whole project; the
|
|
68
|
+
* delta only decides whether it runs.
|
|
69
|
+
*/
|
|
70
|
+
export declare const csharpFormatCheck: import("../..").Check;
|
|
71
|
+
/**
|
|
72
|
+
* Formats changed PHP with PHP-CS-Fixer, using the repo's own
|
|
73
|
+
* `.php-cs-fixer.php` when present. `--using-cache=no` because the cache keys on
|
|
74
|
+
* absolute paths, and a CI checkout directory that changes between runs makes a
|
|
75
|
+
* stale cache report files as clean that were never examined.
|
|
76
|
+
*/
|
|
77
|
+
export declare const phpFormatCheck: import("../..").Check;
|