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
72 changes: 68 additions & 4 deletions apps/oxlint/src-js/plugins/fix.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { getMessage } from "./report.ts";
import { typeAssertIs } from "../utils/asserts.ts";

import type { RuleDetails } from "./load.ts";
import type { Range, Ranged } from "./location.ts";
import type { Diagnostic } from "./report.ts";
import type { Diagnostic, Suggestion, SuggestionReport } from "./report.ts";

// Type of `fix` function.
// `fix` can return a single fix, an array of fixes, or any iterator that yields fixes.
Expand Down Expand Up @@ -85,6 +86,69 @@ export function getFixes(diagnostic: Diagnostic, ruleDetails: RuleDetails): Fix[
return fixes;
}

/**
* Get suggestions from a `Diagnostic`.
*
* Returns `null` if no `suggest` array, or if it produces no suggestions
* (e.g. all fix functions return falsy values).
*
* Throws if rule is not marked with `meta.hasSuggestions` but produces suggestions.
*
* @param diagnostic - Diagnostic object
* @param ruleDetails - `RuleDetails` object, containing rule-specific details
* @returns Non-empty array of `SuggestionReport` objects, or `null` if none
* @throws {Error} If rule is not marked with `meta.hasSuggestions` but produces suggestions
* @throws {TypeError} If a suggestion's `fix` is not a function, or message is invalid
*/
export function getSuggestions(
diagnostic: Diagnostic,
ruleDetails: RuleDetails,
): SuggestionReport[] | null {
if (!Object.hasOwn(diagnostic, "suggest")) return null;
const { suggest } = diagnostic;
if (suggest == null) return null;

const suggestLen = suggest.length;
if (suggestLen === 0) return null;

const suggestions: SuggestionReport[] = [];
for (let i = 0; i < suggestLen; i++) {
const suggestion = suggest[i];

// Validate fix is a function (matches ESLint)
const { fix } = suggestion;
if (typeof fix !== "function") throw new TypeError("Suggestion without a fix function");

// Get suggestion message
let messageId: string | null = null;
if (Object.hasOwn(suggestion, "messageId")) {
(messageId as string | null | undefined) = suggestion.messageId;
if (messageId === undefined) messageId = null;
}

const message = getMessage(
Object.hasOwn(suggestion, "desc") ? suggestion.desc : null,
messageId,
suggestion,
ruleDetails,
);

// Call fix function - drop suggestion if fix function produces no fixes
const fixes = getFixesFromFixFn(fix, suggestion);
if (fixes !== null) suggestions.push({ message, fixes });
}

if (suggestions.length === 0) return null;

// Check rule has suggestions enabled.
// This check is skipped if no suggestions are produced, matching what ESLint does.
if (ruleDetails.hasSuggestions === false) {
throw new Error("Rules with suggestions must set `meta.hasSuggestions` to `true`.");
}

return suggestions;
}

