Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 5 additions & 66 deletions apps/oxlint/conformance/snapshots/eslint.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
| Status | Count | % |
| ----------------- | ----- | ------ |
| Total rules | 292 | 100.0% |
| Fully passing | 286 | 97.9% |
| Partially passing | 6 | 2.1% |
| Fully passing | 287 | 98.3% |
| Partially passing | 5 | 1.7% |
| Fully failing | 0 | 0.0% |
| Load errors | 0 | 0.0% |
| No tests run | 0 | 0.0% |
Expand All @@ -18,8 +18,8 @@
| Status | Count | % |
| ----------- | ----- | ------ |
| Total tests | 33159 | 100.0% |
| Passing | 32869 | 99.1% |
| Failing | 12 | 0.0% |
| Passing | 32871 | 99.1% |
| Failing | 10 | 0.0% |
| Skipped | 278 | 0.8% |

## Fully Passing Rules
Expand Down Expand Up @@ -303,6 +303,7 @@
- `symbol-description` (8 tests)
- `template-curly-spacing` (57 tests)
- `template-tag-spacing` (63 tests)
- `unicode-bom` (7 tests)
- `use-isnan` (214 tests)
- `valid-typeof` (54 tests)
- `vars-on-top` (61 tests)
Expand All @@ -318,7 +319,6 @@
- `no-unused-labels` - 23 / 26 (88.5%)
- `object-curly-newline` - 143 / 144 (99.3%)
- `space-in-parens` - 137 / 139 (98.6%)
- `unicode-bom` - 5 / 7 (71.4%)

## Rules with Failures Detail

Expand Down Expand Up @@ -771,64 +771,3 @@ AssertionError [ERR_ASSERTION]: Output is incorrect
at apps/oxlint/dist/plugins-dev.js
at it (apps/oxlint/conformance/src/capture.ts:123:5)


### `unicode-bom`

Pass: 5 / 7 (71.4%)
Fail: 2 / 7 (28.6%)
Skip: 0 / 7 (0.0%)

#### unicode-bom > invalid

```js
 var a = 123;
```

```json
{
"output": " var a = 123;",
"errors": [
{
"messageId": "unexpected",
"line": 1,
"column": 1
}
]
}
```

Error: Failed to apply fixes
at assertInvalidTestCasePasses (apps/oxlint/dist/plugins-dev.js)
at runInvalidTestCase (apps/oxlint/dist/plugins-dev.js)
at apps/oxlint/dist/plugins-dev.js
at it (apps/oxlint/conformance/src/capture.ts:123:5)


#### unicode-bom > invalid

```js
 var a = 123;
```

```json
{
"output": " var a = 123;",
"options": [
"never"
],
"errors": [
{
"messageId": "unexpected",
"line": 1,
"column": 1
}
]
}
```

Error: Failed to apply fixes
at assertInvalidTestCasePasses (apps/oxlint/dist/plugins-dev.js)
at runInvalidTestCase (apps/oxlint/dist/plugins-dev.js)
at apps/oxlint/dist/plugins-dev.js
at it (apps/oxlint/conformance/src/capture.ts:123:5)

