@geonosis/lint-parity 2.1.0 → 2.3.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 +3 -0
- package/dist/{chunk-D7OGKQ2C.js → chunk-JPGZG2HA.js} +43 -10
- package/dist/index.d.ts +15 -9
- package/dist/index.js +5 -3
- package/dist/parity-cli.js +11 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @geonosis/lint-parity
|
|
2
2
|
|
|
3
|
+
Through the front door: `geonosis lint-parity` — the metapackage pins this and every other kit tool
|
|
4
|
+
at ONE version, and passes the exit code through unchanged.
|
|
5
|
+
|
|
3
6
|
A repo does not delete its vendored copy of a lint plugin because the version numbers match. It
|
|
4
7
|
deletes it when the **findings** match: same tree, same rule ids, two configs, and a diff.
|
|
5
8
|
`geonosis-lint-parity` is that diff — and, before it, the answer to whether either config's rules
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/envelope.ts
|
|
2
2
|
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
-
import { dirname, join } from "path";
|
|
3
|
+
import { dirname, join, resolve } from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
var ENVELOPES_DIR = ".geonosis/envelopes";
|
|
6
6
|
var envelopePath = (root, tool) => join(root, ENVELOPES_DIR, `${tool}.json`);
|
|
@@ -12,6 +12,14 @@ var UnbalancedEnvelope = class extends Error {
|
|
|
12
12
|
};
|
|
13
13
|
var isCount = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
14
14
|
var unbalancedMessage = (envelope, next) => `${envelope.tool}: considered ${envelope.considered} but accounts for ${envelope.read + envelope.refused.length + envelope.excused.length} \u2014 ${envelope.read} read + ${envelope.refused.length} refused + ${envelope.excused.length} excused. A run that has lost count of its own inputs cannot say what it measured, so no verdict was rendered and no envelope was written. Next: ${next}`;
|
|
15
|
+
var FORBIDDEN_ROOT = "GEONOSIS_ENVELOPES_FORBIDDEN_ROOT";
|
|
16
|
+
var refuseForbiddenRoot = (root) => {
|
|
17
|
+
const forbidden = process.env[FORBIDDEN_ROOT];
|
|
18
|
+
if (forbidden === void 0 || resolve(forbidden) !== resolve(root)) return;
|
|
19
|
+
throw new UnbalancedEnvelope(
|
|
20
|
+
`${root} is off limits to envelope writers in this process (${FORBIDDEN_ROOT}) \u2014 a run that writes one into a shared root races every other run reading it, and leaves a file the next one takes for real. Point this at a scratch root of its own: tooling/scratch-dir.ts.`
|
|
21
|
+
);
|
|
22
|
+
};
|
|
15
23
|
var writeEnvelope = ({
|
|
16
24
|
envelope,
|
|
17
25
|
next,
|
|
@@ -35,6 +43,7 @@ var writeEnvelope = ({
|
|
|
35
43
|
if (envelope.considered !== envelope.read + envelope.refused.length + envelope.excused.length) {
|
|
36
44
|
throw new UnbalancedEnvelope(unbalancedMessage(envelope, next));
|
|
37
45
|
}
|
|
46
|
+
refuseForbiddenRoot(root);
|
|
38
47
|
const at = envelopePath(root, envelope.tool);
|
|
39
48
|
mkdirSync(dirname(at), { recursive: true });
|
|
40
49
|
writeFileSync(at, `${JSON.stringify(envelope, void 0, 2)}
|
|
@@ -94,7 +103,7 @@ import {
|
|
|
94
103
|
writeFileSync as writeFileSync2
|
|
95
104
|
} from "fs";
|
|
96
105
|
import { tmpdir } from "os";
|
|
97
|
-
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
106
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
98
107
|
var ANSI = /\[[0-9;]*m/g;
|
|
99
108
|
var SUMMARY_COUNT = /^(\d+) problems?$/m;
|
|
100
109
|
var RULE_IN_LINE = /\[(?:Error|Warning)\/([^\]]+)\]$/;
|
|
@@ -103,7 +112,7 @@ var FINDING_PLACE = /^(\S[^\n]*?:\d+:\d+): .*\[(?:Error|Warning)\/([^\]\n]+)\]$/
|
|
|
103
112
|
var resolveOxlint = (from) => {
|
|
104
113
|
let dir = from;
|
|
105
114
|
for (; ; ) {
|
|
106
|
-
const bin =
|
|
115
|
+
const bin = resolve2(dir, "node_modules/.bin/oxlint");
|
|
107
116
|
if (existsSync(bin)) return bin;
|
|
108
117
|
const parent = dirname2(dir);
|
|
109
118
|
if (parent === dir) return "oxlint";
|
|
@@ -117,6 +126,8 @@ var ParityError = class extends Error {
|
|
|
117
126
|
}
|
|
118
127
|
};
|
|
119
128
|
var normaliseFindings = (output, cwd) => output.replaceAll(ANSI, "").split("\n").map((line) => line.trim().replace(`${cwd}/`, "").replace(/^\.\//, "")).filter((line) => UNIX_FINDING.test(line)).toSorted();
|
|
129
|
+
var REFUSED_CONFIG = /Failed to parse oxlint configuration file|Failed to setup JS plugin options/;
|
|
130
|
+
var refusalIn = (output) => (/^\s*(?:x|Error:)\s*(.+)$/m.exec(output)?.[1] ?? output.trim().split("\n")[0] ?? "").trim();
|
|
120
131
|
var readRun = ({ code, output }, cwd, side) => {
|
|
121
132
|
const findings = normaliseFindings(output, cwd);
|
|
122
133
|
const summary = SUMMARY_COUNT.exec(output);
|
|
@@ -127,6 +138,11 @@ ${output.trim()}`);
|
|
|
127
138
|
if (summary?.[1] !== void 0 && Number(summary[1]) !== findings.length) {
|
|
128
139
|
return refuse(`oxlint says ${summary[1]} problems, ${findings.length} lines parsed as findings`);
|
|
129
140
|
}
|
|
141
|
+
if (REFUSED_CONFIG.test(output)) {
|
|
142
|
+
return refuse(
|
|
143
|
+
`oxlint refused to parse this config and linted nothing \u2014 ${refusalIn(output)}. That is not a run with no findings. For a corpus comparison, config A is the BASE COMMIT's own corpus config against the BASE build, never HEAD's config against an older build: see docs/releasing.md.`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
130
146
|
if (findings.length === 0 && code !== 0) {
|
|
131
147
|
return refuse(`oxlint exited ${code} without reporting a single finding`);
|
|
132
148
|
}
|
|
@@ -239,15 +255,22 @@ var formatSummary = (parity) => {
|
|
|
239
255
|
changed
|
|
240
256
|
].join("\n");
|
|
241
257
|
};
|
|
242
|
-
var
|
|
258
|
+
var OFF = /* @__PURE__ */ new Set(["off", 0]);
|
|
259
|
+
var severityOf = (level) => Array.isArray(level) ? level[0] : level;
|
|
260
|
+
var layersOf = (config) => {
|
|
243
261
|
const parsed = JSON.parse(readFileSync2(config, "utf8"));
|
|
244
|
-
return [
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
262
|
+
return [parsed.rules ?? {}, ...(parsed.overrides ?? []).map((one) => one.rules ?? {})];
|
|
263
|
+
};
|
|
264
|
+
var namedBy = (config) => {
|
|
265
|
+
const layers = layersOf(config);
|
|
266
|
+
const named = [...new Set(layers.flatMap((one) => Object.keys(one)))];
|
|
267
|
+
const enabled = named.filter(
|
|
268
|
+
(rule) => layers.some((one) => rule in one && !OFF.has(severityOf(one[rule])))
|
|
269
|
+
);
|
|
270
|
+
return { enabled, off: named.filter((rule) => !enabled.includes(rule)) };
|
|
250
271
|
};
|
|
272
|
+
var rulesNamedBy = (config) => namedBy(config).enabled;
|
|
273
|
+
var rulesTurnedOffBy = (config) => namedBy(config).off;
|
|
251
274
|
var ruleIdOf = (line) => {
|
|
252
275
|
const token = RULE_IN_LINE.exec(line)?.[1];
|
|
253
276
|
if (token === void 0) return UNATTRIBUTED;
|
|
@@ -333,6 +356,7 @@ var corpusOf = ({
|
|
|
333
356
|
rmSync(root, { force: true, recursive: true });
|
|
334
357
|
}
|
|
335
358
|
const configured = [.../* @__PURE__ */ new Set([...rulesNamedBy(configA), ...rulesNamedBy(configB)])].toSorted();
|
|
359
|
+
const off = [.../* @__PURE__ */ new Set([...rulesTurnedOffBy(configA), ...rulesTurnedOffBy(configB)])].filter((rule) => !configured.includes(rule)).toSorted();
|
|
336
360
|
const reach = configured.filter((rule) => exercised.has(rule)).map((rule) => {
|
|
337
361
|
const foundInA = inA.get(rule) ?? 0;
|
|
338
362
|
const foundInB = inB.get(rule) ?? 0;
|
|
@@ -350,6 +374,7 @@ var corpusOf = ({
|
|
|
350
374
|
claims,
|
|
351
375
|
claimsHeld: claims.every((one) => one.verdict === "held"),
|
|
352
376
|
neither: reach.filter((one) => !one.firedInA && !one.firedInB).map((one) => one.rule),
|
|
377
|
+
off,
|
|
353
378
|
outside: configured.filter((rule) => !exercised.has(rule)),
|
|
354
379
|
plugin: manifest.plugin,
|
|
355
380
|
reach
|
|
@@ -387,6 +412,7 @@ var formatCorpus = (report) => {
|
|
|
387
412
|
`- rules in scope of this corpus: **${report.reach.length}**`,
|
|
388
413
|
`- fired under neither config: **${report.neither.length}**`,
|
|
389
414
|
`- outside this corpus: **${report.outside.length}**`,
|
|
415
|
+
`- turned off by a config, so out of scope: **${report.off.length}**${report.off.length === 0 ? "" : ` (${report.off.map((rule) => `\`${rule}\``).join(", ")})`}`,
|
|
390
416
|
"",
|
|
391
417
|
report.neither.length === 0 ? "**PASS** \u2014 every in-scope rule has at least one corpus file that fires it." : `**FAIL** \u2014 these rules fire nowhere in the corpus, so nothing shows they reach anything:
|
|
392
418
|
${report.neither.map((rule) => `- \`${rule}\``).join("\n")}`,
|
|
@@ -416,9 +442,14 @@ var parseParityArgs = (argv) => {
|
|
|
416
442
|
const read = {};
|
|
417
443
|
let corpus;
|
|
418
444
|
let expectChanged;
|
|
445
|
+
let json = false;
|
|
419
446
|
for (let index = 0; index < flags.length; index += 1) {
|
|
420
447
|
const flag = flags[index];
|
|
421
448
|
const value = flags[index + 1];
|
|
449
|
+
if (flag === "--json") {
|
|
450
|
+
json = true;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
422
453
|
if (flag === CORPUS_FLAG) {
|
|
423
454
|
if (value === void 0 || value.startsWith("--")) {
|
|
424
455
|
throw new Error(
|
|
@@ -457,6 +488,7 @@ var parseParityArgs = (argv) => {
|
|
|
457
488
|
configB: read.configB,
|
|
458
489
|
...corpus === void 0 ? {} : { corpus },
|
|
459
490
|
...expectChanged === void 0 ? {} : { expectChanged },
|
|
491
|
+
json,
|
|
460
492
|
...read.out === void 0 ? {} : { out: read.out },
|
|
461
493
|
...read.oxlint === void 0 ? {} : { oxlint: read.oxlint },
|
|
462
494
|
paths: paths.length === 0 ? ["."] : paths
|
|
@@ -552,6 +584,7 @@ export {
|
|
|
552
584
|
compareFindings,
|
|
553
585
|
formatSummary,
|
|
554
586
|
rulesNamedBy,
|
|
587
|
+
rulesTurnedOffBy,
|
|
555
588
|
ruleIdOf,
|
|
556
589
|
MANIFEST_FILE,
|
|
557
590
|
manifestOf,
|
package/dist/index.d.ts
CHANGED
|
@@ -47,13 +47,6 @@ type Parity = {
|
|
|
47
47
|
* thread-scheduling detail, not a finding.
|
|
48
48
|
*/
|
|
49
49
|
declare const normaliseFindings: (output: string, cwd: string) => string[];
|
|
50
|
-
/**
|
|
51
|
-
* The findings of one run, or a refusal to guess. oxlint exits 1 both when it reported errors and
|
|
52
|
-
* when it never linted a file at all — a rule name it does not know, options a rule has no schema
|
|
53
|
-
* for, a plugin it could not load. All three print a paragraph and no findings, and "0 findings" on
|
|
54
|
-
* one side of a parity run reads as agreement with the other. So: a summary counting findings no
|
|
55
|
-
* line accounts for, or a non-zero exit with nothing readable on it, is an error, not a count.
|
|
56
|
-
*/
|
|
57
50
|
declare const readRun: ({ code, output }: {
|
|
58
51
|
code: number;
|
|
59
52
|
output: string;
|
|
@@ -100,20 +93,31 @@ type CorpusReport = {
|
|
|
100
93
|
* about anybody else's rule, and a verdict on one would be a guess.
|
|
101
94
|
*/
|
|
102
95
|
outside: string[];
|
|
96
|
+
/**
|
|
97
|
+
* Rules a config names and turns off. Out of scope — a rule that does not run cannot be shown to
|
|
98
|
+
* reach anything — and counted, so "in scope" is never a number with a silent subtraction in it.
|
|
99
|
+
*/
|
|
100
|
+
off: string[];
|
|
103
101
|
/** The plugin the corpus declared itself to speak for. */
|
|
104
102
|
plugin: string;
|
|
105
103
|
reach: RuleReach[];
|
|
106
104
|
};
|
|
107
105
|
/**
|
|
108
|
-
* The rule ids an oxlint config
|
|
106
|
+
* The rule ids an oxlint config ENABLES, exactly as it wrote them — plugin prefix included, and
|
|
109
107
|
* from `overrides[]` as well as from `rules`.
|
|
110
108
|
*
|
|
111
109
|
* A config's base block is only its first layer: an overrides entry may name a rule the base block
|
|
112
110
|
* never mentions, and oxlint runs it on every file that entry's globs claim. Reading `rules` alone
|
|
113
111
|
* left such a rule out of the reach scope entirely — it fired, and the report about which rules
|
|
114
112
|
* reach anything did not have a row for it.
|
|
113
|
+
*
|
|
114
|
+
* Severity is read (#16). A rule set to `"off"` does not run, so a corpus that never fires it has
|
|
115
|
+
* shown nothing about it: three rules read as "fire nowhere" under a consumer's config where the
|
|
116
|
+
* doctor, which does read severity, said one.
|
|
115
117
|
*/
|
|
116
118
|
declare const rulesNamedBy: (config: string) => string[];
|
|
119
|
+
/** The rules a config names and turns off — out of the reach scope, and counted rather than hidden. */
|
|
120
|
+
declare const rulesTurnedOffBy: (config: string) => string[];
|
|
117
121
|
/**
|
|
118
122
|
* The plugin-qualified id behind a finding's `plugin(rule)` token, in the `plugin/rule` shape a
|
|
119
123
|
* config names it by. Prefix included on purpose: `typescript(no-unused-vars)` is a different rule
|
|
@@ -176,6 +180,8 @@ type ParityArgs = {
|
|
|
176
180
|
* an empty list is not expressible, because a claim about no rules is not a claim.
|
|
177
181
|
*/
|
|
178
182
|
expectChanged?: string[];
|
|
183
|
+
/** Print the report as a document rather than as a summary a caller has to read by regex. */
|
|
184
|
+
json: boolean;
|
|
179
185
|
out?: string;
|
|
180
186
|
oxlint?: string;
|
|
181
187
|
paths: string[];
|
|
@@ -219,4 +225,4 @@ declare const parityOf: ({ configA, configB, cwd, out, oxlint, paths, }: {
|
|
|
219
225
|
paths: string[];
|
|
220
226
|
}) => Parity;
|
|
221
227
|
|
|
222
|
-
export { type CorpusClaim, type CorpusManifest, type CorpusReport, type FileCensus, MANIFEST_FILE, type MessageChange, type Parity, type ParityArgs, ParityError, type RuleReach, type RuleTally, compareFindings, corpusOf, fileCensus, filesLintedBy, formatCorpus, formatSummary, manifestOf, normaliseFindings, parityOf, parseParityArgs, readManifest, readRun, resolveOxlint, ruleIdOf, rulesNamedBy };
|
|
228
|
+
export { type CorpusClaim, type CorpusManifest, type CorpusReport, type FileCensus, MANIFEST_FILE, type MessageChange, type Parity, type ParityArgs, ParityError, type RuleReach, type RuleTally, compareFindings, corpusOf, fileCensus, filesLintedBy, formatCorpus, formatSummary, manifestOf, normaliseFindings, parityOf, parseParityArgs, readManifest, readRun, resolveOxlint, ruleIdOf, rulesNamedBy, rulesTurnedOffBy };
|
package/dist/index.js
CHANGED
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
readRun,
|
|
16
16
|
resolveOxlint,
|
|
17
17
|
ruleIdOf,
|
|
18
|
-
rulesNamedBy
|
|
19
|
-
|
|
18
|
+
rulesNamedBy,
|
|
19
|
+
rulesTurnedOffBy
|
|
20
|
+
} from "./chunk-JPGZG2HA.js";
|
|
20
21
|
export {
|
|
21
22
|
MANIFEST_FILE,
|
|
22
23
|
ParityError,
|
|
@@ -34,5 +35,6 @@ export {
|
|
|
34
35
|
readRun,
|
|
35
36
|
resolveOxlint,
|
|
36
37
|
ruleIdOf,
|
|
37
|
-
rulesNamedBy
|
|
38
|
+
rulesNamedBy,
|
|
39
|
+
rulesTurnedOffBy
|
|
38
40
|
};
|
package/dist/parity-cli.js
CHANGED
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
parseParityArgs,
|
|
10
10
|
resolveOxlint,
|
|
11
11
|
writeEnvelope
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JPGZG2HA.js";
|
|
13
13
|
|
|
14
14
|
// src/parity-cli.ts
|
|
15
15
|
import { mkdirSync, writeFileSync } from "fs";
|
|
16
16
|
import { join, resolve } from "path";
|
|
17
|
-
var USAGE = `geonosis-lint-parity --a <config-a.json> --b <config-b.json> [--oxlint <path>] [--out <dir>] -- <paths\u2026>
|
|
18
|
-
geonosis-lint-parity --corpus <dir> --a <config-a.json> --b <config-b.json> [--out <dir>]
|
|
17
|
+
var USAGE = `geonosis-lint-parity --a <config-a.json> --b <config-b.json> [--oxlint <path>] [--out <dir>] [--json] -- <paths\u2026>
|
|
18
|
+
geonosis-lint-parity --corpus <dir> --a <config-a.json> --b <config-b.json> [--out <dir>] [--json]
|
|
19
19
|
[--expect-changed <rule,rule,\u2026>]
|
|
20
20
|
|
|
21
21
|
Runs oxlint twice over the same paths under two configs and diffs the findings. Exits 1 when a
|
|
@@ -71,7 +71,8 @@ and it reads them as oxlint does rather than interpreting them itself.
|
|
|
71
71
|
oxlint
|
|
72
72
|
});
|
|
73
73
|
const text = formatCorpus(report);
|
|
74
|
-
process.stdout.write(`${
|
|
74
|
+
process.stdout.write(args.json ? `${JSON.stringify(report, void 0, 2)}
|
|
75
|
+
` : `${text}
|
|
75
76
|
`);
|
|
76
77
|
if (args.out !== void 0) {
|
|
77
78
|
const out = resolve(cwd, args.out);
|
|
@@ -89,7 +90,7 @@ and it reads them as oxlint does rather than interpreting them itself.
|
|
|
89
90
|
oxlint,
|
|
90
91
|
paths: args.paths
|
|
91
92
|
});
|
|
92
|
-
process.stdout.write(`${formatSummary(parity)}
|
|
93
|
+
if (!args.json) process.stdout.write(`${formatSummary(parity)}
|
|
93
94
|
`);
|
|
94
95
|
const at = writeEnvelope({
|
|
95
96
|
envelope: parityEnvelope(
|
|
@@ -100,8 +101,11 @@ and it reads them as oxlint does rather than interpreting them itself.
|
|
|
100
101
|
next: PARITY_NEXT,
|
|
101
102
|
root: cwd
|
|
102
103
|
});
|
|
103
|
-
process.stdout.write(
|
|
104
|
-
|
|
104
|
+
process.stdout.write(
|
|
105
|
+
args.json ? `${JSON.stringify(parity, void 0, 2)}
|
|
106
|
+
` : `envelope: ${at}
|
|
107
|
+
`
|
|
108
|
+
);
|
|
105
109
|
return parity.lost ? 1 : 0;
|
|
106
110
|
};
|
|
107
111
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geonosis/lint-parity",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"types": "./dist/index.d.ts",
|
|
5
5
|
"description": "Findings parity and rule reach over any two oxlint configs — the diff a fork is deleted on.",
|
|
6
6
|
"keywords": [
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@geonosis/ratchet": "2.
|
|
47
|
+
"@geonosis/ratchet": "2.3.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsup",
|