/**
* Call a `FixFn` and process its return value into an array of `Fix` objects.
*
Expand All @@ -110,9 +174,9 @@ export function getFixes(diagnostic: Diagnostic, ruleDetails: RuleDetails): Fix[
* @returns Non-empty array of `Fix` objects, or `null` if none
* @throws {Error} If `fixFn` returns any invalid `Fix` objects
*/
function getFixesFromFixFn(fixFn: FixFn, thisArg: Diagnostic): Fix[] | null {
// In ESLint, `fix` is called with `this` as a clone of the `diagnostic` object.
// We just use the original `diagnostic` object - that should be close enough.
function getFixesFromFixFn(fixFn: FixFn, thisArg: Diagnostic | Suggestion): Fix[] | null {
// In ESLint, `fix` is called with `this` as a clone of the `Diagnostic` or `Suggestion` object.
// We just use the original object - that should be close enough.
let fixes = fixFn.call(thisArg, FIXER);

// ESLint ignores falsy values
Expand Down
25 changes: 17 additions & 8 deletions apps/oxlint/src-js/plugins/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import { filePath } from "./context.ts";
import { getFixes } from "./fix.ts";
import { getFixes, getSuggestions } from "./fix.ts";
import { initLines, lines, lineStartIndices, debugAssertLinesIsInitialized } from "./location.ts";
import { sourceText } from "./source_code.ts";
import { debugAssertIsNonNull, typeAssertIs } from "../utils/asserts.ts";
Expand Down Expand Up @@ -34,7 +34,7 @@ interface DiagnosticBase {
loc?: LocationWithOptionalEnd | LineColumn;
data?: DiagnosticData | null | undefined;
fix?: FixFn;
suggest?: Suggestion[];
suggest?: Suggestion[] | null | undefined;
}

/**
Expand All @@ -52,15 +52,22 @@ export type DiagnosticData = Record<string, string | number | boolean | bigint |

/**
* Suggested fix.
* NOT IMPLEMENTED YET.
*/
export type Suggestion = RequireAtLeastOne<SuggestionBase, "desc" | "messageId">;

interface SuggestionBase {
desc?: string;
messageId?: string;
fix: FixFn;
data?: DiagnosticData | null | undefined;
fix: FixFn;
}

/**
* Suggested fix in form sent to Rust.
*/
export interface SuggestionReport {
message: string;
fixes: Fix[];
}

// Diagnostic in form sent to Rust.
Expand All @@ -71,6 +78,7 @@ export interface DiagnosticReport {
end: number;
ruleIndex: number;
fixes: Fix[] | null;
suggestions: SuggestionReport[] | null;
messageId: string | null;
// Only used in conformance tests
loc?: LocationWithOptionalEnd | null;
Expand Down Expand Up @@ -183,28 +191,29 @@ export function report(diagnostic: Diagnostic, ruleDetails: RuleDetails): void {
end,
ruleIndex: ruleDetails.ruleIndex,
fixes: getFixes(diagnostic, ruleDetails),
suggestions: getSuggestions(diagnostic, ruleDetails),
});

// We need the original location in conformance tests
if (CONFORMANCE) diagnostics.at(-1)!.loc = conformedLoc;
}

/**
* Get message from a diagnostic.
* Get message from a diagnostic or suggestion.
*
* Resolve message from `messageId` if present, and interpolate placeholders {{key}} with data values.
*
* @param message - Provided message string
* @param messageId - Provided message ID
* @param descriptor - Diagnostic object
* @param descriptor - Diagnostic or suggestion object
* @param ruleDetails - `RuleDetails` object, containing rule-specific `messages`
* @returns Message string
* @throws {Error|TypeError} If neither `message` nor `messageId` provided, or of wrong type
*/
function getMessage(
export function getMessage(
message: string | null | undefined,
messageId: string | null,
descriptor: Diagnostic,
descriptor: Diagnostic | Suggestion,
ruleDetails: RuleDetails,
): string {
// Resolve from `messageId` if present, otherwise use `message`
Expand Down
9 changes: 9 additions & 0 deletions apps/oxlint/test/fixtures/suggestions/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"jsPlugins": ["./plugin.ts"],
"categories": {
"correctness": "off"
},
"rules": {
"suggestions-plugin/suggestions": "error"
}
}
15 changes: 15 additions & 0 deletions apps/oxlint/test/fixtures/suggestions/files/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
debugger;

let a = 1;
let b = 2;
let c = 3;
let d = 4;
let e = 5;
let f = 6;
let g = 7;
let h = 8;
let i = 9;
let j = 10;
let k = 11;

debugger;
32 changes: 32 additions & 0 deletions apps/oxlint/test/fixtures/suggestions/fix-suggestions.snap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Exit code
0

# stdout
```
Found 0 warnings and 0 errors.
Finished in Xms on 1 file with 1 rules using X threads.
```

# stderr
```
```

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


let daddy = 1;
let abacus = 2;
let magic = 3;
let damned = 4;
let elephant = 5;
let feck = 6;
let numpty = 7;
let dangermouse = 8;
let granular = 9;
let cowabunga = 10;
let kaboom = 11;



```
114 changes: 114 additions & 0 deletions apps/oxlint/test/fixtures/suggestions/fix.snap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Exit code
1

# stdout
```
x suggestions-plugin(suggestions): Remove debugger statement
,-[files/index.js:1:1]
1 | debugger;
: ^^^^^^^^^
2 |
`----

x suggestions-plugin(suggestions): Replace "a" with "daddy"
,-[files/index.js:3:5]
2 |
3 | let a = 1;
: ^
4 | let b = 2;
`----

x suggestions-plugin(suggestions): Replace "b" with "abacus"
,-[files/index.js:4:5]
3 | let a = 1;
4 | let b = 2;
: ^
5 | let c = 3;
`----

x suggestions-plugin(suggestions): Prefix "c" with "magi"
,-[files/index.js:5:5]
4 | let b = 2;
5 | let c = 3;
: ^
6 | let d = 4;
`----

x suggestions-plugin(suggestions): Prefix "d" with "damne"
,-[files/index.js:6:5]
5 | let c = 3;
6 | let d = 4;
: ^
7 | let e = 5;
`----

x suggestions-plugin(suggestions): Postfix "e" with "lephant"
,-[files/index.js:7:5]
6 | let d = 4;
7 | let e = 5;
: ^
8 | let f = 6;
`----

x suggestions-plugin(suggestions): Postfix "f" with "eck"
,-[files/index.js:8:5]
7 | let e = 5;
8 | let f = 6;
: ^
9 | let g = 7;
`----

x suggestions-plugin(suggestions): Replace "g" with "numpty"
,-[files/index.js:9:5]
8 | let f = 6;
9 | let g = 7;
: ^
10 | let h = 8;
`----

x suggestions-plugin(suggestions): Replace "h" with "dangermouse"
,-[files/index.js:10:5]
9 | let g = 7;
10 | let h = 8;
: ^
11 | let i = 9;
`----

x suggestions-plugin(suggestions): Replace "i" with "granular"
,-[files/index.js:11:5]
10 | let h = 8;
11 | let i = 9;
: ^
12 | let j = 10;
`----

x suggestions-plugin(suggestions): Replace "j" with "cowabunga"
,-[files/index.js:12:5]
11 | let i = 9;
12 | let j = 10;
: ^
13 | let k = 11;
`----

x suggestions-plugin(suggestions): Replace "k" with "kaboom"
,-[files/index.js:13:5]
12 | let j = 10;
13 | let k = 11;
: ^
14 |
`----

x suggestions-plugin(suggestions): Remove debugger statement
,-[files/index.js:15:1]
14 |
15 | debugger;
: ^^^^^^^^^
`----

Found 0 warnings and 13 errors.
Finished in Xms on 1 file with 1 rules using X threads.
```

# stderr
```
```
4 changes: 4 additions & 0 deletions apps/oxlint/test/fixtures/suggestions/options.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"fix": true,
"fixSuggestions": true
}
Loading
Loading