11import { getMessage } from "./report.ts" ;
2- import { typeAssertIs } from "../utils/asserts.ts" ;
32
43import type { RuleDetails } from "./load.ts" ;
54import type { Range , Ranged } from "./location.ts" ;
@@ -17,8 +16,22 @@ export type FixFn = (
1716 | null
1817 | undefined ;
1918
20- // Type of a fix, as returned by `fix` function.
21- export type Fix = { range : Range ; text : string } ;
19+ /**
20+ * Fix, as returned by `fix` function.
21+ */
22+ export interface Fix {
23+ range : Range ;
24+ text : string ;
25+ }
26+
27+ /**
28+ * Fix, in form sent to Rust.
29+ */
30+ export interface FixReport {
31+ start : number ;
32+ end : number ;
33+ text : string ;
34+ }
2235
2336// Fixer, passed as argument to `fix` function passed to `Context#report()`.
2437//
@@ -65,11 +78,11 @@ export type Fixer = typeof FIXER;
6578 *
6679 * @param diagnostic - Diagnostic object
6780 * @param ruleDetails - `RuleDetails` object, containing rule-specific `isFixable` value
68- * @returns Non-empty array of `Fix ` objects, or `null` if none
81+ * @returns Non-empty array of `FixReport ` objects, or `null` if none
6982 * @throws {Error } If rule is not marked as fixable but `fix` function returns fixes,
7083 * or if `fix` function returns any invalid `Fix` objects
7184 */
72- export function getFixes ( diagnostic : Diagnostic , ruleDetails : RuleDetails ) : Fix [ ] | null {
85+ export function getFixes ( diagnostic : Diagnostic , ruleDetails : RuleDetails ) : FixReport [ ] | null {
7386 // ESLint silently ignores non-function `fix` values, so we do the same
7487 const { fix } = diagnostic ;
7588 if ( typeof fix !== "function" ) return null ;
@@ -150,119 +163,70 @@ export function getSuggestions(
150163}
151164
152165/**
153- * Call a `FixFn` and process its return value into an array of `Fix ` objects.
166+ * Call a `FixFn` and process its return value into an array of `FixReport ` objects.
154167 *
155168 * Returns `null` if any of:
156169 *
157170 * 1. `fixFn` returns a falsy value.
158171 * 2. `fixFn` returns an empty array/iterator.
159172 * 3. `fixFn` returns an array/iterator containing only falsy values.
160173 *
161- * Otherwise, returns a non-empty array of `Fix ` objects.
174+ * Otherwise, returns a non-empty array of `FixReport ` objects.
162175 *
163- * `Fix` objects are validated and conformed to expected shape.
164- * Does not mutate the `fixes` array returned by `fixFn`, but avoids cloning if possible.
176+ * `Fix` objects are validated.
165177 *
166178 * This function aims to replicate ESLint's behavior as closely as possible.
167179 *
168- * TODO: Are prototype checks, and checks for `toJSON` methods excessive?
169- * We're not handling all possible edge cases e.g. `fixes` or individual `Fix` objects being `Proxy`s or objects
170- * with getters. As we're not managing to be 100% bulletproof anyway, maybe we don't need to be quite so defensive.
171- *
172180 * @param fixFn - Fix function to call
173181 * @param thisArg - `this` value for the fix function call
174- * @returns Non-empty array of `Fix ` objects, or `null` if none
182+ * @returns Non-empty array of `FixReport ` objects, or `null` if none
175183 * @throws {Error } If `fixFn` returns any invalid `Fix` objects
176184 */
177- function getFixesFromFixFn ( fixFn : FixFn , thisArg : Diagnostic | Suggestion ) : Fix [ ] | null {
185+ function getFixesFromFixFn ( fixFn : FixFn , thisArg : Diagnostic | Suggestion ) : FixReport [ ] | null {
178186 // In ESLint, `fix` is called with `this` as a clone of the `Diagnostic` or `Suggestion` object.
179187 // We just use the original object - that should be close enough.
180- let fixes = fixFn . call ( thisArg , FIXER ) ;
188+ const fixes = fixFn . call ( thisArg , FIXER ) ;
181189
182190 // ESLint ignores falsy values
183191 if ( ! fixes ) return null ;
184192
185193 // `fixes` can be any iterator, not just an array e.g. `fix: function*() { yield fix1; yield fix2; }`
186194 if ( Symbol . iterator in fixes ) {
187- let isCloned = false ;
188-
189- // Check prototype instead of using `Array.isArray()`, to ensure it is a native `Array`,
190- // not a subclass which may have overridden `toJSON()` in a way which could make `JSON.stringify()` throw
191- if ( Object . getPrototypeOf ( fixes ) !== Array . prototype || Object . hasOwn ( fixes , "toJSON" ) ) {
192- fixes = Array . from ( fixes ) ;
193- isCloned = true ;
195+ const fixReports : FixReport [ ] = [ ] ;
196+ for ( const fix of fixes ) {
197+ // ESLint ignores falsy values
198+ if ( fix ) fixReports . push ( validateAndConvertFix ( fix ) ) ;
194199 }
195200
196- const fixesLen = fixes . length ;
197- if ( fixesLen === 0 ) return null ;
198-
199- for ( let i = 0 ; i < fixesLen ; i ++ ) {
200- const fix = fixes [ i ] ;
201-
202- // ESLint ignores falsy values.
203- // Filter them out. This branch can only be taken once.
204- if ( ! fix ) {
205- fixes = fixes . filter ( Boolean ) ;
206- if ( fixes . length === 0 ) return null ;
207- isCloned = true ;
208- i -- ;
209- continue ;
210- }
211-
212- const conformedFix = validateAndConformFix ( fix ) ;
213- if ( conformedFix !== fix ) {
214- // Don't mutate `fixes` array
215- if ( isCloned === false ) {
216- fixes = fixes . slice ( ) ;
217- isCloned = true ;
218- }
219- fixes [ i ] = conformedFix ;
220- }
221- }
222-
223- return fixes ;
201+ return fixReports . length === 0 ? null : fixReports ;
224202 }
225203
226- return [ validateAndConformFix ( fixes ) ] ;
204+ return [ validateAndConvertFix ( fixes ) ] ;
227205}
228206
229207/**
230- * Validate that a `Fix` object is well-formed, and conform it to expected shape .
208+ * Validate that a `Fix` object is well-formed, and convert it to a `FixReport` .
231209 *
232- * - Convert `text` to string if needed.
233- * - Shorten `range` to 2 elements if it has extra elements.
234- * - Remove any additional properties on the object.
210+ * Check that `range` has 2 numeric elements, and convert `text` to string if needed.
235211 *
236- * Purpose is to ensure any input which ESLint accepts does not cause an error in `JSON.stringify()`,
212+ * Purpose of validation is to ensure any input which ESLint accepts does not cause an error in `JSON.stringify()`,
237213 * or in deserializing on Rust side.
238214 *
239215 * @param fix - Fix object to validate, possibly malformed
240- * @returns `Fix` object
216+ * @returns `FixReport` object
217+ * @throws {Error } If `fix` has invalid `range`
241218 */
242- function validateAndConformFix ( fix : unknown ) : Fix {
243- typeAssertIs < Fix > ( fix ) ;
219+ function validateAndConvertFix ( fix : Fix ) : FixReport {
244220 const { range, text } = fix ;
245221
246- // These checks follow ESLint, which throws if `range` is missing or invalid
247- if ( ! range || typeof range [ 0 ] !== "number" || typeof range [ 1 ] !== "number" ) {
248- throw new Error ( `Fix has invalid range: ${ JSON . stringify ( fix , null , 2 ) } ` ) ;
249- }
250-
251- // If `fix` is already well-formed, return it as-is.
252- // Note: `ownKeys(fix).length === 2` rules out `fix` having a custom `toJSON` method.
253- const fixPrototype = Object . getPrototypeOf ( fix ) ;
254- if (
255- ( fixPrototype === Object . prototype || fixPrototype === null ) &&
256- Reflect . ownKeys ( fix ) . length === 2 &&
257- Object . getPrototypeOf ( range ) === Array . prototype &&
258- ! Object . hasOwn ( range , "toJSON" ) &&
259- range . length === 2 &&
260- typeof text === "string"
261- ) {
262- return fix ;
222+ if ( range != null ) {
223+ const start = range [ 0 ] ,
224+ end = range [ 1 ] ;
225+ if ( typeof start === "number" && typeof end === "number" ) {
226+ // Converting `text` to string follows ESLint, which does that implicitly
227+ return { start, end, text : String ( text ) } ;
228+ }
263229 }
264230
265- // Conform fix object to expected shape.
266- // Converting `text` to string follows ESLint, which does that implicitly.
267- return { range : [ range [ 0 ] , range [ 1 ] ] , text : String ( text ) } ;
231+ throw new Error ( `Fix has invalid range: ${ JSON . stringify ( fix , null , 2 ) } ` ) ;
268232}
0 commit comments