3 changes: 3 additions & 0 deletions apps/oxlint/src-js/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export declare const enum Severity {
* one group per diagnostic which provides fixes.
* Each inner array should have length of 1 at minimum.
*
* If source text starts with a BOM, `JSFix`es must have offsets relative to the start
* of the source text *without* the BOM.
*
* Each group's fixes are merged, then all merged fixes are applied to `source_text`.
*
* Fix ranges are converted from UTF-16 code units to UTF-8 bytes.
Expand Down
26 changes: 22 additions & 4 deletions apps/oxlint/src-js/plugins/fix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export type FixFn = (

/**
* Fix, as returned by `fix` function.
*
* `range` offsets are relative to start of the source text.
* When the file has a BOM, they are relative to the start of the source text *without* the BOM.
*
* To represent a position *before* a BOM, -1 is used to mean "before the BOM".
* ESLint's `unicode-bom` rule produces a fix `{ range: [-1, 0], text: "" }` to remove a BOM.
*/
export interface Fix {
range: Range;
Expand All @@ -26,10 +32,19 @@ export interface Fix {

/**
* Fix, in form sent to Rust.
*
* Offsets have 1 added to them, so that -1 in `Fix`'s `range` is represented as 0 in `FixReport`.
* This means that both `startPlusOne` and `endPlusOne` are valid `u32`s, even if original range elements were -1.
*
* `Fix { range: [0, 10], text: "xxx" }` -> `FixReport { startPlusOne: 1, endPlusOne: 11, text: "xxx" }`.
* `Fix { range: [-1, 0], text: "" }` -> `FixReport { startPlusOne: 0, endPlusOne: 1, text: "" }`.
*
* Range offsets can be -1 when the file has a BOM, and the fix starts before the BOM.
* ESLint's `unicode-bom` rule produces a fix `{ range: [-1, 0], text: "" }` to remove a BOM.
*/
export interface FixReport {
start: number;
end: number;
startPlusOne: number;
endPlusOne: number;
text: string;
}

Expand Down Expand Up @@ -223,8 +238,11 @@ function validateAndConvertFix(fix: Fix): FixReport {
const start = range[0],
end = range[1];
if (typeof start === "number" && typeof end === "number") {
// Converting `text` to string follows ESLint, which does that implicitly
return { start, end, text: String(text) };
// Add 1 to `start` and `end`, to allow original offsets to be -1 (meaning "before the BOM"),
// and `FixReport`'s `startPlusOne` and `endPlusOne` are still valid `u32`s on Rust side.
//
// Converting `text` to string follows ESLint, which does that implicitly.
return { startPlusOne: start + 1, endPlusOne: end + 1, text: String(text) };
}
}

Expand Down
19 changes: 16 additions & 3 deletions apps/oxlint/src/js_plugins/fix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ use oxc_ast_visit::utf8_to_utf16::Utf8ToUtf16;
use oxc_diagnostics::OxcDiagnostic;
use oxc_linter::{Fixer, JsFix, Message, PossibleFixes, convert_and_merge_js_fixes};

const BOM: &str = "\u{feff}";
#[expect(clippy::cast_possible_truncation)]
const BOM_LEN: u32 = BOM.len() as u32;

/// Apply fixes to source text and return the fixed code.
///
/// - `source_text` is the original source code.
/// - `fixes_json` is a JSON string containing `Vec<Vec<JsFix>>` — an array of fix groups,
/// one group per diagnostic which provides fixes.
/// Each inner array should have length of 1 at minimum.
///
/// If source text starts with a BOM, `JSFix`es must have offsets relative to the start
/// of the source text *without* the BOM.
///
/// Each group's fixes are merged, then all merged fixes are applied to `source_text`.
///
/// Fix ranges are converted from UTF-16 code units to UTF-8 bytes.
Expand All @@ -20,14 +27,20 @@ pub fn apply_fixes(source_text: String, fixes_json: String) -> Option<String> {
// Deserialize fixes JSON
let fix_groups: Vec<Vec<JsFix>> = serde_json::from_str(&fixes_json).ok()?;

// Create `Utf8ToUtf16` converter
let span_converter = Utf8ToUtf16::new(&source_text);
// Create `Utf8ToUtf16` converter.
// If file has a BOM, trim it off start of the source text before creating the converter.
let has_bom = source_text.starts_with(BOM);
let span_converter = if has_bom {
Utf8ToUtf16::new_with_offset(&source_text[BOM_LEN as usize..], BOM_LEN)
} else {
Utf8ToUtf16::new(&source_text)
};

// Merge fix groups into a single fix per group
let messages = fix_groups
.into_iter()
.map(|group| {
convert_and_merge_js_fixes(group, &source_text, &span_converter)
convert_and_merge_js_fixes(group, &source_text, &span_converter, has_bom)
.ok()
.map(|fix| Message::new(OxcDiagnostic::error(""), PossibleFixes::Single(fix)))
})
Expand Down
2 changes: 2 additions & 0 deletions apps/oxlint/test/fixtures/fixes/files/bom_remove.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
a = c;
d = b
2 changes: 2 additions & 0 deletions apps/oxlint/test/fixtures/fixes/files/bom_remove2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
a = c;
d = b
22 changes: 17 additions & 5 deletions apps/oxlint/test/fixtures/fixes/fix.snap.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
```
x Error running JS plugin.
| File path: <fixture>/files/range_end_negative.js
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `-10`, expected u32 at line 1 column 116
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `-9`, expected u32 at line 1 column 129

x Plugin `fixes-plugin/fixes` returned invalid fixes.
| File path: <fixture>/files/range_end_out_of_bounds.js
Expand All @@ -17,7 +17,7 @@

x Error running JS plugin.
| File path: <fixture>/files/range_end_too_large.js
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `4294967296`, expected u32 at line 1 column 124
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `4294967297`, expected u32 at line 1 column 138

x Plugin `fixes-plugin/fixes` returned invalid fixes.
| File path: <fixture>/files/range_start_after_end.js
Expand All @@ -29,11 +29,11 @@

x Error running JS plugin.
| File path: <fixture>/files/range_start_negative.js
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `-10`, expected u32 at line 1 column 110
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `-9`, expected u32 at line 1 column 116

x Error running JS plugin.
| File path: <fixture>/files/range_start_too_large.js
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `4294967296`, expected u32 at line 1 column 118
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `4294967297`, expected u32 at line 1 column 125

x fixes-plugin(fixes): end out of bounds
,-[files/range_end_out_of_bounds.js:1:5]
Expand All @@ -60,7 +60,7 @@
`----

Found 0 warnings and 12 errors.
Finished in Xms on 10 files with 1 rules using X threads.
Finished in Xms on 12 files with 1 rules using X threads.
```

# stderr
Expand All @@ -80,6 +80,18 @@ rage = abacus
rage = abacus
```

# File altered: files/bom_remove.js
```
daddy = magic;
damned = abacus
```

# File altered: files/bom_remove2.js
```
daddy = magic;
damned = abacus
```

# File altered: files/index.js
```

Expand Down
80 changes: 74 additions & 6 deletions apps/oxlint/test/fixtures/fixes/output.snap.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
```
x Error running JS plugin.
| File path: <fixture>/files/range_end_negative.js
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `-10`, expected u32 at line 1 column 116
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `-9`, expected u32 at line 1 column 129

x Plugin `fixes-plugin/fixes` returned invalid fixes.
| File path: <fixture>/files/range_end_out_of_bounds.js
Expand All @@ -17,7 +17,7 @@

x Error running JS plugin.
| File path: <fixture>/files/range_end_too_large.js
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `4294967296`, expected u32 at line 1 column 124
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `4294967297`, expected u32 at line 1 column 138

x Plugin `fixes-plugin/fixes` returned invalid fixes.
| File path: <fixture>/files/range_start_after_end.js
Expand All @@ -29,11 +29,11 @@

x Error running JS plugin.
| File path: <fixture>/files/range_start_negative.js
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `-10`, expected u32 at line 1 column 110
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `-9`, expected u32 at line 1 column 116

x Error running JS plugin.
| File path: <fixture>/files/range_start_too_large.js
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `4294967296`, expected u32 at line 1 column 118
| Failed to deserialize JSON returned by `lintFile`: invalid value: integer `4294967297`, expected u32 at line 1 column 125

x fixes-plugin(fixes): Replace "a" with "daddy"
,-[files/bom.js:1:4]
Expand Down Expand Up @@ -91,6 +91,74 @@
: ^
`----

x fixes-plugin(fixes): Replace "a" with "daddy"
,-[files/bom_remove.js:1:4]
1 | a = c;
: ^
2 | d = b
`----

x fixes-plugin(fixes): Remove BOM
,-[files/bom_remove.js:1:4]
1 | ,-> a = c;
2 | `-> d = b
`----

x fixes-plugin(fixes): Prefix "c" with "magi"
,-[files/bom_remove.js:1:8]
1 | a = c;
: ^
2 | d = b
`----

x fixes-plugin(fixes): Prefix "d" with "damne"
,-[files/bom_remove.js:2:1]
1 | a = c;
2 | d = b
: ^
`----

x fixes-plugin(fixes): Replace "b" with "abacus"
,-[files/bom_remove.js:2:5]
1 | a = c;
2 | d = b
: ^
`----

x fixes-plugin(fixes): Replace "a" with "daddy"
,-[files/bom_remove2.js:1:4]
1 | a = c;
: ^
2 | d = b
`----

x fixes-plugin(fixes): Remove BOM multiple
,-[files/bom_remove2.js:1:4]
1 | ,-> a = c;
2 | `-> d = b
`----

x fixes-plugin(fixes): Prefix "c" with "magi"
,-[files/bom_remove2.js:1:8]
1 | a = c;
: ^
2 | d = b
`----

x fixes-plugin(fixes): Prefix "d" with "damne"
,-[files/bom_remove2.js:2:1]
1 | a = c;
2 | d = b
: ^
`----

x fixes-plugin(fixes): Replace "b" with "abacus"
,-[files/bom_remove2.js:2:5]
1 | a = c;
2 | d = b
: ^
`----

x fixes-plugin(fixes): Remove debugger statement
,-[files/index.js:1:1]
1 | debugger;
Expand Down Expand Up @@ -237,8 +305,8 @@
: ^
`----

Found 0 warnings and 36 errors.
Finished in Xms on 10 files with 1 rules using X threads.
Found 0 warnings and 46 errors.
Finished in Xms on 12 files with 1 rules using X threads.
```

# stderr
Expand Down
Loading
Loading