mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
ecd24560a1
Fix an edge case bug where Oxlint was unable to handle fixes with with offset `-1`. Offset `-1` is used in files which contain a BOM to represent "before the BOM".
ESLint's `unicode-bom` rule produces a fix `{ range: [-1, 0], text: "" }` to remove BOM from a file with such a BOM.
`-1` can't be represented as a `u32`, so we have to take a rather annoying workaround - add 1 to offsets before sending them to Rust.
An alternative would have been to make `start` and `end` `i64`s so they can represent `-1`, but this would seem wasteful since we only need to represent -1, not any other negative value. So adding 1 to offsets is cheaper.
This fixes 2 ESLint conformance tests.
305 lines
9.5 KiB
TypeScript
305 lines
9.5 KiB
TypeScript
import { sep as pathSep } from "node:path";
|
|
import assert from "node:assert";
|
|
|
|
import type { Diagnostic, Node, Plugin, Rule } from "#oxlint/plugins";
|
|
|
|
const U32_MAX_PLUS_ONE = 2 ** 32;
|
|
|
|
const rule: Rule = {
|
|
meta: {
|
|
fixable: "code",
|
|
},
|
|
create(context) {
|
|
// Check file has not been formatted by accident.
|
|
// We want the fixture files not to have trailing whitespace to check fixes at very end of file.
|
|
const sourceText = context.sourceCode.text;
|
|
assert(!sourceText.endsWith("\n"), "Fixture file has been formatted");
|
|
|
|
const path = context.filename;
|
|
const filename = path.slice(path.lastIndexOf(pathSep) + 1);
|
|
|
|
let debuggerCount = 0;
|
|
|
|
return {
|
|
Program(node) {
|
|
// Remove BOM in `bom_remove.js` and `bom_remove2.js`.
|
|
// ESLint's `unicode-bom` rule returns fixes with `range: [-1, 0]` to remove BOM.
|
|
// Check behavior is correct for both `fix` returning a single fix and returning multiple fixes.
|
|
if (filename === "bom_remove.js") {
|
|
context.report({
|
|
message: "Remove BOM",
|
|
node,
|
|
fix(fixer) {
|
|
return fixer.removeRange([-1, 0]);
|
|
},
|
|
});
|
|
} else if (filename === "bom_remove2.js") {
|
|
context.report({
|
|
message: "Remove BOM multiple",
|
|
node,
|
|
fix(fixer) {
|
|
return [fixer.removeRange([0, 0]), fixer.removeRange([-1, 0])];
|
|
},
|
|
});
|
|
}
|
|
},
|
|
|
|
DebuggerStatement(node) {
|
|
debuggerCount++;
|
|
|
|
let thisIsReport;
|
|
const report: Diagnostic = {
|
|
message: "Remove debugger statement",
|
|
node,
|
|
fix(fixer) {
|
|
thisIsReport = this === report;
|
|
if (debuggerCount === 1) return fixer.remove(node);
|
|
return fixer.removeRange([node.start, node.end]);
|
|
},
|
|
};
|
|
context.report(report);
|
|
|
|
if (!thisIsReport) {
|
|
context.report({ message: `this in fix function is not report object`, node });
|
|
}
|
|
},
|
|
|
|
Identifier(node) {
|
|
switch (node.name) {
|
|
case "a":
|
|
return context.report({
|
|
message: 'Replace "a" with "daddy"',
|
|
node,
|
|
fix(fixer) {
|
|
return fixer.replaceText(node, "daddy");
|
|
},
|
|
});
|
|
|
|
case "b":
|
|
return context.report({
|
|
message: 'Replace "b" with "abacus"',
|
|
node,
|
|
fix(fixer) {
|
|
return fixer.replaceTextRange([node.start, node.end], "abacus");
|
|
},
|
|
});
|
|
|
|
case "c":
|
|
return context.report({
|
|
message: 'Prefix "c" with "magi"',
|
|
node,
|
|
fix(fixer) {
|
|
return fixer.insertTextBefore(node, "magi");
|
|
},
|
|
});
|
|
|
|
case "d":
|
|
return context.report({
|
|
message: 'Prefix "d" with "damne"',
|
|
node,
|
|
fix(fixer) {
|
|
return fixer.insertTextBeforeRange([node.start, node.end], "damne");
|
|
},
|
|
});
|
|
|
|
case "e":
|
|
return context.report({
|
|
message: 'Postfix "e" with "lephant"',
|
|
node,
|
|
fix(fixer) {
|
|
return fixer.insertTextAfter(node, "lephant");
|
|
},
|
|
});
|
|
|
|
case "f":
|
|
return context.report({
|
|
message: 'Postfix "f" with "eck"',
|
|
node,
|
|
fix(fixer) {
|
|
return fixer.insertTextAfterRange([node.start, node.end], "eck");
|
|
},
|
|
});
|
|
|
|
case "g":
|
|
return context.report({
|
|
message: 'Replace "g" with "rage"',
|
|
node,
|
|
fix(fixer) {
|
|
// Add text before and after the node only, to test combining fixes that include original source.
|
|
// Fixes can be in any order.
|
|
return [
|
|
// Test that any object with `range` property works
|
|
fixer.insertTextAfter({ range: [node.start, node.end] }, "e"),
|
|
fixer.insertTextBefore(node, "ra"),
|
|
];
|
|
},
|
|
});
|
|
|
|
case "h":
|
|
return context.report({
|
|
message: 'Replace "h" with "dangermouse"',
|
|
node,
|
|
fix(fixer) {
|
|
// Fixes can be in any order
|
|
const { range } = node;
|
|
return [
|
|
fixer.replaceTextRange(range, "er"),
|
|
fixer.insertTextAfterRange(range, "mouse"),
|
|
fixer.insertTextBeforeRange(range, "dang"),
|
|
];
|
|
},
|
|
});
|
|
|
|
case "i":
|
|
return context.report({
|
|
message: 'Replace "i" with "granular"',
|
|
node,
|
|
// `fix` can be a generator function
|
|
*fix(fixer) {
|
|
yield fixer.insertTextBefore(node, "gra");
|
|
yield fixer.replaceText(node, "nu");
|
|
// Test that any object with `range` property works
|
|
yield fixer.insertTextAfter({ range: [node.start, node.end] } as Node, "lar");
|
|
},
|
|
});
|
|
|
|
case "j":
|
|
return context.report({
|
|
message: 'Replace "j" with "cowabunga"',
|
|
node,
|
|
// `fix` can be a generator function
|
|
*fix(fixer) {
|
|
// Fixes can be in any order
|
|
const { range } = node;
|
|
yield fixer.insertTextAfterRange(range, "bunga");
|
|
yield fixer.replaceTextRange(range, "a");
|
|
yield fixer.insertTextBeforeRange(range, "cow");
|
|
},
|
|
});
|
|
|
|
// Test behavior when `fix` returns a fix with illegal range.
|
|
// Check behavior is correct for both `fix` returning a single fix and returning multiple fixes.
|
|
case "x":
|
|
switch (filename) {
|
|
case "range_start_after_end.js":
|
|
context.report({
|
|
message: "start after end",
|
|
node,
|
|
fix(fixer) {
|
|
return fixer.removeRange([3, 2]);
|
|
},
|
|
});
|
|
context.report({
|
|
message: "start after end multiple",
|
|
node,
|
|
fix(fixer) {
|
|
return [fixer.removeRange([0, 0]), fixer.removeRange([3, 2])];
|
|
},
|
|
});
|
|
break;
|
|
|
|
case "range_end_out_of_bounds.js":
|
|
context.report({
|
|
message: "end out of bounds",
|
|
node,
|
|
fix(fixer) {
|
|
return fixer.removeRange([0, sourceText.length + 1]);
|
|
},
|
|
});
|
|
context.report({
|
|
message: "end out of bounds multiple",
|
|
node,
|
|
fix(fixer) {
|
|
return [
|
|
fixer.removeRange([0, 0]),
|
|
fixer.removeRange([0, sourceText.length + 1]),
|
|
];
|
|
},
|
|
});
|
|
break;
|
|
|
|
case "range_start_too_large.js":
|
|
context.report({
|
|
message: "start too large",
|
|
node,
|
|
fix(fixer) {
|
|
return fixer.removeRange([U32_MAX_PLUS_ONE, 0]);
|
|
},
|
|
});
|
|
context.report({
|
|
message: "start too large multiple",
|
|
node,
|
|
fix(fixer) {
|
|
return [fixer.removeRange([0, 0]), fixer.removeRange([U32_MAX_PLUS_ONE, 0])];
|
|
},
|
|
});
|
|
break;
|
|
|
|
case "range_end_too_large.js":
|
|
context.report({
|
|
message: "end too large",
|
|
node,
|
|
fix(fixer) {
|
|
return fixer.removeRange([0, U32_MAX_PLUS_ONE]);
|
|
},
|
|
});
|
|
context.report({
|
|
message: "end too large multiple",
|
|
node,
|
|
fix(fixer) {
|
|
return [fixer.removeRange([0, 0]), fixer.removeRange([0, U32_MAX_PLUS_ONE])];
|
|
},
|
|
});
|
|
break;
|
|
|
|
case "range_start_negative.js":
|
|
context.report({
|
|
message: "start negative",
|
|
node,
|
|
fix(fixer) {
|
|
return fixer.removeRange([-10, 0]);
|
|
},
|
|
});
|
|
context.report({
|
|
message: "start negative multiple",
|
|
node,
|
|
fix(fixer) {
|
|
return [fixer.removeRange([0, 0]), fixer.removeRange([-10, 0])];
|
|
},
|
|
});
|
|
break;
|
|
|
|
case "range_end_negative.js":
|
|
context.report({
|
|
message: "end negative",
|
|
node,
|
|
fix(fixer) {
|
|
return fixer.removeRange([0, -10]);
|
|
},
|
|
});
|
|
context.report({
|
|
message: "end negative multiple",
|
|
node,
|
|
fix(fixer) {
|
|
return [fixer.removeRange([0, 0]), fixer.removeRange([0, -10])];
|
|
},
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
const plugin: Plugin = {
|
|
meta: {
|
|
name: "fixes-plugin",
|
|
},
|
|
rules: {
|
|
fixes: rule,
|
|
},
|
|
};
|
|
|
|
export default plugin;
|