oxlint-harness 1.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 +212 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +6 -0
- package/dist/code-snippet.d.ts +17 -0
- package/dist/code-snippet.js +94 -0
- package/dist/colors.d.ts +51 -0
- package/dist/colors.js +107 -0
- package/dist/command.d.ts +18 -0
- package/dist/command.js +89 -0
- package/dist/error-reporter.d.ts +6 -0
- package/dist/error-reporter.js +70 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/oxlint-runner.d.ts +7 -0
- package/dist/oxlint-runner.js +133 -0
- package/dist/suppression-manager.d.ts +10 -0
- package/dist/suppression-manager.js +96 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# oxlint-harness
|
|
2
|
+
|
|
3
|
+
A harness for [oxlint](https://oxc.rs) that provides ESLint-style bulk suppressions support. This tool allows you to gradually adopt stricter linting rules by suppressing existing violations while preventing new ones.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Bulk Suppressions**: Suppress specific counts of lint violations per file per rule
|
|
8
|
+
- ✅ **Incremental Adoption**: Only fail on new violations beyond suppressed counts
|
|
9
|
+
- ✅ **Rich Colored Output**: Beautiful terminal colors with syntax highlighting
|
|
10
|
+
- ✅ **Code Snippets**: Show actual code context for files with few errors (configurable)
|
|
11
|
+
- ✅ **Smart Package Detection**: Auto-detects pnpm/yarn/npm with monorepo support
|
|
12
|
+
- ✅ **Update Mode**: Generate/update suppression files automatically
|
|
13
|
+
- ✅ **Pass-through**: Forward all arguments to oxlint seamlessly
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install oxlint-harness
|
|
19
|
+
# or
|
|
20
|
+
pnpm add oxlint-harness
|
|
21
|
+
# or
|
|
22
|
+
yarn add oxlint-harness
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Run oxlint with suppressions (fails on excess errors)
|
|
29
|
+
npx oxlint-harness src/
|
|
30
|
+
|
|
31
|
+
# Generate initial suppression file
|
|
32
|
+
npx oxlint-harness --update src/
|
|
33
|
+
|
|
34
|
+
# Use custom suppression file
|
|
35
|
+
npx oxlint-harness --suppressions .my-suppressions.json src/
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Suppression File Format
|
|
39
|
+
|
|
40
|
+
The suppression file uses a count-based format:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"src/App.tsx": {
|
|
45
|
+
"@typescript-eslint/no-unused-vars": { "count": 1 },
|
|
46
|
+
"eqeqeq": { "count": 1 },
|
|
47
|
+
"no-var": { "count": 1 },
|
|
48
|
+
"prefer-const": { "count": 1 }
|
|
49
|
+
},
|
|
50
|
+
"src/utils.ts": {
|
|
51
|
+
"no-console": { "count": 3 }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## CLI Options
|
|
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 | - |
|
|
66
|
+
|
|
67
|
+
All additional arguments are passed directly to oxlint.
|
|
68
|
+
|
|
69
|
+
## Usage Examples
|
|
70
|
+
|
|
71
|
+
### Basic Usage
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Check files with suppressions
|
|
75
|
+
npx oxlint-harness src/ lib/
|
|
76
|
+
|
|
77
|
+
# Pass oxlint options
|
|
78
|
+
npx oxlint-harness --type-aware src/
|
|
79
|
+
|
|
80
|
+
# Use different suppression file
|
|
81
|
+
npx oxlint-harness -s .eslint-suppressions.json src/
|
|
82
|
+
|
|
83
|
+
# Show code snippets for files with ≤1 error
|
|
84
|
+
npx oxlint-harness --show-code 1 src/
|
|
85
|
+
|
|
86
|
+
# Disable code snippets (simple list only)
|
|
87
|
+
npx oxlint-harness --show-code 0 src/
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Generating Suppressions
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Create initial suppression file (records current error counts)
|
|
94
|
+
npx oxlint-harness --update src/
|
|
95
|
+
|
|
96
|
+
# Update existing suppressions with new counts
|
|
97
|
+
npx oxlint-harness --update --suppressions custom.json src/
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### CI Integration
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# In CI: fail build if new errors are introduced
|
|
104
|
+
npx oxlint-harness src/
|
|
105
|
+
|
|
106
|
+
# For reporting mode (don't fail build)
|
|
107
|
+
npx oxlint-harness --no-fail-on-excess src/
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Workflow
|
|
111
|
+
|
|
112
|
+
### Initial Setup
|
|
113
|
+
|
|
114
|
+
1. Run with `--update` to create initial suppression file:
|
|
115
|
+
```bash
|
|
116
|
+
npx oxlint-harness --update src/
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
2. Commit the `.oxlint-suppressions.json` file
|
|
120
|
+
|
|
121
|
+
3. Configure CI to run `npx oxlint-harness src/`
|
|
122
|
+
|
|
123
|
+
### Daily Development
|
|
124
|
+
|
|
125
|
+
- **Normal runs**: Only new violations (beyond suppressed counts) will cause failures
|
|
126
|
+
- **Fixing violations**: Counts automatically decrease as issues are resolved
|
|
127
|
+
- **Adding suppressions**: Update counts manually or re-run with `--update`
|
|
128
|
+
|
|
129
|
+
### Example Output
|
|
130
|
+
|
|
131
|
+
#### With Code Snippets (default for files with ≤3 errors)
|
|
132
|
+
```
|
|
133
|
+
❌ Found unsuppressed errors:
|
|
134
|
+
|
|
135
|
+
📄 src/App.tsx:
|
|
136
|
+
⚠️ eslint(no-unused-vars): 1 excess error (expected: 0, actual: 1)
|
|
137
|
+
|
|
138
|
+
× eslint(no-unused-vars): Variable 'thing' is declared but never used. Unused variables should start with a '_'.
|
|
139
|
+
╭─[24:7]
|
|
140
|
+
24 │ }));
|
|
141
|
+
25 │
|
|
142
|
+
26 │ const thing = 3;
|
|
143
|
+
─────────────
|
|
144
|
+
27 │
|
|
145
|
+
28 │ const PartnerAppRootBase: React.VFC = () => {
|
|
146
|
+
╰────
|
|
147
|
+
help: Consider removing this declaration.
|
|
148
|
+
|
|
149
|
+
📝 To suppress, add to suppression file:
|
|
150
|
+
"src/App.tsx": { "eslint(no-unused-vars)": { "count": 1 } }
|
|
151
|
+
|
|
152
|
+
📊 Summary:
|
|
153
|
+
• Files with issues: 1
|
|
154
|
+
• Rules with excess errors: 1
|
|
155
|
+
• Total excess errors: 1
|
|
156
|
+
|
|
157
|
+
💡 To suppress all current errors, run:
|
|
158
|
+
oxlint-harness --update src/
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### Simple List (for files with many errors)
|
|
162
|
+
```
|
|
163
|
+
📄 src/Components.tsx:
|
|
164
|
+
⚠️ prefer-const: 15 excess errors (expected: 10, actual: 25)
|
|
165
|
+
• src/Components.tsx:42:12: 'data' is never reassigned
|
|
166
|
+
• src/Components.tsx:58:8: 'config' is never reassigned
|
|
167
|
+
• src/Components.tsx:74:15: 'result' is never reassigned
|
|
168
|
+
... and 12 more
|
|
169
|
+
|
|
170
|
+
📝 To suppress, add to suppression file:
|
|
171
|
+
"src/Components.tsx": { "prefer-const": { "count": 25 } }
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Requirements
|
|
175
|
+
|
|
176
|
+
- Node.js 22+ (for native TypeScript support)
|
|
177
|
+
- `oxlint` installed and available (via pnpm/yarn/npm)
|
|
178
|
+
|
|
179
|
+
## How It Works
|
|
180
|
+
|
|
181
|
+
1. **Package Manager Detection**: Automatically detects if you're using pnpm, yarn, or npm by looking for lock files up the directory tree (supports monorepos)
|
|
182
|
+
2. **Oxlint Execution**: Runs `pnpm oxlint`, `yarn oxlint`, or `npx oxlint` with JSON output
|
|
183
|
+
3. **Suppression Matching**: Compares actual error counts against your suppression file
|
|
184
|
+
4. **Smart Reporting**: Shows code snippets for files with few errors, simple lists for files with many errors
|
|
185
|
+
5. **Colored Output**: Beautiful terminal colors that automatically disable in non-TTY environments
|
|
186
|
+
|
|
187
|
+
## Contributing
|
|
188
|
+
|
|
189
|
+
Issues and pull requests are welcome! Please see the [GitHub repository](https://github.com/MIreland/oxlint-harness) for more information.
|
|
190
|
+
|
|
191
|
+
## Development
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# Install dependencies
|
|
195
|
+
pnpm install
|
|
196
|
+
|
|
197
|
+
# Run tests
|
|
198
|
+
pnpm test
|
|
199
|
+
|
|
200
|
+
# Type checking
|
|
201
|
+
pnpm typecheck
|
|
202
|
+
|
|
203
|
+
# Lint
|
|
204
|
+
pnpm lint
|
|
205
|
+
|
|
206
|
+
# Development mode
|
|
207
|
+
pnpm dev --help
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ProcessedDiagnostic } from './types.js';
|
|
2
|
+
export interface CodeSnippet {
|
|
3
|
+
beforeLines: string[];
|
|
4
|
+
targetLine: string;
|
|
5
|
+
afterLines: string[];
|
|
6
|
+
lineNumber: number;
|
|
7
|
+
columnStart: number;
|
|
8
|
+
columnEnd: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class CodeSnippetExtractor {
|
|
11
|
+
private fileCache;
|
|
12
|
+
private colors;
|
|
13
|
+
getCodeSnippet(diagnostic: ProcessedDiagnostic): CodeSnippet | null;
|
|
14
|
+
private getFileLines;
|
|
15
|
+
formatCodeSnippet(snippet: CodeSnippet, rule: string, message: string, help?: string): string;
|
|
16
|
+
private highlightSyntax;
|
|
17
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { ColorFormatter } from './colors.js';
|
|
3
|
+
export class CodeSnippetExtractor {
|
|
4
|
+
fileCache = new Map();
|
|
5
|
+
colors = new ColorFormatter();
|
|
6
|
+
getCodeSnippet(diagnostic) {
|
|
7
|
+
if (!diagnostic.line || !diagnostic.filename) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const lines = this.getFileLines(diagnostic.filename);
|
|
11
|
+
if (!lines) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const lineIndex = diagnostic.line - 1; // Convert to 0-based index
|
|
15
|
+
const contextLines = 2; // Show 2 lines before/after
|
|
16
|
+
const beforeLines = lines.slice(Math.max(0, lineIndex - contextLines), lineIndex);
|
|
17
|
+
const targetLine = lines[lineIndex] || '';
|
|
18
|
+
const afterLines = lines.slice(lineIndex + 1, Math.min(lines.length, lineIndex + 1 + contextLines));
|
|
19
|
+
return {
|
|
20
|
+
beforeLines,
|
|
21
|
+
targetLine,
|
|
22
|
+
afterLines,
|
|
23
|
+
lineNumber: diagnostic.line,
|
|
24
|
+
columnStart: diagnostic.column || 0,
|
|
25
|
+
columnEnd: (diagnostic.column || 0) + (diagnostic.message.length || 10), // Rough estimate
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
getFileLines(filename) {
|
|
29
|
+
if (this.fileCache.has(filename)) {
|
|
30
|
+
return this.fileCache.get(filename);
|
|
31
|
+
}
|
|
32
|
+
if (!existsSync(filename)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const content = readFileSync(filename, 'utf8');
|
|
37
|
+
const lines = content.split('\n');
|
|
38
|
+
this.fileCache.set(filename, lines);
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
formatCodeSnippet(snippet, rule, message, help) {
|
|
46
|
+
let output = '';
|
|
47
|
+
// Error header with colors
|
|
48
|
+
output += ` ${this.colors.errorIcon()} ${this.colors.rule(rule)}: ${message}\n`;
|
|
49
|
+
const startLineNum = snippet.lineNumber - snippet.beforeLines.length;
|
|
50
|
+
const endLineNum = snippet.lineNumber + snippet.afterLines.length;
|
|
51
|
+
// File path and line range with colors
|
|
52
|
+
output += ` ${this.colors.border('╭─')}${this.colors.filename(`[${startLineNum}:${snippet.columnStart}]`)}\n`;
|
|
53
|
+
// Before lines
|
|
54
|
+
snippet.beforeLines.forEach((line, index) => {
|
|
55
|
+
const lineNum = startLineNum + index;
|
|
56
|
+
const lineNumStr = this.colors.lineNumber(lineNum.toString().padStart(4));
|
|
57
|
+
const border = this.colors.border(' │ ');
|
|
58
|
+
output += `${lineNumStr}${border}${this.highlightSyntax(line)}\n`;
|
|
59
|
+
});
|
|
60
|
+
// Target line with error highlight
|
|
61
|
+
const targetLineNumStr = this.colors.lineNumber(snippet.lineNumber.toString().padStart(4));
|
|
62
|
+
const targetBorder = this.colors.border(' │ ');
|
|
63
|
+
output += `${targetLineNumStr}${targetBorder}${this.highlightSyntax(snippet.targetLine)}\n`;
|
|
64
|
+
// Error pointer with color
|
|
65
|
+
const indent = ' '.repeat(4 + 3 + snippet.columnStart); // Line number + "│ " + column offset
|
|
66
|
+
const pointer = this.colors.errorPointer('─'.repeat(Math.max(1, Math.min(snippet.columnEnd - snippet.columnStart, 20))));
|
|
67
|
+
output += `${indent}${pointer}\n`;
|
|
68
|
+
// After lines
|
|
69
|
+
snippet.afterLines.forEach((line, index) => {
|
|
70
|
+
const lineNum = snippet.lineNumber + 1 + index;
|
|
71
|
+
const lineNumStr = this.colors.lineNumber(lineNum.toString().padStart(4));
|
|
72
|
+
const border = this.colors.border(' │ ');
|
|
73
|
+
output += `${lineNumStr}${border}${this.highlightSyntax(line)}\n`;
|
|
74
|
+
});
|
|
75
|
+
output += ` ${this.colors.border('╰────')}\n`;
|
|
76
|
+
// Help text with color
|
|
77
|
+
if (help) {
|
|
78
|
+
output += ` ${this.colors.help('help:')} ${help}\n`;
|
|
79
|
+
}
|
|
80
|
+
return output;
|
|
81
|
+
}
|
|
82
|
+
highlightSyntax(line) {
|
|
83
|
+
// Simple syntax highlighting for common patterns
|
|
84
|
+
return line
|
|
85
|
+
// Keywords
|
|
86
|
+
.replace(/\b(const|let|var|function|class|if|else|for|while|return|import|export|from|await|async)\b/g, match => this.colors.keyword(match))
|
|
87
|
+
// Strings
|
|
88
|
+
.replace(/(['"`])((?:\\.|(?!\1)[^\\])*)\1/g, match => this.colors.string(match))
|
|
89
|
+
// Comments
|
|
90
|
+
.replace(/(\/\/.*$|\/\*.*?\*\/)/g, match => this.colors.comment(match))
|
|
91
|
+
// Numbers
|
|
92
|
+
.replace(/\b\d+\b/g, match => this.colors.info(match));
|
|
93
|
+
}
|
|
94
|
+
}
|
package/dist/colors.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare const colors: {
|
|
2
|
+
reset: string;
|
|
3
|
+
bright: string;
|
|
4
|
+
dim: string;
|
|
5
|
+
red: string;
|
|
6
|
+
green: string;
|
|
7
|
+
yellow: string;
|
|
8
|
+
blue: string;
|
|
9
|
+
magenta: string;
|
|
10
|
+
cyan: string;
|
|
11
|
+
white: string;
|
|
12
|
+
gray: string;
|
|
13
|
+
bgRed: string;
|
|
14
|
+
bgGreen: string;
|
|
15
|
+
bgYellow: string;
|
|
16
|
+
bgBlue: string;
|
|
17
|
+
bgMagenta: string;
|
|
18
|
+
bgCyan: string;
|
|
19
|
+
brightRed: string;
|
|
20
|
+
brightGreen: string;
|
|
21
|
+
brightYellow: string;
|
|
22
|
+
brightBlue: string;
|
|
23
|
+
brightMagenta: string;
|
|
24
|
+
brightCyan: string;
|
|
25
|
+
brightWhite: string;
|
|
26
|
+
};
|
|
27
|
+
export declare class ColorFormatter {
|
|
28
|
+
private useColors;
|
|
29
|
+
constructor(useColors?: boolean);
|
|
30
|
+
private colorize;
|
|
31
|
+
error(text: string): string;
|
|
32
|
+
warning(text: string): string;
|
|
33
|
+
success(text: string): string;
|
|
34
|
+
info(text: string): string;
|
|
35
|
+
rule(text: string): string;
|
|
36
|
+
filename(text: string): string;
|
|
37
|
+
lineNumber(text: string): string;
|
|
38
|
+
highlight(text: string): string;
|
|
39
|
+
help(text: string): string;
|
|
40
|
+
keyword(text: string): string;
|
|
41
|
+
string(text: string): string;
|
|
42
|
+
comment(text: string): string;
|
|
43
|
+
border(text: string): string;
|
|
44
|
+
emphasis(text: string): string;
|
|
45
|
+
muted(text: string): string;
|
|
46
|
+
errorPointer(text: string): string;
|
|
47
|
+
errorIcon(): string;
|
|
48
|
+
warningIcon(): string;
|
|
49
|
+
successIcon(): string;
|
|
50
|
+
infoIcon(): string;
|
|
51
|
+
}
|
package/dist/colors.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ANSI color codes for terminal output
|
|
2
|
+
export const colors = {
|
|
3
|
+
// Basic colors
|
|
4
|
+
reset: '\x1b[0m',
|
|
5
|
+
bright: '\x1b[1m',
|
|
6
|
+
dim: '\x1b[2m',
|
|
7
|
+
// Foreground colors
|
|
8
|
+
red: '\x1b[31m',
|
|
9
|
+
green: '\x1b[32m',
|
|
10
|
+
yellow: '\x1b[33m',
|
|
11
|
+
blue: '\x1b[34m',
|
|
12
|
+
magenta: '\x1b[35m',
|
|
13
|
+
cyan: '\x1b[36m',
|
|
14
|
+
white: '\x1b[37m',
|
|
15
|
+
gray: '\x1b[90m',
|
|
16
|
+
// Background colors
|
|
17
|
+
bgRed: '\x1b[41m',
|
|
18
|
+
bgGreen: '\x1b[42m',
|
|
19
|
+
bgYellow: '\x1b[43m',
|
|
20
|
+
bgBlue: '\x1b[44m',
|
|
21
|
+
bgMagenta: '\x1b[45m',
|
|
22
|
+
bgCyan: '\x1b[46m',
|
|
23
|
+
// Bright foreground colors
|
|
24
|
+
brightRed: '\x1b[91m',
|
|
25
|
+
brightGreen: '\x1b[92m',
|
|
26
|
+
brightYellow: '\x1b[93m',
|
|
27
|
+
brightBlue: '\x1b[94m',
|
|
28
|
+
brightMagenta: '\x1b[95m',
|
|
29
|
+
brightCyan: '\x1b[96m',
|
|
30
|
+
brightWhite: '\x1b[97m',
|
|
31
|
+
};
|
|
32
|
+
export class ColorFormatter {
|
|
33
|
+
useColors;
|
|
34
|
+
constructor(useColors = true) {
|
|
35
|
+
this.useColors = useColors && process.stdout.isTTY;
|
|
36
|
+
}
|
|
37
|
+
colorize(text, color) {
|
|
38
|
+
if (!this.useColors)
|
|
39
|
+
return text;
|
|
40
|
+
return `${color}${text}${colors.reset}`;
|
|
41
|
+
}
|
|
42
|
+
// Error levels
|
|
43
|
+
error(text) {
|
|
44
|
+
return this.colorize(text, colors.brightRed);
|
|
45
|
+
}
|
|
46
|
+
warning(text) {
|
|
47
|
+
return this.colorize(text, colors.brightYellow);
|
|
48
|
+
}
|
|
49
|
+
success(text) {
|
|
50
|
+
return this.colorize(text, colors.brightGreen);
|
|
51
|
+
}
|
|
52
|
+
info(text) {
|
|
53
|
+
return this.colorize(text, colors.brightBlue);
|
|
54
|
+
}
|
|
55
|
+
// Syntax highlighting
|
|
56
|
+
rule(text) {
|
|
57
|
+
return this.colorize(text, colors.brightRed);
|
|
58
|
+
}
|
|
59
|
+
filename(text) {
|
|
60
|
+
return this.colorize(text, colors.brightBlue);
|
|
61
|
+
}
|
|
62
|
+
lineNumber(text) {
|
|
63
|
+
return this.colorize(text, colors.gray);
|
|
64
|
+
}
|
|
65
|
+
highlight(text) {
|
|
66
|
+
return this.colorize(text, colors.brightMagenta);
|
|
67
|
+
}
|
|
68
|
+
help(text) {
|
|
69
|
+
return this.colorize(text, colors.cyan);
|
|
70
|
+
}
|
|
71
|
+
// Code elements
|
|
72
|
+
keyword(text) {
|
|
73
|
+
return this.colorize(text, colors.magenta);
|
|
74
|
+
}
|
|
75
|
+
string(text) {
|
|
76
|
+
return this.colorize(text, colors.green);
|
|
77
|
+
}
|
|
78
|
+
comment(text) {
|
|
79
|
+
return this.colorize(text, colors.gray);
|
|
80
|
+
}
|
|
81
|
+
// UI elements
|
|
82
|
+
border(text) {
|
|
83
|
+
return this.colorize(text, colors.gray);
|
|
84
|
+
}
|
|
85
|
+
emphasis(text) {
|
|
86
|
+
return this.colorize(text, colors.bright);
|
|
87
|
+
}
|
|
88
|
+
muted(text) {
|
|
89
|
+
return this.colorize(text, colors.dim);
|
|
90
|
+
}
|
|
91
|
+
// Special formatting
|
|
92
|
+
errorPointer(text) {
|
|
93
|
+
return this.colorize(text, colors.brightMagenta);
|
|
94
|
+
}
|
|
95
|
+
errorIcon() {
|
|
96
|
+
return this.error('×');
|
|
97
|
+
}
|
|
98
|
+
warningIcon() {
|
|
99
|
+
return this.warning('⚠️');
|
|
100
|
+
}
|
|
101
|
+
successIcon() {
|
|
102
|
+
return this.success('✅');
|
|
103
|
+
}
|
|
104
|
+
infoIcon() {
|
|
105
|
+
return this.info('💡');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class OxlintHarness extends Command {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
suppressions: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
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>;
|
|
12
|
+
};
|
|
13
|
+
static args: {
|
|
14
|
+
paths: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
15
|
+
};
|
|
16
|
+
static strict: boolean;
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
}
|
package/dist/command.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
export default class OxlintHarness extends Command {
|
|
7
|
+
static summary = 'Run oxlint with bulk suppressions support';
|
|
8
|
+
static description = `
|
|
9
|
+
Runs oxlint with support for bulk suppressions similar to ESLint.
|
|
10
|
+
|
|
11
|
+
The suppression file format uses counts per rule per file:
|
|
12
|
+
{
|
|
13
|
+
"src/App.tsx": {
|
|
14
|
+
"no-unused-vars": { "count": 1 },
|
|
15
|
+
"prefer-const": { "count": 2 }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
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/',
|
|
23
|
+
];
|
|
24
|
+
static flags = {
|
|
25
|
+
suppressions: Flags.string({
|
|
26
|
+
char: 's',
|
|
27
|
+
description: 'Path to suppression file',
|
|
28
|
+
default: '.oxlint-suppressions.json',
|
|
29
|
+
}),
|
|
30
|
+
update: Flags.boolean({
|
|
31
|
+
char: 'u',
|
|
32
|
+
description: 'Update/create suppression file with current error counts',
|
|
33
|
+
default: false,
|
|
34
|
+
}),
|
|
35
|
+
'fail-on-excess': Flags.boolean({
|
|
36
|
+
description: 'Exit with non-zero code if there are unsuppressed errors',
|
|
37
|
+
default: true,
|
|
38
|
+
allowNo: true,
|
|
39
|
+
}),
|
|
40
|
+
'show-code': Flags.integer({
|
|
41
|
+
description: 'Show code snippets for files with N or fewer errors (0 to disable)',
|
|
42
|
+
default: 3,
|
|
43
|
+
}),
|
|
44
|
+
help: Flags.help({ char: 'h' }),
|
|
45
|
+
};
|
|
46
|
+
static args = {
|
|
47
|
+
paths: Args.string({
|
|
48
|
+
description: 'Files or directories to lint (passed to oxlint)',
|
|
49
|
+
required: false,
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
static strict = false; // Allow additional args to be passed to oxlint
|
|
53
|
+
async run() {
|
|
54
|
+
const { flags, argv } = await this.parse(OxlintHarness);
|
|
55
|
+
const colors = new ColorFormatter();
|
|
56
|
+
try {
|
|
57
|
+
// Run oxlint with remaining args
|
|
58
|
+
const runner = new OxlintRunner();
|
|
59
|
+
const diagnostics = await runner.run(argv);
|
|
60
|
+
// Handle suppression logic
|
|
61
|
+
const suppressionManager = new SuppressionManager(flags.suppressions);
|
|
62
|
+
if (flags.update) {
|
|
63
|
+
// Update mode: generate/update suppression file
|
|
64
|
+
const currentSuppressions = suppressionManager.loadSuppressions();
|
|
65
|
+
const updatedSuppressions = suppressionManager.updateSuppressions(currentSuppressions, diagnostics);
|
|
66
|
+
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())}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Normal mode: check suppressions
|
|
72
|
+
const suppressions = suppressionManager.loadSuppressions();
|
|
73
|
+
const excessErrors = suppressionManager.findExcessErrors(diagnostics, suppressions);
|
|
74
|
+
if (excessErrors.length === 0) {
|
|
75
|
+
this.log(`${colors.successIcon()} ${colors.success('All errors are suppressed')}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Report excess errors
|
|
79
|
+
const reporter = new ErrorReporter();
|
|
80
|
+
reporter.reportExcessErrors(excessErrors, flags['show-code']);
|
|
81
|
+
if (flags['fail-on-excess']) {
|
|
82
|
+
this.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
this.error(error instanceof Error ? error.message : String(error));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CodeSnippetExtractor } from './code-snippet.js';
|
|
2
|
+
import { ColorFormatter } from './colors.js';
|
|
3
|
+
export class ErrorReporter {
|
|
4
|
+
snippetExtractor = new CodeSnippetExtractor();
|
|
5
|
+
colors = new ColorFormatter();
|
|
6
|
+
reportExcessErrors(excessErrors, showCodeThreshold = 3) {
|
|
7
|
+
console.error(`${this.colors.error('❌')} Found unsuppressed errors:\n`);
|
|
8
|
+
let totalExcess = 0;
|
|
9
|
+
// Group errors by file for better readability
|
|
10
|
+
const errorsByFile = new Map();
|
|
11
|
+
for (const error of excessErrors) {
|
|
12
|
+
if (!errorsByFile.has(error.filename)) {
|
|
13
|
+
errorsByFile.set(error.filename, []);
|
|
14
|
+
}
|
|
15
|
+
errorsByFile.get(error.filename).push(error);
|
|
16
|
+
}
|
|
17
|
+
for (const [filename, fileErrors] of errorsByFile.entries()) {
|
|
18
|
+
console.error(`📄 ${this.colors.filename(filename)}:`);
|
|
19
|
+
for (const error of fileErrors) {
|
|
20
|
+
const excess = error.actual - error.expected;
|
|
21
|
+
totalExcess += excess;
|
|
22
|
+
const excessText = excess === 1 ? 'error' : 'errors';
|
|
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);
|
|
32
|
+
}
|
|
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)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
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)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (error.diagnostics.length > exampleCount) {
|
|
55
|
+
console.error(` ${this.colors.muted(`... and ${error.diagnostics.length - exampleCount} more`)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// 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`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
console.error(`\n📊 ${this.colors.info('Summary:')}`);
|
|
64
|
+
console.error(` • Files with issues: ${this.colors.emphasis(errorsByFile.size.toString())}`);
|
|
65
|
+
console.error(` • Rules with excess errors: ${this.colors.emphasis(excessErrors.length.toString())}`);
|
|
66
|
+
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]')}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { OxlintRunner } from './oxlint-runner.js';
|
|
2
|
+
export { SuppressionManager } from './suppression-manager.js';
|
|
3
|
+
export { ErrorReporter } from './error-reporter.js';
|
|
4
|
+
export { CodeSnippetExtractor } from './code-snippet.js';
|
|
5
|
+
export { ColorFormatter } from './colors.js';
|
|
6
|
+
export * from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { OxlintRunner } from './oxlint-runner.js';
|
|
2
|
+
export { SuppressionManager } from './suppression-manager.js';
|
|
3
|
+
export { ErrorReporter } from './error-reporter.js';
|
|
4
|
+
export { CodeSnippetExtractor } from './code-snippet.js';
|
|
5
|
+
export { ColorFormatter } from './colors.js';
|
|
6
|
+
export * from './types.js';
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { resolve, dirname, parse } from 'path';
|
|
4
|
+
export class OxlintRunner {
|
|
5
|
+
findLockFile(startDir) {
|
|
6
|
+
let currentDir = startDir;
|
|
7
|
+
const root = parse(currentDir).root;
|
|
8
|
+
while (currentDir !== root) {
|
|
9
|
+
// Check for lock files in order of preference
|
|
10
|
+
const lockFiles = ['pnpm-lock.yaml', 'yarn.lock', 'package-lock.json'];
|
|
11
|
+
for (const lockFile of lockFiles) {
|
|
12
|
+
if (existsSync(resolve(currentDir, lockFile))) {
|
|
13
|
+
return resolve(currentDir, lockFile);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Move up one directory
|
|
17
|
+
const parent = dirname(currentDir);
|
|
18
|
+
if (parent === currentDir)
|
|
19
|
+
break; // Reached filesystem root
|
|
20
|
+
currentDir = parent;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
detectPackageManager() {
|
|
25
|
+
const cwd = process.cwd();
|
|
26
|
+
const lockFile = this.findLockFile(cwd);
|
|
27
|
+
if (lockFile) {
|
|
28
|
+
const lockFileName = parse(lockFile).base;
|
|
29
|
+
if (lockFileName === 'pnpm-lock.yaml') {
|
|
30
|
+
return { command: 'pnpm', args: ['exec', 'oxlint'] };
|
|
31
|
+
}
|
|
32
|
+
if (lockFileName === 'yarn.lock') {
|
|
33
|
+
return { command: 'yarn', args: ['exec', 'oxlint'] };
|
|
34
|
+
}
|
|
35
|
+
if (lockFileName === 'package-lock.json') {
|
|
36
|
+
return { command: 'npx', args: ['oxlint'] };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Fallback to direct oxlint if no lock file found
|
|
40
|
+
return { command: 'oxlint', args: [] };
|
|
41
|
+
}
|
|
42
|
+
async run(args = []) {
|
|
43
|
+
const packageManager = this.detectPackageManager();
|
|
44
|
+
const finalArgs = [...packageManager.args, '-f', 'json', ...args];
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const process = spawn(packageManager.command, finalArgs, {
|
|
47
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
48
|
+
});
|
|
49
|
+
let stdout = '';
|
|
50
|
+
let stderr = '';
|
|
51
|
+
process.stdout.on('data', (data) => {
|
|
52
|
+
stdout += data.toString();
|
|
53
|
+
});
|
|
54
|
+
process.stderr.on('data', (data) => {
|
|
55
|
+
stderr += data.toString();
|
|
56
|
+
});
|
|
57
|
+
process.on('close', (code) => {
|
|
58
|
+
try {
|
|
59
|
+
// oxlint exits with non-zero when linting issues are found
|
|
60
|
+
// We still want to parse the JSON output
|
|
61
|
+
const diagnostics = this.parseOxlintOutput(stdout);
|
|
62
|
+
resolve(diagnostics);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
reject(new Error(`Failed to parse oxlint output: ${error instanceof Error ? error.message : String(error)}\nStdout: ${stdout}\nStderr: ${stderr}`));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
process.on('error', (error) => {
|
|
69
|
+
reject(new Error(`Failed to run ${packageManager.command}: ${error.message}`));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
parseOxlintOutput(output) {
|
|
74
|
+
if (!output.trim()) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(output);
|
|
79
|
+
const diagnostics = [];
|
|
80
|
+
// Handle the new oxlint JSON format with diagnostics array
|
|
81
|
+
if (parsed.diagnostics && Array.isArray(parsed.diagnostics)) {
|
|
82
|
+
for (const diagnostic of parsed.diagnostics) {
|
|
83
|
+
const processed = {
|
|
84
|
+
filename: diagnostic.filename || 'unknown',
|
|
85
|
+
rule: diagnostic.code || 'unknown',
|
|
86
|
+
severity: diagnostic.severity,
|
|
87
|
+
message: diagnostic.message,
|
|
88
|
+
help: diagnostic.help,
|
|
89
|
+
url: diagnostic.url
|
|
90
|
+
};
|
|
91
|
+
// Extract line/column from labels if available
|
|
92
|
+
if (diagnostic.labels && diagnostic.labels.length > 0) {
|
|
93
|
+
const firstLabel = diagnostic.labels[0];
|
|
94
|
+
if (firstLabel.span) {
|
|
95
|
+
processed.line = firstLabel.span.line;
|
|
96
|
+
processed.column = firstLabel.span.column;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
diagnostics.push(processed);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Fallback to old format if needed
|
|
103
|
+
else if (typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
104
|
+
for (const [filename, fileDiagnostics] of Object.entries(parsed)) {
|
|
105
|
+
if (Array.isArray(fileDiagnostics)) {
|
|
106
|
+
for (const diagnostic of fileDiagnostics) {
|
|
107
|
+
const processed = {
|
|
108
|
+
filename,
|
|
109
|
+
rule: diagnostic.rule_id || diagnostic.code || 'unknown',
|
|
110
|
+
severity: diagnostic.severity,
|
|
111
|
+
message: diagnostic.message,
|
|
112
|
+
help: diagnostic.help,
|
|
113
|
+
url: diagnostic.url
|
|
114
|
+
};
|
|
115
|
+
if (diagnostic.labels && diagnostic.labels.length > 0) {
|
|
116
|
+
const firstLabel = diagnostic.labels[0];
|
|
117
|
+
if (firstLabel.span) {
|
|
118
|
+
processed.line = firstLabel.span.line || firstLabel.span.start;
|
|
119
|
+
processed.column = firstLabel.span.column || (firstLabel.span.end - firstLabel.span.start);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
diagnostics.push(processed);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return diagnostics;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
throw new Error(`Invalid JSON output: ${error instanceof Error ? error.message : String(error)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SuppressionFile, ProcessedDiagnostic, ExcessError } from './types.js';
|
|
2
|
+
export declare class SuppressionManager {
|
|
3
|
+
private suppressionFile;
|
|
4
|
+
constructor(suppressionFile?: string);
|
|
5
|
+
loadSuppressions(): SuppressionFile;
|
|
6
|
+
saveSuppressions(suppressions: SuppressionFile): void;
|
|
7
|
+
generateSuppressions(diagnostics: ProcessedDiagnostic[]): SuppressionFile;
|
|
8
|
+
findExcessErrors(diagnostics: ProcessedDiagnostic[], suppressions: SuppressionFile): ExcessError[];
|
|
9
|
+
updateSuppressions(currentSuppressions: SuppressionFile, diagnostics: ProcessedDiagnostic[]): SuppressionFile;
|
|
10
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
2
|
+
export class SuppressionManager {
|
|
3
|
+
suppressionFile;
|
|
4
|
+
constructor(suppressionFile = '.oxlint-suppressions.json') {
|
|
5
|
+
this.suppressionFile = suppressionFile;
|
|
6
|
+
}
|
|
7
|
+
loadSuppressions() {
|
|
8
|
+
if (!existsSync(this.suppressionFile)) {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const content = readFileSync(this.suppressionFile, 'utf8');
|
|
13
|
+
return JSON.parse(content);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
throw new Error(`Failed to parse suppression file ${this.suppressionFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
saveSuppressions(suppressions) {
|
|
20
|
+
try {
|
|
21
|
+
const content = JSON.stringify(suppressions, null, 2);
|
|
22
|
+
writeFileSync(this.suppressionFile, content, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new Error(`Failed to write suppression file ${this.suppressionFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
generateSuppressions(diagnostics) {
|
|
29
|
+
const suppressions = {};
|
|
30
|
+
// Group diagnostics by file and rule
|
|
31
|
+
const counts = new Map();
|
|
32
|
+
for (const diagnostic of diagnostics) {
|
|
33
|
+
if (!counts.has(diagnostic.filename)) {
|
|
34
|
+
counts.set(diagnostic.filename, new Map());
|
|
35
|
+
}
|
|
36
|
+
const fileRules = counts.get(diagnostic.filename);
|
|
37
|
+
const currentCount = fileRules.get(diagnostic.rule) || 0;
|
|
38
|
+
fileRules.set(diagnostic.rule, currentCount + 1);
|
|
39
|
+
}
|
|
40
|
+
// Convert to suppression format
|
|
41
|
+
for (const [filename, rules] of counts.entries()) {
|
|
42
|
+
suppressions[filename] = {};
|
|
43
|
+
for (const [rule, count] of rules.entries()) {
|
|
44
|
+
suppressions[filename][rule] = { count };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return suppressions;
|
|
48
|
+
}
|
|
49
|
+
findExcessErrors(diagnostics, suppressions) {
|
|
50
|
+
const excessErrors = [];
|
|
51
|
+
// Group diagnostics by file and rule
|
|
52
|
+
const actualCounts = new Map();
|
|
53
|
+
for (const diagnostic of diagnostics) {
|
|
54
|
+
if (!actualCounts.has(diagnostic.filename)) {
|
|
55
|
+
actualCounts.set(diagnostic.filename, new Map());
|
|
56
|
+
}
|
|
57
|
+
const fileRules = actualCounts.get(diagnostic.filename);
|
|
58
|
+
if (!fileRules.has(diagnostic.rule)) {
|
|
59
|
+
fileRules.set(diagnostic.rule, []);
|
|
60
|
+
}
|
|
61
|
+
fileRules.get(diagnostic.rule).push(diagnostic);
|
|
62
|
+
}
|
|
63
|
+
// Compare actual vs suppressed counts
|
|
64
|
+
for (const [filename, rules] of actualCounts.entries()) {
|
|
65
|
+
for (const [rule, diagnosticsForRule] of rules.entries()) {
|
|
66
|
+
const actual = diagnosticsForRule.length;
|
|
67
|
+
const expected = suppressions[filename]?.[rule]?.count || 0;
|
|
68
|
+
if (actual > expected) {
|
|
69
|
+
excessErrors.push({
|
|
70
|
+
rule,
|
|
71
|
+
filename,
|
|
72
|
+
expected,
|
|
73
|
+
actual,
|
|
74
|
+
diagnostics: diagnosticsForRule
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return excessErrors;
|
|
80
|
+
}
|
|
81
|
+
updateSuppressions(currentSuppressions, diagnostics) {
|
|
82
|
+
const newSuppressions = this.generateSuppressions(diagnostics);
|
|
83
|
+
// Merge with existing suppressions, using new counts
|
|
84
|
+
const updated = { ...currentSuppressions };
|
|
85
|
+
for (const [filename, rules] of Object.entries(newSuppressions)) {
|
|
86
|
+
updated[filename] = { ...updated[filename], ...rules };
|
|
87
|
+
}
|
|
88
|
+
// Remove files with no rules
|
|
89
|
+
for (const [filename, rules] of Object.entries(updated)) {
|
|
90
|
+
if (Object.keys(rules).length === 0) {
|
|
91
|
+
delete updated[filename];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return updated;
|
|
95
|
+
}
|
|
96
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface OxlintDiagnostic {
|
|
2
|
+
severity: 'error' | 'warning';
|
|
3
|
+
message: string;
|
|
4
|
+
labels: Array<{
|
|
5
|
+
span: {
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
};
|
|
9
|
+
message: string;
|
|
10
|
+
}>;
|
|
11
|
+
rule_id?: string;
|
|
12
|
+
help?: string;
|
|
13
|
+
url?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface OxlintOutput {
|
|
16
|
+
[filename: string]: OxlintDiagnostic[];
|
|
17
|
+
}
|
|
18
|
+
export interface SuppressionRule {
|
|
19
|
+
count: number;
|
|
20
|
+
}
|
|
21
|
+
export interface SuppressionFile {
|
|
22
|
+
[filename: string]: {
|
|
23
|
+
[ruleName: string]: SuppressionRule;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export interface ProcessedDiagnostic {
|
|
27
|
+
filename: string;
|
|
28
|
+
rule: string;
|
|
29
|
+
severity: 'error' | 'warning';
|
|
30
|
+
message: string;
|
|
31
|
+
line?: number;
|
|
32
|
+
column?: number;
|
|
33
|
+
help?: string;
|
|
34
|
+
url?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ExcessError {
|
|
37
|
+
rule: string;
|
|
38
|
+
filename: string;
|
|
39
|
+
expected: number;
|
|
40
|
+
actual: number;
|
|
41
|
+
diagnostics: ProcessedDiagnostic[];
|
|
42
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oxlint-harness",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A harness for oxlint with bulk suppressions support",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"oxlint-harness": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsgo",
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"dev": "node src/cli.ts",
|
|
19
|
+
"test": "vitest",
|
|
20
|
+
"test:watch": "vitest --watch",
|
|
21
|
+
"lint": "oxlint src/",
|
|
22
|
+
"typecheck": "tsgo"
|
|
23
|
+
},
|
|
24
|
+
"keywords": ["oxlint", "linting", "suppressions", "eslint", "bulk-suppressions", "cli"],
|
|
25
|
+
"author": "Generated with Claude Code",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/MIreland/oxlint-harness.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/MIreland/oxlint-harness/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/MIreland/oxlint-harness#readme",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@oclif/core": "^4.0.0",
|
|
37
|
+
"@oclif/plugin-help": "^6.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^20.10.0",
|
|
41
|
+
"@typescript/native-preview": "7.0.0-dev.20251222.1",
|
|
42
|
+
"vitest": "4.0.16"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=22"
|
|
46
|
+
}
|
|
47
|
+
}
|