oxlint-harness 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,16 +55,40 @@ The suppression file uses a count-based format:
55
55
 
56
56
  ## CLI Options
57
57
 
58
- | Option | Short | Description | Default |
59
- |--------|--------|-------------|---------|
60
- | `--suppressions` | `-s` | Path to suppression file | `.oxlint-suppressions.json` |
61
- | `--update` | `-u` | Update/create suppression file | `false` |
62
- | `--show-code` | | Show code snippets for files with N or fewer errors (0 to disable) | `3` |
63
- | `--fail-on-excess` | | Exit 1 if unsuppressed errors exist | `true` |
64
- | `--no-fail-on-excess` | | Don't exit 1 on unsuppressed errors | - |
65
- | `--help` | `-h` | Show help | - |
58
+ | Option | Short | Description | Default |
59
+ | --------------------- | ----- | ------------------------------------------------------------------ | --------------------------- |
60
+ | `--suppressions` | `-s` | Path to suppression file | `.oxlint-suppressions.json` |
61
+ | `--update` | `-u` | Update/create suppression file | `false` |
62
+ | `--show-code` | | Show code snippets for files with N or fewer errors (0 to disable) | `3` |
63
+ | `--fail-on-excess` | | Exit 1 if unsuppressed errors exist | `true` |
64
+ | `--no-fail-on-excess` | | Don't exit 1 on unsuppressed errors | - |
65
+ | `--help` | `-h` | Show help | - |
66
66
 
67
- All additional arguments are passed directly to oxlint.
67
+ ### Environment Variables
68
+
69
+ | Variable | Description | Equivalent Flag |
70
+ | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------- |
71
+ | `OXLINT_HARNESS_UPDATE_BULK_SUPPRESSION` | Set to `true` to update/create suppression file | `--update` |
72
+ | `OXLINT_HARNESS_TIGHTEN_BULK_SUPPRESSION` | Set to `true` to automatically remove/reduce suppressions for cleaned-up violations during non-update runs | - |
73
+
74
+ Example:
75
+
76
+ ```bash
77
+ OXLINT_HARNESS_UPDATE_BULK_SUPPRESSION=true npx oxlint-harness src/
78
+ OXLINT_HARNESS_TIGHTEN_BULK_SUPPRESSION=true npx oxlint-harness src/
79
+ ```
80
+
81
+ ### Passing Additional oxlint Flags
82
+
83
+ All additional arguments and unknown flags are passed directly to oxlint. This allows you to use any oxlint-specific options, even if they are not documented here.
84
+
85
+ For example, to enable type-aware linting:
86
+
87
+ ```bash
88
+ npx oxlint-harness --type-aware src/
89
+ ```
90
+
91
+ Any flag not recognized by oxlint-harness will be forwarded to oxlint automatically.
68
92
 
69
93
  ## Usage Examples
70
94
 
@@ -112,6 +136,7 @@ npx oxlint-harness --no-fail-on-excess src/
112
136
  ### Initial Setup
113
137
 
114
138
  1. Run with `--update` to create initial suppression file:
139
+
115
140
  ```bash
116
141
  npx oxlint-harness --update src/
117
142
  ```
@@ -129,6 +154,7 @@ npx oxlint-harness --no-fail-on-excess src/
129
154
  ### Example Output
130
155
 
131
156
  #### With Code Snippets (default for files with ≤3 errors)
157
+
132
158
  ```
133
159
  ❌ Found unsuppressed errors:
134
160
 
@@ -146,8 +172,8 @@ npx oxlint-harness --no-fail-on-excess src/
146
172
  ╰────
147
173
  help: Consider removing this declaration.
148
174
 
149
- 📝 To suppress, add to suppression file:
150
- "src/App.tsx": { "eslint(no-unused-vars)": { "count": 1 } }
175
+ 📝 To suppress, re-run with:
176
+ OXLINT_HARNESS_UPDATE_BULK_SUPPRESSION=true oxlint-harness [your-args]
151
177
 
152
178
  📊 Summary:
153
179
  • Files with issues: 1
@@ -158,17 +184,24 @@ npx oxlint-harness --no-fail-on-excess src/
158
184
  oxlint-harness --update src/
159
185
  ```
160
186
 
161
- #### Simple List (for files with many errors)
187
+ #### With Many Errors (first error shown with code snippet)
188
+
162
189
  ```
163
190
  📄 src/Components.tsx:
164
191
  ⚠️ prefer-const: 15 excess errors (expected: 10, actual: 25)
165
- • src/Components.tsx:42:12: 'data' is never reassigned
192
+
193
+ × prefer-const: 'data' is never reassigned. Use 'const' instead.
194
+ ╭─[42:12]
195
+ 42 │ let data = fetchData();
196
+ ─────────────
197
+ help: Use 'const' instead.
198
+
166
199
  • src/Components.tsx:58:8: 'config' is never reassigned
167
200
  • src/Components.tsx:74:15: 'result' is never reassigned
168
201
  ... and 12 more
169
202
 
170
- 📝 To suppress, add to suppression file:
171
- "src/Components.tsx": { "prefer-const": { "count": 25 } }
203
+ 📝 To suppress, re-run with:
204
+ OXLINT_HARNESS_UPDATE_BULK_SUPPRESSION=true oxlint-harness [your-args]
172
205
  ```
173
206
 
174
207
  ## Requirements
@@ -209,4 +242,4 @@ pnpm dev --help
209
242
 
210
243
  ## License
211
244
 
212
- MIT
245
+ MIT
package/dist/cli.js CHANGED
@@ -1,6 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  import OxlintHarness from './command.js';
3
- OxlintHarness.run().catch((error) => {
4
- console.error(error);
5
- process.exit(1);
6
- });
3
+ OxlintHarness.run();
package/dist/command.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Command } from '@oclif/core';
1
+ import { Command } from "@oclif/core";
2
2
  export default class OxlintHarness extends Command {
3
3
  static summary: string;
4
4
  static description: string;
@@ -6,9 +6,8 @@ export default class OxlintHarness extends Command {
6
6
  static flags: {
7
7
  suppressions: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
8
  update: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
- 'fail-on-excess': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
- 'show-code': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
11
- help: import("@oclif/core/interfaces").BooleanFlag<void>;
9
+ "fail-on-excess": import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ "show-code": import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
12
11
  };
13
12
  static args: {
14
13
  paths: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
package/dist/command.js CHANGED
@@ -1,10 +1,10 @@
1
- import { Command, Flags, Args } from '@oclif/core';
2
- import { OxlintRunner } from './oxlint-runner.js';
3
- import { SuppressionManager } from './suppression-manager.js';
4
- import { ErrorReporter } from './error-reporter.js';
5
- import { ColorFormatter } from './colors.js';
1
+ import { Command, Flags, Args } from "@oclif/core";
2
+ import { OxlintRunner } from "./oxlint-runner.js";
3
+ import { SuppressionManager } from "./suppression-manager.js";
4
+ import { ErrorReporter } from "./error-reporter.js";
5
+ import { ColorFormatter } from "./colors.js";
6
6
  export default class OxlintHarness extends Command {
7
- static summary = 'Run oxlint with bulk suppressions support';
7
+ static summary = "Run oxlint with bulk suppressions support";
8
8
  static description = `
9
9
  Runs oxlint with support for bulk suppressions similar to ESLint.
10
10
 
@@ -17,46 +17,141 @@ The suppression file format uses counts per rule per file:
17
17
  }
18
18
  `;
19
19
  static examples = [
20
- '<%= config.bin %> <%= command.id %> src/',
21
- '<%= config.bin %> <%= command.id %> --update src/',
22
- '<%= config.bin %> <%= command.id %> --suppressions .my-suppressions.json src/',
20
+ "<%= config.bin %> <%= command.id %> src/",
21
+ "<%= config.bin %> <%= command.id %> --update src/",
22
+ "<%= config.bin %> <%= command.id %> --suppressions .my-suppressions.json src/",
23
23
  ];
24
24
  static flags = {
25
25
  suppressions: Flags.string({
26
- char: 's',
27
- description: 'Path to suppression file',
28
- default: '.oxlint-suppressions.json',
26
+ char: "s",
27
+ description: "Path to suppression file",
28
+ default: ".oxlint-suppressions.json",
29
29
  }),
30
30
  update: Flags.boolean({
31
- char: 'u',
32
- description: 'Update/create suppression file with current error counts',
31
+ char: "u",
32
+ description: "Update/create suppression file with current error counts",
33
33
  default: false,
34
34
  }),
35
- 'fail-on-excess': Flags.boolean({
36
- description: 'Exit with non-zero code if there are unsuppressed errors',
35
+ "fail-on-excess": Flags.boolean({
36
+ description: "Exit with non-zero code if there are unsuppressed errors",
37
37
  default: true,
38
38
  allowNo: true,
39
39
  }),
40
- 'show-code': Flags.integer({
41
- description: 'Show code snippets for files with N or fewer errors (0 to disable)',
40
+ "show-code": Flags.integer({
41
+ description: "Show code snippets for files with N or fewer errors (0 to disable)",
42
42
  default: 3,
43
43
  }),
44
- help: Flags.help({ char: 'h' }),
45
44
  };
46
45
  static args = {
47
46
  paths: Args.string({
48
- description: 'Files or directories to lint (passed to oxlint)',
47
+ description: "Files or directories to lint (passed to oxlint)",
49
48
  required: false,
50
49
  }),
51
50
  };
52
51
  static strict = false; // Allow additional args to be passed to oxlint
53
52
  async run() {
54
- const { flags, argv } = await this.parse(OxlintHarness);
53
+ // Check for help flag first - check both this.argv and process.argv
54
+ const rawArgs = this.argv.slice(1);
55
+ const processArgs = process.argv.slice(2); // Skip 'node' and script path
56
+ const hasHelp = rawArgs.includes("--help") ||
57
+ rawArgs.includes("-h") ||
58
+ processArgs.includes("--help") ||
59
+ processArgs.includes("-h");
60
+ if (hasHelp) {
61
+ this.log(OxlintHarness.description);
62
+ this.log("");
63
+ this.log("USAGE");
64
+ this.log(` $ oxlint-harness [FLAGS] [ARGS]`);
65
+ this.log("");
66
+ this.log("FLAGS");
67
+ this.log(" -s, --suppressions <path> Path to suppression file (default: .oxlint-suppressions.json)");
68
+ this.log(" -u, --update Update/create suppression file with current error counts");
69
+ this.log(" --fail-on-excess Exit 1 if unsuppressed errors exist (default: true)");
70
+ this.log(" --no-fail-on-excess Don't exit 1 on unsuppressed errors");
71
+ this.log(" --show-code <number> Show code snippets for files with N or fewer errors (default: 3, 0 to disable)");
72
+ this.log(" -h, --help Show this help message");
73
+ this.log("");
74
+ this.log("ARGS");
75
+ this.log(" <paths> Files or directories to lint (passed to oxlint)");
76
+ this.log("");
77
+ this.log("EXAMPLES");
78
+ this.log(` $ oxlint-harness src/`);
79
+ this.log(` $ oxlint-harness --update src/`);
80
+ this.log(` $ oxlint-harness --type-aware src/`);
81
+ this.log(` $ oxlint-harness --suppressions .my-suppressions.json src/`);
82
+ return;
83
+ }
84
+ // Parse with error handling for unknown flags
85
+ let flags;
86
+ let oxlintArgs = [];
87
+ try {
88
+ const parsed = await this.parse(OxlintHarness);
89
+ flags = parsed.flags;
90
+ oxlintArgs = parsed.argv;
91
+ }
92
+ catch (error) {
93
+ // If error is due to unknown flags (NonExistentFlagsError), manually parse
94
+ if (error.name === "NonExistentFlagsError" ||
95
+ (error.flags && Array.isArray(error.flags))) {
96
+ const parsedFlags = {
97
+ suppressions: ".oxlint-suppressions.json",
98
+ update: false,
99
+ "fail-on-excess": true,
100
+ "show-code": 3,
101
+ };
102
+ const knownFlagNames = new Set([
103
+ "--suppressions",
104
+ "-s",
105
+ "--update",
106
+ "-u",
107
+ "--fail-on-excess",
108
+ "--no-fail-on-excess",
109
+ "--show-code",
110
+ ]);
111
+ // Manually parse known flags and collect unknown ones
112
+ for (let i = 0; i < rawArgs.length; i++) {
113
+ const arg = rawArgs[i];
114
+ if (arg === "--suppressions" || arg === "-s") {
115
+ parsedFlags.suppressions = rawArgs[++i] || parsedFlags.suppressions;
116
+ }
117
+ else if (arg === "--update" || arg === "-u") {
118
+ parsedFlags.update = true;
119
+ }
120
+ else if (arg === "--fail-on-excess") {
121
+ parsedFlags["fail-on-excess"] = true;
122
+ }
123
+ else if (arg === "--no-fail-on-excess") {
124
+ parsedFlags["fail-on-excess"] = false;
125
+ }
126
+ else if (arg === "--show-code") {
127
+ const value = rawArgs[++i];
128
+ if (value)
129
+ parsedFlags["show-code"] = parseInt(value, 10) || 3;
130
+ }
131
+ else if (!knownFlagNames.has(arg) &&
132
+ arg !== "--help" &&
133
+ arg !== "-h") {
134
+ // Unknown flag or positional arg - pass to oxlint
135
+ oxlintArgs.push(arg);
136
+ }
137
+ }
138
+ flags = parsedFlags;
139
+ }
140
+ else {
141
+ // Re-throw if it's a different error
142
+ throw error;
143
+ }
144
+ }
145
+ // Check for OXLINT_HARNESS_UPDATE_BULK_SUPPRESSION environment variable
146
+ if (process.env.OXLINT_HARNESS_UPDATE_BULK_SUPPRESSION?.toLowerCase() ===
147
+ "true") {
148
+ flags.update = true;
149
+ }
55
150
  const colors = new ColorFormatter();
56
151
  try {
57
152
  // Run oxlint with remaining args
58
153
  const runner = new OxlintRunner();
59
- const diagnostics = await runner.run(argv);
154
+ const diagnostics = await runner.run(oxlintArgs);
60
155
  // Handle suppression logic
61
156
  const suppressionManager = new SuppressionManager(flags.suppressions);
62
157
  if (flags.update) {
@@ -64,21 +159,30 @@ The suppression file format uses counts per rule per file:
64
159
  const currentSuppressions = suppressionManager.loadSuppressions();
65
160
  const updatedSuppressions = suppressionManager.updateSuppressions(currentSuppressions, diagnostics);
66
161
  suppressionManager.saveSuppressions(updatedSuppressions);
67
- this.log(`${colors.success('Updated suppression file:')} ${colors.filename(flags.suppressions)}`);
68
- this.log(`${colors.info('Total diagnostics:')} ${colors.emphasis(diagnostics.length.toString())}`);
162
+ this.log(`${colors.success("Updated suppression file:")} ${colors.filename(flags.suppressions)}`);
163
+ this.log(`${colors.info("Total diagnostics:")} ${colors.emphasis(diagnostics.length.toString())}`);
69
164
  return;
70
165
  }
71
166
  // Normal mode: check suppressions
72
167
  const suppressions = suppressionManager.loadSuppressions();
73
168
  const excessErrors = suppressionManager.findExcessErrors(diagnostics, suppressions);
169
+ // Check for OXLINT_HARNESS_TIGHTEN_BULK_SUPPRESSION environment variable
170
+ const shouldTighten = process.env.OXLINT_HARNESS_TIGHTEN_BULK_SUPPRESSION?.toLowerCase() ===
171
+ "true";
172
+ if (shouldTighten) {
173
+ // Tighten suppressions by removing/reducing cleaned-up violations
174
+ const tightenedSuppressions = suppressionManager.tightenSuppressions(suppressions, diagnostics);
175
+ suppressionManager.saveSuppressions(tightenedSuppressions);
176
+ this.log(`${colors.success("Tightened suppression file:")} ${colors.filename(flags.suppressions)}`);
177
+ }
74
178
  if (excessErrors.length === 0) {
75
- this.log(`${colors.successIcon()} ${colors.success('All errors are suppressed')}`);
179
+ this.log(`${colors.successIcon()} ${colors.success("All errors are suppressed")}`);
76
180
  return;
77
181
  }
78
182
  // Report excess errors
79
183
  const reporter = new ErrorReporter();
80
- reporter.reportExcessErrors(excessErrors, flags['show-code']);
81
- if (flags['fail-on-excess']) {
184
+ reporter.reportExcessErrors(excessErrors, flags["show-code"]);
185
+ if (flags["fail-on-excess"]) {
82
186
  this.exit(1);
83
187
  }
84
188
  }
@@ -1,4 +1,4 @@
1
- import { ExcessError } from './types.js';
1
+ import { ExcessError } from "./types.js";
2
2
  export declare class ErrorReporter {
3
3
  private snippetExtractor;
4
4
  private colors;
@@ -1,10 +1,10 @@
1
- import { CodeSnippetExtractor } from './code-snippet.js';
2
- import { ColorFormatter } from './colors.js';
1
+ import { CodeSnippetExtractor } from "./code-snippet.js";
2
+ import { ColorFormatter } from "./colors.js";
3
3
  export class ErrorReporter {
4
4
  snippetExtractor = new CodeSnippetExtractor();
5
5
  colors = new ColorFormatter();
6
6
  reportExcessErrors(excessErrors, showCodeThreshold = 3) {
7
- console.error(`${this.colors.error('')} Found unsuppressed errors:\n`);
7
+ console.error(`${this.colors.error("")} Found unsuppressed errors:\n`);
8
8
  let totalExcess = 0;
9
9
  // Group errors by file for better readability
10
10
  const errorsByFile = new Map();
@@ -19,52 +19,80 @@ export class ErrorReporter {
19
19
  for (const error of fileErrors) {
20
20
  const excess = error.actual - error.expected;
21
21
  totalExcess += excess;
22
- const excessText = excess === 1 ? 'error' : 'errors';
22
+ const excessText = excess === 1 ? "error" : "errors";
23
23
  console.error(` ${this.colors.warningIcon()} ${this.colors.rule(error.rule)}: ${this.colors.emphasis(excess.toString())} excess ${excessText} (expected: ${this.colors.muted(error.expected.toString())}, actual: ${this.colors.emphasis(error.actual.toString())})`);
24
- // Show detailed code snippets for files with few errors
25
- if (showCodeThreshold > 0 && error.diagnostics.length <= showCodeThreshold) {
26
- console.error(''); // Add spacing before code snippets
27
- error.diagnostics.forEach((diagnostic, index) => {
28
- const snippet = this.snippetExtractor.getCodeSnippet(diagnostic);
29
- if (snippet) {
30
- const formattedSnippet = this.snippetExtractor.formatCodeSnippet(snippet, diagnostic.rule, diagnostic.message, diagnostic.help);
31
- console.error(formattedSnippet);
24
+ // Always show the first diagnostic with full code snippet
25
+ if (showCodeThreshold > 0 && error.diagnostics.length > 0) {
26
+ console.error(""); // Add spacing before code snippets
27
+ // Always show first diagnostic with code snippet
28
+ const firstDiagnostic = error.diagnostics[0];
29
+ const snippet = this.snippetExtractor.getCodeSnippet(firstDiagnostic);
30
+ if (snippet) {
31
+ const formattedSnippet = this.snippetExtractor.formatCodeSnippet(snippet, firstDiagnostic.rule, firstDiagnostic.message, firstDiagnostic.help);
32
+ console.error(formattedSnippet);
33
+ }
34
+ else {
35
+ // Fallback to simple format if can't read file
36
+ const location = firstDiagnostic.line
37
+ ? `:${this.colors.lineNumber(firstDiagnostic.line.toString())}:${this.colors.lineNumber((firstDiagnostic.column || 0).toString())}`
38
+ : "";
39
+ console.error(` • ${this.colors.filename(firstDiagnostic.filename)}${location}: ${firstDiagnostic.message}`);
40
+ if (firstDiagnostic.help) {
41
+ console.error(` ${this.colors.infoIcon()} ${this.colors.help(firstDiagnostic.help)}`);
32
42
  }
33
- else {
34
- // Fallback to simple format if can't read file
35
- const location = diagnostic.line ? `:${this.colors.lineNumber(diagnostic.line.toString())}:${this.colors.lineNumber((diagnostic.column || 0).toString())}` : '';
36
- console.error(` • ${this.colors.filename(diagnostic.filename)}${location}: ${diagnostic.message}`);
37
- if (diagnostic.help) {
38
- console.error(` ${this.colors.infoIcon()} ${this.colors.help(diagnostic.help)}`);
43
+ }
44
+ // If there are more diagnostics, show them based on threshold
45
+ if (error.diagnostics.length > 1) {
46
+ if (error.diagnostics.length <= showCodeThreshold) {
47
+ // Show remaining diagnostics with snippets if under threshold
48
+ for (let i = 1; i < error.diagnostics.length; i++) {
49
+ const diagnostic = error.diagnostics[i];
50
+ const snippet = this.snippetExtractor.getCodeSnippet(diagnostic);
51
+ if (snippet) {
52
+ const formattedSnippet = this.snippetExtractor.formatCodeSnippet(snippet, diagnostic.rule, diagnostic.message, diagnostic.help);
53
+ console.error(formattedSnippet);
54
+ }
55
+ else {
56
+ // Fallback to simple format if can't read file
57
+ const location = diagnostic.line
58
+ ? `:${this.colors.lineNumber(diagnostic.line.toString())}:${this.colors.lineNumber((diagnostic.column || 0).toString())}`
59
+ : "";
60
+ console.error(` • ${this.colors.filename(diagnostic.filename)}${location}: ${diagnostic.message}`);
61
+ if (diagnostic.help) {
62
+ console.error(` ${this.colors.infoIcon()} ${this.colors.help(diagnostic.help)}`);
63
+ }
64
+ }
39
65
  }
40
66
  }
41
- });
42
- }
43
- else {
44
- // Show up to 3 example diagnostics for files with many errors
45
- const exampleCount = Math.min(3, error.diagnostics.length);
46
- for (let i = 0; i < exampleCount; i++) {
47
- const diagnostic = error.diagnostics[i];
48
- const location = diagnostic.line ? `:${this.colors.lineNumber(diagnostic.line.toString())}:${this.colors.lineNumber((diagnostic.column || 0).toString())}` : '';
49
- console.error(` • ${this.colors.filename(diagnostic.filename)}${location}: ${diagnostic.message}`);
50
- if (diagnostic.help) {
51
- console.error(` ${this.colors.infoIcon()} ${this.colors.help(diagnostic.help)}`);
67
+ else {
68
+ // Show remaining diagnostics (up to 2 more) as simple list items
69
+ const remainingCount = Math.min(2, error.diagnostics.length - 1);
70
+ for (let i = 1; i <= remainingCount; i++) {
71
+ const diagnostic = error.diagnostics[i];
72
+ const location = diagnostic.line
73
+ ? `:${this.colors.lineNumber(diagnostic.line.toString())}:${this.colors.lineNumber((diagnostic.column || 0).toString())}`
74
+ : "";
75
+ console.error(` • ${this.colors.filename(diagnostic.filename)}${location}: ${diagnostic.message}`);
76
+ if (diagnostic.help) {
77
+ console.error(` ${this.colors.infoIcon()} ${this.colors.help(diagnostic.help)}`);
78
+ }
79
+ }
80
+ if (error.diagnostics.length > remainingCount + 1) {
81
+ console.error(` ${this.colors.muted(`... and ${error.diagnostics.length - remainingCount - 1} more`)}`);
82
+ }
52
83
  }
53
84
  }
54
- if (error.diagnostics.length > exampleCount) {
55
- console.error(` ${this.colors.muted(`... and ${error.diagnostics.length - exampleCount} more`)}`);
56
- }
57
85
  }
58
86
  // Suggestion to suppress
59
- console.error(` 📝 ${this.colors.info('To suppress, add to suppression file:')}`);
60
- console.error(` ${this.colors.muted('"')}${this.colors.filename(error.filename)}${this.colors.muted('"')}: { ${this.colors.muted('"')}${this.colors.rule(error.rule)}${this.colors.muted('"')}: { ${this.colors.muted('"count"')}: ${this.colors.emphasis(error.actual.toString())} } }\n`);
87
+ console.error(` 📝 ${this.colors.info("To suppress, re-run with:")}`);
88
+ console.error(` ${this.colors.emphasis("OXLINT_HARNESS_UPDATE_BULK_SUPPRESSION=true oxlint-harness [your-args]")}\n`);
61
89
  }
62
90
  }
63
- console.error(`\n📊 ${this.colors.info('Summary:')}`);
91
+ console.error(`\n📊 ${this.colors.info("Summary:")}`);
64
92
  console.error(` • Files with issues: ${this.colors.emphasis(errorsByFile.size.toString())}`);
65
93
  console.error(` • Rules with excess errors: ${this.colors.emphasis(excessErrors.length.toString())}`);
66
94
  console.error(` • Total excess errors: ${this.colors.emphasis(totalExcess.toString())}`);
67
- console.error(`\n${this.colors.infoIcon()} ${this.colors.info('To suppress all current errors, run:')}`);
68
- console.error(` ${this.colors.emphasis('oxlint-harness --update [your-args]')}`);
95
+ console.error(`\n${this.colors.infoIcon()} ${this.colors.info("To suppress all current errors, run:")}`);
96
+ console.error(` ${this.colors.emphasis("oxlint-harness --update [your-args]")}`);
69
97
  }
70
98
  }
@@ -7,4 +7,5 @@ export declare class SuppressionManager {
7
7
  generateSuppressions(diagnostics: ProcessedDiagnostic[]): SuppressionFile;
8
8
  findExcessErrors(diagnostics: ProcessedDiagnostic[], suppressions: SuppressionFile): ExcessError[];
9
9
  updateSuppressions(currentSuppressions: SuppressionFile, diagnostics: ProcessedDiagnostic[]): SuppressionFile;
10
+ tightenSuppressions(currentSuppressions: SuppressionFile, diagnostics: ProcessedDiagnostic[]): SuppressionFile;
10
11
  }
@@ -93,4 +93,45 @@ export class SuppressionManager {
93
93
  }
94
94
  return updated;
95
95
  }
96
+ tightenSuppressions(currentSuppressions, diagnostics) {
97
+ // Group diagnostics by file and rule to get actual counts
98
+ const actualCounts = new Map();
99
+ for (const diagnostic of diagnostics) {
100
+ if (!actualCounts.has(diagnostic.filename)) {
101
+ actualCounts.set(diagnostic.filename, new Map());
102
+ }
103
+ const fileRules = actualCounts.get(diagnostic.filename);
104
+ const currentCount = fileRules.get(diagnostic.rule) || 0;
105
+ fileRules.set(diagnostic.rule, currentCount + 1);
106
+ }
107
+ // Create a copy of current suppressions to modify
108
+ const tightened = { ...currentSuppressions };
109
+ // Process each file in current suppressions
110
+ for (const [filename, rules] of Object.entries(tightened)) {
111
+ const fileRules = { ...rules };
112
+ // Process each rule in the file
113
+ for (const [rule, suppression] of Object.entries(fileRules)) {
114
+ const expected = suppression.count;
115
+ const actual = actualCounts.get(filename)?.get(rule) || 0;
116
+ if (actual === 0) {
117
+ // Remove entry if no actual errors exist
118
+ delete fileRules[rule];
119
+ }
120
+ else if (actual < expected) {
121
+ // Reduce count to actual if violations were cleaned up
122
+ fileRules[rule] = { count: actual };
123
+ }
124
+ // If actual >= expected, keep as is (excess errors handled by findExcessErrors)
125
+ }
126
+ // Update the file entry
127
+ if (Object.keys(fileRules).length === 0) {
128
+ // Remove file entry if no rules remain
129
+ delete tightened[filename];
130
+ }
131
+ else {
132
+ tightened[filename] = fileRules;
133
+ }
134
+ }
135
+ return tightened;
136
+ }
96
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-harness",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A harness for oxlint with bulk suppressions support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -43,5 +43,6 @@
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=22"
46
- }
46
+ },
47
+ "packageManager": "pnpm@9.15.9"
47
48
  }