oxlint-harness 1.0.0 → 1.0.2
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 +49 -16
- package/dist/cli.js +1 -4
- package/dist/command.d.ts +6 -4
- package/dist/command.js +178 -27
- package/dist/error-reporter.d.ts +1 -1
- package/dist/error-reporter.js +65 -37
- package/dist/suppression-manager.d.ts +2 -1
- package/dist/suppression-manager.js +46 -5
- package/package.json +4 -3
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
|
|
59
|
-
|
|
60
|
-
| `--suppressions`
|
|
61
|
-
| `--update`
|
|
62
|
-
| `--show-code`
|
|
63
|
-
| `--fail-on-excess`
|
|
64
|
-
| `--no-fail-on-excess` |
|
|
65
|
-
| `--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
|
-
|
|
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,
|
|
150
|
-
|
|
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
|
-
####
|
|
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
|
-
|
|
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,
|
|
171
|
-
|
|
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
package/dist/command.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command } from
|
|
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,13 +6,15 @@ 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+
"results-path": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
"save-results": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
};
|
|
13
14
|
static args: {
|
|
14
15
|
paths: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
15
16
|
};
|
|
16
17
|
static strict: boolean;
|
|
17
18
|
run(): Promise<void>;
|
|
19
|
+
private saveResults;
|
|
18
20
|
}
|
package/dist/command.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { Command, Flags, Args } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { Command, Flags, Args } from "@oclif/core";
|
|
2
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import { OxlintRunner } from "./oxlint-runner.js";
|
|
5
|
+
import { SuppressionManager } from "./suppression-manager.js";
|
|
6
|
+
import { ErrorReporter } from "./error-reporter.js";
|
|
7
|
+
import { ColorFormatter } from "./colors.js";
|
|
6
8
|
export default class OxlintHarness extends Command {
|
|
7
|
-
static summary =
|
|
9
|
+
static summary = "Run oxlint with bulk suppressions support";
|
|
8
10
|
static description = `
|
|
9
11
|
Runs oxlint with support for bulk suppressions similar to ESLint.
|
|
10
12
|
|
|
@@ -17,46 +19,181 @@ The suppression file format uses counts per rule per file:
|
|
|
17
19
|
}
|
|
18
20
|
`;
|
|
19
21
|
static examples = [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
"<%= config.bin %> <%= command.id %> src/",
|
|
23
|
+
"<%= config.bin %> <%= command.id %> --update src/",
|
|
24
|
+
"<%= config.bin %> <%= command.id %> --suppressions .my-suppressions.json src/",
|
|
23
25
|
];
|
|
24
26
|
static flags = {
|
|
25
27
|
suppressions: Flags.string({
|
|
26
|
-
char:
|
|
27
|
-
description:
|
|
28
|
-
default:
|
|
28
|
+
char: "s",
|
|
29
|
+
description: "Path to suppression file",
|
|
30
|
+
default: ".oxlint-suppressions.json",
|
|
29
31
|
}),
|
|
30
32
|
update: Flags.boolean({
|
|
31
|
-
char:
|
|
32
|
-
description:
|
|
33
|
+
char: "u",
|
|
34
|
+
description: "Update/create suppression file with current error counts",
|
|
33
35
|
default: false,
|
|
34
36
|
}),
|
|
35
|
-
|
|
36
|
-
description:
|
|
37
|
+
"fail-on-excess": Flags.boolean({
|
|
38
|
+
description: "Exit with non-zero code if there are unsuppressed errors",
|
|
37
39
|
default: true,
|
|
38
40
|
allowNo: true,
|
|
39
41
|
}),
|
|
40
|
-
|
|
41
|
-
description:
|
|
42
|
+
"show-code": Flags.integer({
|
|
43
|
+
description: "Show code snippets for files with N or fewer errors (0 to disable)",
|
|
42
44
|
default: 3,
|
|
43
45
|
}),
|
|
44
|
-
|
|
46
|
+
"results-path": Flags.string({
|
|
47
|
+
description: "Path to save oxlint JSON results (default: artifacts/oxlint-results.json)",
|
|
48
|
+
default: "artifacts/oxlint-results.json",
|
|
49
|
+
env: "OXLINT_HARNESS_RESULTS_PATH",
|
|
50
|
+
}),
|
|
51
|
+
"save-results": Flags.boolean({
|
|
52
|
+
description: "Save oxlint JSON results to file",
|
|
53
|
+
default: true,
|
|
54
|
+
allowNo: true,
|
|
55
|
+
}),
|
|
45
56
|
};
|
|
46
57
|
static args = {
|
|
47
58
|
paths: Args.string({
|
|
48
|
-
description:
|
|
59
|
+
description: "Files or directories to lint (passed to oxlint)",
|
|
49
60
|
required: false,
|
|
50
61
|
}),
|
|
51
62
|
};
|
|
52
63
|
static strict = false; // Allow additional args to be passed to oxlint
|
|
53
64
|
async run() {
|
|
54
|
-
|
|
65
|
+
// Check for help flag first - check both this.argv and process.argv
|
|
66
|
+
const rawArgs = this.argv.slice(1);
|
|
67
|
+
const processArgs = process.argv.slice(2); // Skip 'node' and script path
|
|
68
|
+
const hasHelp = rawArgs.includes("--help") ||
|
|
69
|
+
rawArgs.includes("-h") ||
|
|
70
|
+
processArgs.includes("--help") ||
|
|
71
|
+
processArgs.includes("-h");
|
|
72
|
+
if (hasHelp) {
|
|
73
|
+
this.log(OxlintHarness.description);
|
|
74
|
+
this.log("");
|
|
75
|
+
this.log("USAGE");
|
|
76
|
+
this.log(` $ oxlint-harness [FLAGS] [ARGS]`);
|
|
77
|
+
this.log("");
|
|
78
|
+
this.log("FLAGS");
|
|
79
|
+
this.log(" -s, --suppressions <path> Path to suppression file (default: .oxlint-suppressions.json)");
|
|
80
|
+
this.log(" -u, --update Update/create suppression file with current error counts");
|
|
81
|
+
this.log(" --fail-on-excess Exit 1 if unsuppressed errors exist (default: true)");
|
|
82
|
+
this.log(" --no-fail-on-excess Don't exit 1 on unsuppressed errors");
|
|
83
|
+
this.log(" --show-code <number> Show code snippets for files with N or fewer errors (default: 3, 0 to disable)");
|
|
84
|
+
this.log(" --results-path <path> Path to save oxlint JSON results (default: artifacts/oxlint-results.json)");
|
|
85
|
+
this.log(" --no-save-results Don't save oxlint JSON results to file");
|
|
86
|
+
this.log(" -h, --help Show this help message");
|
|
87
|
+
this.log("");
|
|
88
|
+
this.log("ENVIRONMENT VARIABLES");
|
|
89
|
+
this.log(" OXLINT_HARNESS_RESULTS_PATH Override the results file path");
|
|
90
|
+
this.log(" OXLINT_HARNESS_NO_FAIL_ON_EXCESS Set to 'true' to exit 0 on new errors");
|
|
91
|
+
this.log("");
|
|
92
|
+
this.log("ARGS");
|
|
93
|
+
this.log(" <paths> Files or directories to lint (passed to oxlint)");
|
|
94
|
+
this.log("");
|
|
95
|
+
this.log("EXAMPLES");
|
|
96
|
+
this.log(` $ oxlint-harness src/`);
|
|
97
|
+
this.log(` $ oxlint-harness --update src/`);
|
|
98
|
+
this.log(` $ oxlint-harness --type-aware src/`);
|
|
99
|
+
this.log(` $ oxlint-harness --suppressions .my-suppressions.json src/`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Parse with error handling for unknown flags
|
|
103
|
+
let flags;
|
|
104
|
+
let oxlintArgs = [];
|
|
105
|
+
try {
|
|
106
|
+
const parsed = await this.parse(OxlintHarness);
|
|
107
|
+
flags = parsed.flags;
|
|
108
|
+
oxlintArgs = parsed.argv;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
// If error is due to unknown flags (NonExistentFlagsError), manually parse
|
|
112
|
+
if (error.name === "NonExistentFlagsError" ||
|
|
113
|
+
(error.flags && Array.isArray(error.flags))) {
|
|
114
|
+
const parsedFlags = {
|
|
115
|
+
suppressions: ".oxlint-suppressions.json",
|
|
116
|
+
update: false,
|
|
117
|
+
"fail-on-excess": true,
|
|
118
|
+
"show-code": 3,
|
|
119
|
+
"results-path": process.env.OXLINT_HARNESS_RESULTS_PATH ||
|
|
120
|
+
"artifacts/oxlint-results.json",
|
|
121
|
+
"save-results": true,
|
|
122
|
+
};
|
|
123
|
+
const knownFlagNames = new Set([
|
|
124
|
+
"--suppressions",
|
|
125
|
+
"-s",
|
|
126
|
+
"--update",
|
|
127
|
+
"-u",
|
|
128
|
+
"--fail-on-excess",
|
|
129
|
+
"--no-fail-on-excess",
|
|
130
|
+
"--show-code",
|
|
131
|
+
"--results-path",
|
|
132
|
+
"--save-results",
|
|
133
|
+
"--no-save-results",
|
|
134
|
+
]);
|
|
135
|
+
// Manually parse known flags and collect unknown ones
|
|
136
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
137
|
+
const arg = rawArgs[i];
|
|
138
|
+
if (arg === "--suppressions" || arg === "-s") {
|
|
139
|
+
parsedFlags.suppressions = rawArgs[++i] || parsedFlags.suppressions;
|
|
140
|
+
}
|
|
141
|
+
else if (arg === "--update" || arg === "-u") {
|
|
142
|
+
parsedFlags.update = true;
|
|
143
|
+
}
|
|
144
|
+
else if (arg === "--fail-on-excess") {
|
|
145
|
+
parsedFlags["fail-on-excess"] = true;
|
|
146
|
+
}
|
|
147
|
+
else if (arg === "--no-fail-on-excess") {
|
|
148
|
+
parsedFlags["fail-on-excess"] = false;
|
|
149
|
+
}
|
|
150
|
+
else if (arg === "--show-code") {
|
|
151
|
+
const value = rawArgs[++i];
|
|
152
|
+
if (value)
|
|
153
|
+
parsedFlags["show-code"] = parseInt(value, 10) || 3;
|
|
154
|
+
}
|
|
155
|
+
else if (arg === "--results-path") {
|
|
156
|
+
parsedFlags["results-path"] =
|
|
157
|
+
rawArgs[++i] || parsedFlags["results-path"];
|
|
158
|
+
}
|
|
159
|
+
else if (arg === "--save-results") {
|
|
160
|
+
parsedFlags["save-results"] = true;
|
|
161
|
+
}
|
|
162
|
+
else if (arg === "--no-save-results") {
|
|
163
|
+
parsedFlags["save-results"] = false;
|
|
164
|
+
}
|
|
165
|
+
else if (!knownFlagNames.has(arg) &&
|
|
166
|
+
arg !== "--help" &&
|
|
167
|
+
arg !== "-h") {
|
|
168
|
+
// Unknown flag or positional arg - pass to oxlint
|
|
169
|
+
oxlintArgs.push(arg);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
flags = parsedFlags;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Re-throw if it's a different error
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Check for OXLINT_HARNESS_UPDATE_BULK_SUPPRESSION environment variable
|
|
180
|
+
if (process.env.OXLINT_HARNESS_UPDATE_BULK_SUPPRESSION?.toLowerCase() ===
|
|
181
|
+
"true") {
|
|
182
|
+
flags.update = true;
|
|
183
|
+
}
|
|
184
|
+
// Check for OXLINT_HARNESS_NO_FAIL_ON_EXCESS environment variable
|
|
185
|
+
if (process.env.OXLINT_HARNESS_NO_FAIL_ON_EXCESS?.toLowerCase() === "true") {
|
|
186
|
+
flags["fail-on-excess"] = false;
|
|
187
|
+
}
|
|
55
188
|
const colors = new ColorFormatter();
|
|
56
189
|
try {
|
|
57
190
|
// Run oxlint with remaining args
|
|
58
191
|
const runner = new OxlintRunner();
|
|
59
|
-
const diagnostics = await runner.run(
|
|
192
|
+
const diagnostics = await runner.run(oxlintArgs);
|
|
193
|
+
// Save results if enabled
|
|
194
|
+
if (flags["save-results"]) {
|
|
195
|
+
await this.saveResults(flags["results-path"], diagnostics);
|
|
196
|
+
}
|
|
60
197
|
// Handle suppression logic
|
|
61
198
|
const suppressionManager = new SuppressionManager(flags.suppressions);
|
|
62
199
|
if (flags.update) {
|
|
@@ -64,21 +201,30 @@ The suppression file format uses counts per rule per file:
|
|
|
64
201
|
const currentSuppressions = suppressionManager.loadSuppressions();
|
|
65
202
|
const updatedSuppressions = suppressionManager.updateSuppressions(currentSuppressions, diagnostics);
|
|
66
203
|
suppressionManager.saveSuppressions(updatedSuppressions);
|
|
67
|
-
this.log(`${colors.success(
|
|
68
|
-
this.log(`${colors.info(
|
|
204
|
+
this.log(`${colors.success("Updated suppression file:")} ${colors.filename(flags.suppressions)}`);
|
|
205
|
+
this.log(`${colors.info("Total diagnostics:")} ${colors.emphasis(diagnostics.length.toString())}`);
|
|
69
206
|
return;
|
|
70
207
|
}
|
|
71
208
|
// Normal mode: check suppressions
|
|
72
209
|
const suppressions = suppressionManager.loadSuppressions();
|
|
73
210
|
const excessErrors = suppressionManager.findExcessErrors(diagnostics, suppressions);
|
|
211
|
+
// Check for OXLINT_HARNESS_TIGHTEN_BULK_SUPPRESSION environment variable
|
|
212
|
+
const shouldTighten = process.env.OXLINT_HARNESS_TIGHTEN_BULK_SUPPRESSION?.toLowerCase() ===
|
|
213
|
+
"true";
|
|
214
|
+
if (shouldTighten) {
|
|
215
|
+
// Tighten suppressions by removing/reducing cleaned-up violations
|
|
216
|
+
const tightenedSuppressions = suppressionManager.tightenSuppressions(suppressions, diagnostics);
|
|
217
|
+
suppressionManager.saveSuppressions(tightenedSuppressions);
|
|
218
|
+
this.log(`${colors.success("Tightened suppression file:")} ${colors.filename(flags.suppressions)}`);
|
|
219
|
+
}
|
|
74
220
|
if (excessErrors.length === 0) {
|
|
75
|
-
this.log(`${colors.successIcon()} ${colors.success(
|
|
221
|
+
this.log(`${colors.successIcon()} ${colors.success("All errors are suppressed")}`);
|
|
76
222
|
return;
|
|
77
223
|
}
|
|
78
224
|
// Report excess errors
|
|
79
225
|
const reporter = new ErrorReporter();
|
|
80
|
-
reporter.reportExcessErrors(excessErrors, flags[
|
|
81
|
-
if (flags[
|
|
226
|
+
reporter.reportExcessErrors(excessErrors, flags["show-code"]);
|
|
227
|
+
if (flags["fail-on-excess"]) {
|
|
82
228
|
this.exit(1);
|
|
83
229
|
}
|
|
84
230
|
}
|
|
@@ -86,4 +232,9 @@ The suppression file format uses counts per rule per file:
|
|
|
86
232
|
this.error(error instanceof Error ? error.message : String(error));
|
|
87
233
|
}
|
|
88
234
|
}
|
|
235
|
+
async saveResults(path, diagnostics) {
|
|
236
|
+
const dir = dirname(path);
|
|
237
|
+
await mkdir(dir, { recursive: true });
|
|
238
|
+
await writeFile(path, JSON.stringify(diagnostics, null, 2));
|
|
239
|
+
}
|
|
89
240
|
}
|
package/dist/error-reporter.d.ts
CHANGED
package/dist/error-reporter.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { CodeSnippetExtractor } from
|
|
2
|
-
import { ColorFormatter } from
|
|
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(
|
|
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 ?
|
|
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
|
-
//
|
|
25
|
-
if (showCodeThreshold > 0 && error.diagnostics.length
|
|
26
|
-
console.error(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if
|
|
38
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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(
|
|
60
|
-
console.error(`
|
|
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(
|
|
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(
|
|
68
|
-
console.error(` ${this.colors.emphasis(
|
|
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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SuppressionFile, ProcessedDiagnostic, ExcessError } from
|
|
1
|
+
import { SuppressionFile, ProcessedDiagnostic, ExcessError } from "./types.js";
|
|
2
2
|
export declare class SuppressionManager {
|
|
3
3
|
private suppressionFile;
|
|
4
4
|
constructor(suppressionFile?: string);
|
|
@@ -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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync } from
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
2
2
|
export class SuppressionManager {
|
|
3
3
|
suppressionFile;
|
|
4
|
-
constructor(suppressionFile =
|
|
4
|
+
constructor(suppressionFile = ".oxlint-suppressions.json") {
|
|
5
5
|
this.suppressionFile = suppressionFile;
|
|
6
6
|
}
|
|
7
7
|
loadSuppressions() {
|
|
@@ -9,7 +9,7 @@ export class SuppressionManager {
|
|
|
9
9
|
return {};
|
|
10
10
|
}
|
|
11
11
|
try {
|
|
12
|
-
const content = readFileSync(this.suppressionFile,
|
|
12
|
+
const content = readFileSync(this.suppressionFile, "utf8");
|
|
13
13
|
return JSON.parse(content);
|
|
14
14
|
}
|
|
15
15
|
catch (error) {
|
|
@@ -19,7 +19,7 @@ export class SuppressionManager {
|
|
|
19
19
|
saveSuppressions(suppressions) {
|
|
20
20
|
try {
|
|
21
21
|
const content = JSON.stringify(suppressions, null, 2);
|
|
22
|
-
writeFileSync(this.suppressionFile, content,
|
|
22
|
+
writeFileSync(this.suppressionFile, content, "utf8");
|
|
23
23
|
}
|
|
24
24
|
catch (error) {
|
|
25
25
|
throw new Error(`Failed to write suppression file ${this.suppressionFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -71,7 +71,7 @@ export class SuppressionManager {
|
|
|
71
71
|
filename,
|
|
72
72
|
expected,
|
|
73
73
|
actual,
|
|
74
|
-
diagnostics: diagnosticsForRule
|
|
74
|
+
diagnostics: diagnosticsForRule,
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -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.
|
|
3
|
+
"version": "1.0.2",
|
|
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
|
-
}
|
|
47
|
-
|
|
46
|
+
},
|
|
47
|
+
"packageManager": "pnpm@9.15.9"
|
|
48
|
+
}
|