@geonosis/lint-parity 2.2.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 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
@@ -126,6 +126,8 @@ var ParityError = class extends Error {
126
126
  }
127
127
  };
128
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();
129
131
  var readRun = ({ code, output }, cwd, side) => {
130
132
  const findings = normaliseFindings(output, cwd);
131
133
  const summary = SUMMARY_COUNT.exec(output);
@@ -136,6 +138,11 @@ ${output.trim()}`);
136
138
  if (summary?.[1] !== void 0 && Number(summary[1]) !== findings.length) {
137
139
  return refuse(`oxlint says ${summary[1]} problems, ${findings.length} lines parsed as findings`);
138
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
+ }
139
146
  if (findings.length === 0 && code !== 0) {
140
147
  return refuse(`oxlint exited ${code} without reporting a single finding`);
141
148
  }
@@ -248,15 +255,22 @@ var formatSummary = (parity) => {
248
255
  changed
249
256
  ].join("\n");
250
257
  };
251
- var rulesNamedBy = (config) => {
258
+ var OFF = /* @__PURE__ */ new Set(["off", 0]);
259
+ var severityOf = (level) => Array.isArray(level) ? level[0] : level;
260
+ var layersOf = (config) => {
252
261
  const parsed = JSON.parse(readFileSync2(config, "utf8"));
253
- return [
254
- .../* @__PURE__ */ new Set([
255
- ...Object.keys(parsed.rules ?? {}),
256
- ...(parsed.overrides ?? []).flatMap((one) => Object.keys(one.rules ?? {}))
257
- ])
258
- ];
262
+ return [parsed.rules ?? {}, ...(parsed.overrides ?? []).map((one) => one.rules ?? {})];
259
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)) };
271
+ };
272
+ var rulesNamedBy = (config) => namedBy(config).enabled;
273
+ var rulesTurnedOffBy = (config) => namedBy(config).off;
260
274
  var ruleIdOf = (line) => {
261
275
  const token = RULE_IN_LINE.exec(line)?.[1];
262
276
  if (token === void 0) return UNATTRIBUTED;
@@ -342,6 +356,7 @@ var corpusOf = ({
342
356
  rmSync(root, { force: true, recursive: true });
343
357
  }
344
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();
345
360
  const reach = configured.filter((rule) => exercised.has(rule)).map((rule) => {
346
361
  const foundInA = inA.get(rule) ?? 0;
347
362
  const foundInB = inB.get(rule) ?? 0;
@@ -359,6 +374,7 @@ var corpusOf = ({
359
374
  claims,
360
375
  claimsHeld: claims.every((one) => one.verdict === "held"),
361
376
  neither: reach.filter((one) => !one.firedInA && !one.firedInB).map((one) => one.rule),
377
+ off,
362
378
  outside: configured.filter((rule) => !exercised.has(rule)),
363
379
  plugin: manifest.plugin,
364
380
  reach
@@ -396,6 +412,7 @@ var formatCorpus = (report) => {
396
412
  `- rules in scope of this corpus: **${report.reach.length}**`,
397
413
  `- fired under neither config: **${report.neither.length}**`,
398
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(", ")})`}`,
399
416
  "",
400
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:
401
418
  ${report.neither.map((rule) => `- \`${rule}\``).join("\n")}`,
@@ -425,9 +442,14 @@ var parseParityArgs = (argv) => {
425
442
  const read = {};
426
443
  let corpus;
427
444
  let expectChanged;
445
+ let json = false;
428
446
  for (let index = 0; index < flags.length; index += 1) {
429
447
  const flag = flags[index];
430
448
  const value = flags[index + 1];
449
+ if (flag === "--json") {
450
+ json = true;
451
+ continue;
452
+ }
431
453
  if (flag === CORPUS_FLAG) {
432
454
  if (value === void 0 || value.startsWith("--")) {
433
455
  throw new Error(
@@ -466,6 +488,7 @@ var parseParityArgs = (argv) => {
466
488
  configB: read.configB,
467
489
  ...corpus === void 0 ? {} : { corpus },
468
490
  ...expectChanged === void 0 ? {} : { expectChanged },
491
+ json,
469
492
  ...read.out === void 0 ? {} : { out: read.out },
470
493
  ...read.oxlint === void 0 ? {} : { oxlint: read.oxlint },
471
494
  paths: paths.length === 0 ? ["."] : paths
@@ -561,6 +584,7 @@ export {
561
584
  compareFindings,
562
585
  formatSummary,
563
586
  rulesNamedBy,
587
+ rulesTurnedOffBy,
564
588
  ruleIdOf,
565
589
  MANIFEST_FILE,
566
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 enables, exactly as it wrote them — plugin prefix included, and
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
- } from "./chunk-JNTU55OA.js";
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
  };
@@ -9,13 +9,13 @@ import {
9
9
  parseParityArgs,
10
10
  resolveOxlint,
11
11
  writeEnvelope
12
- } from "./chunk-JNTU55OA.js";
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(`${text}
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(`envelope: ${at}
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.2.0",
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.2.0"
47
+ "@geonosis/ratchet": "2.3.0"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsup",