diff --git a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts index 6494b8d6372f..ec418d10d249 100644 --- a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts +++ b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts @@ -546,6 +546,16 @@ let value: unknown; endColumn: 15, endLine: 1, messageId: 'noEmptyObject', + suggestions: [ + { + messageId: 'replaceEmptyObjectType', + output: 'type Base = object | null;', + }, + { + messageId: 'replaceEmptyObjectType', + output: 'type Base = unknown | null;', + }, + ], }, ], options: [{ allowWithName: 'Base' }], @@ -559,6 +569,16 @@ let value: unknown; endColumn: 15, endLine: 1, messageId: 'noEmptyObject', + suggestions: [ + { + messageId: 'replaceEmptyObjectType', + output: 'type Base = object;', + }, + { + messageId: 'replaceEmptyObjectType', + output: 'type Base = unknown;', + }, + ], }, ], options: [{ allowWithName: 'Mismatch' }], diff --git a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts index e6087e512265..4911e944b64d 100644 --- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts +++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts @@ -749,18 +749,70 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + void Promise.resolve('value'); + Promise.resolve('value').then(() => {}); + Promise.resolve('value').catch(); + Promise.resolve('value').finally(); +} + `, + }, + ], }, { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.resolve('value'); + void Promise.resolve('value').then(() => {}); + Promise.resolve('value').catch(); + Promise.resolve('value').finally(); +} + `, + }, + ], }, { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.resolve('value'); + Promise.resolve('value').then(() => {}); + void Promise.resolve('value').catch(); + Promise.resolve('value').finally(); +} + `, + }, + ], }, { line: 6, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.resolve('value'); + Promise.resolve('value').then(() => {}); + Promise.resolve('value').catch(); + void Promise.resolve('value').finally(); +} + `, + }, + ], }, ], }, @@ -791,34 +843,250 @@ doSomething(); { line: 11, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + void obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + obj3.a?.b.c?.(); + obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + callback?.(); +}; + +doSomething(); + `, + }, + ], }, { line: 12, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + void obj2.a?.b?.c(); + obj3.a?.b.c?.(); + obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + callback?.(); +}; + +doSomething(); + `, + }, + ], }, { line: 13, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + void obj3.a?.b.c?.(); + obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + callback?.(); +}; + +doSomething(); + `, + }, + ], }, { line: 14, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + obj3.a?.b.c?.(); + void obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + callback?.(); +}; + +doSomething(); + `, + }, + ], }, { line: 15, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + obj3.a?.b.c?.(); + obj4.a.b.c?.(); + void obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + callback?.(); +}; + +doSomething(); + `, + }, + ], }, { line: 16, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + obj3.a?.b.c?.(); + obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + void obj6?.a.b.c?.(); + + callback?.(); +}; + +doSomething(); + `, + }, + ], }, { line: 18, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + obj3.a?.b.c?.(); + obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + void callback?.(); +}; + +doSomething(); + `, + }, + ], }, { line: 21, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const doSomething = async ( + obj1: { a?: { b?: { c?: () => Promise } } }, + obj2: { a?: { b?: { c: () => Promise } } }, + obj3: { a?: { b: { c?: () => Promise } } }, + obj4: { a: { b: { c?: () => Promise } } }, + obj5: { a?: () => { b?: { c?: () => Promise } } }, + obj6?: { a: { b: { c?: () => Promise } } }, + callback?: () => Promise, +): Promise => { + obj1.a?.b?.c?.(); + obj2.a?.b?.c(); + obj3.a?.b.c?.(); + obj4.a.b.c?.(); + obj5.a?.().b?.c?.(); + obj6?.a.b.c?.(); + + callback?.(); +}; + +void doSomething(); + `, + }, + ], }, ], }, @@ -831,6 +1099,15 @@ myTag\`abc\`; { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const myTag: (strings: TemplateStringsArray) => Promise; +void myTag\`abc\`; + `, + }, + ], }, ], }, @@ -843,6 +1120,15 @@ myTag\`abc\`.then(() => {}); { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const myTag: (strings: TemplateStringsArray) => Promise; +void myTag\`abc\`.then(() => {}); + `, + }, + ], }, ], }, @@ -855,6 +1141,15 @@ myTag\`abc\`.finally(() => {}); { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const myTag: (strings: TemplateStringsArray) => Promise; +void myTag\`abc\`.finally(() => {}); + `, + }, + ], }, ], }, @@ -889,25 +1184,77 @@ async function test() { Promise.reject(new Error('message')); Promise.reject(new Error('message')).then(() => {}); Promise.reject(new Error('message')).catch(); - Promise.reject(new Error('message')).finally(); + Promise.reject(new Error('message')).finally(); +} + `, + errors: [ + { + line: 3, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + void Promise.reject(new Error('message')); + Promise.reject(new Error('message')).then(() => {}); + Promise.reject(new Error('message')).catch(); + Promise.reject(new Error('message')).finally(); +} + `, + }, + ], + }, + { + line: 4, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.reject(new Error('message')); + void Promise.reject(new Error('message')).then(() => {}); + Promise.reject(new Error('message')).catch(); + Promise.reject(new Error('message')).finally(); +} + `, + }, + ], + }, + { + line: 5, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.reject(new Error('message')); + Promise.reject(new Error('message')).then(() => {}); + void Promise.reject(new Error('message')).catch(); + Promise.reject(new Error('message')).finally(); +} + `, + }, + ], + }, + { + line: 6, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.reject(new Error('message')); + Promise.reject(new Error('message')).then(() => {}); + Promise.reject(new Error('message')).catch(); + void Promise.reject(new Error('message')).finally(); } `, - errors: [ - { - line: 3, - messageId: 'floatingVoid', - }, - { - line: 4, - messageId: 'floatingVoid', - }, - { - line: 5, - messageId: 'floatingVoid', - }, - { - line: 6, - messageId: 'floatingVoid', + }, + ], }, ], }, @@ -923,14 +1270,50 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + void (async () => true)(); + (async () => true)().then(() => {}); + (async () => true)().catch(); +} + `, + }, + ], }, { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + (async () => true)(); + void (async () => true)().then(() => {}); + (async () => true)().catch(); +} + `, + }, + ], }, { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + (async () => true)(); + (async () => true)().then(() => {}); + void (async () => true)().catch(); +} + `, + }, + ], }, ], }, @@ -949,18 +1332,78 @@ async function test() { { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + async function returnsPromise() {} + + void returnsPromise(); + returnsPromise().then(() => {}); + returnsPromise().catch(); + returnsPromise().finally(); +} + `, + }, + ], }, { line: 6, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + async function returnsPromise() {} + + returnsPromise(); + void returnsPromise().then(() => {}); + returnsPromise().catch(); + returnsPromise().finally(); +} + `, + }, + ], }, { line: 7, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + async function returnsPromise() {} + + returnsPromise(); + returnsPromise().then(() => {}); + void returnsPromise().catch(); + returnsPromise().finally(); +} + `, + }, + ], }, { line: 8, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + async function returnsPromise() {} + + returnsPromise(); + returnsPromise().then(() => {}); + returnsPromise().catch(); + void returnsPromise().finally(); +} + `, + }, + ], }, ], }, @@ -975,10 +1418,32 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + void (Math.random() > 0.5 ? Promise.resolve() : null); + Math.random() > 0.5 ? null : Promise.resolve(); +} + `, + }, + ], }, { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Math.random() > 0.5 ? Promise.resolve() : null; + void (Math.random() > 0.5 ? null : Promise.resolve()); +} + `, + }, + ], }, ], }, @@ -994,14 +1459,50 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + void (Promise.resolve(), 123); + 123, Promise.resolve(); + 123, Promise.resolve(), 123; +} + `, + }, + ], }, { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.resolve(), 123; + void (123, Promise.resolve()); + 123, Promise.resolve(), 123; +} + `, + }, + ], }, { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + Promise.resolve(), 123; + 123, Promise.resolve(); + void (123, Promise.resolve(), 123); +} + `, + }, + ], }, ], }, @@ -1171,6 +1672,17 @@ async function test() { { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + const obj = { foo: Promise.resolve() }; + void obj.foo; +} + `, + }, + ], }, ], }, @@ -1184,36 +1696,106 @@ async function test() { { line: 3, messageId: 'floatingVoid', - }, - ], - }, - { - code: ` + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + void new Promise(resolve => resolve()); +} + `, + }, + ], + }, + ], + }, + { + code: ` +async function test() { + declare const promiseValue: Promise; + + promiseValue; + promiseValue.then(() => {}); + promiseValue.catch(); + promiseValue.finally(); +} + `, + errors: [ + { + line: 5, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseValue: Promise; + + void promiseValue; + promiseValue.then(() => {}); + promiseValue.catch(); + promiseValue.finally(); +} + `, + }, + ], + }, + { + line: 6, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseValue: Promise; + + promiseValue; + void promiseValue.then(() => {}); + promiseValue.catch(); + promiseValue.finally(); +} + `, + }, + ], + }, + { + line: 7, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseValue: Promise; + + promiseValue; + promiseValue.then(() => {}); + void promiseValue.catch(); + promiseValue.finally(); +} + `, + }, + ], + }, + { + line: 8, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` async function test() { declare const promiseValue: Promise; promiseValue; promiseValue.then(() => {}); promiseValue.catch(); - promiseValue.finally(); + void promiseValue.finally(); } `, - errors: [ - { - line: 5, - messageId: 'floatingVoid', - }, - { - line: 6, - messageId: 'floatingVoid', - }, - { - line: 7, - messageId: 'floatingVoid', - }, - { - line: 8, - messageId: 'floatingVoid', + }, + ], }, ], }, @@ -1229,6 +1811,18 @@ async function test() { { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseUnion: Promise | number; + + void promiseUnion; +} + `, + }, + ], }, ], }, @@ -1246,14 +1840,56 @@ async function test() { { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseIntersection: Promise & number; + + void promiseIntersection; + promiseIntersection.then(() => {}); + promiseIntersection.catch(); +} + `, + }, + ], }, { line: 6, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseIntersection: Promise & number; + + promiseIntersection; + void promiseIntersection.then(() => {}); + promiseIntersection.catch(); +} + `, + }, + ], }, { line: 7, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + declare const promiseIntersection: Promise & number; + + promiseIntersection; + promiseIntersection.then(() => {}); + void promiseIntersection.catch(); +} + `, + }, + ], }, ], }, @@ -1273,18 +1909,82 @@ async function test() { { line: 6, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + class CanThen extends Promise {} + const canThen: CanThen = Foo.resolve(2); + + void canThen; + canThen.then(() => {}); + canThen.catch(); + canThen.finally(); +} + `, + }, + ], }, { line: 7, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + class CanThen extends Promise {} + const canThen: CanThen = Foo.resolve(2); + + canThen; + void canThen.then(() => {}); + canThen.catch(); + canThen.finally(); +} + `, + }, + ], }, { line: 8, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + class CanThen extends Promise {} + const canThen: CanThen = Foo.resolve(2); + + canThen; + canThen.then(() => {}); + void canThen.catch(); + canThen.finally(); +} + `, + }, + ], }, { line: 9, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + class CanThen extends Promise {} + const canThen: CanThen = Foo.resolve(2); + + canThen; + canThen.then(() => {}); + canThen.catch(); + void canThen.finally(); +} + `, + }, + ], }, ], }, @@ -1306,10 +2006,46 @@ async function test() { { line: 10, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + class CatchableThenable { + then(callback: () => void, callback: () => void): CatchableThenable { + return new CatchableThenable(); + } + } + const thenable = new CatchableThenable(); + + void thenable; + thenable.then(() => {}); +} + `, + }, + ], }, { line: 11, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function test() { + class CatchableThenable { + then(callback: () => void, callback: () => void): CatchableThenable { + return new CatchableThenable(); + } + } + const thenable = new CatchableThenable(); + + thenable; + void thenable.then(() => {}); +} + `, + }, + ], }, ], }, @@ -1340,14 +2076,95 @@ async function test() { { line: 18, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/promise-polyfill/index.d.ts +// Type definitions for promise-polyfill 6.0 +// Project: https://github.com/taylorhakes/promise-polyfill +// Definitions by: Steve Jenkins +// Daniel Cassidy +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +interface PromisePolyfillConstructor extends PromiseConstructor { + _immediateFn?: (handler: (() => void) | string) => void; +} + +declare const PromisePolyfill: PromisePolyfillConstructor; + +async function test() { + const promise = new PromisePolyfill(() => {}); + + void promise; + promise.then(() => {}); + promise.catch(); +} + `, + }, + ], }, { line: 19, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/promise-polyfill/index.d.ts +// Type definitions for promise-polyfill 6.0 +// Project: https://github.com/taylorhakes/promise-polyfill +// Definitions by: Steve Jenkins +// Daniel Cassidy +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +interface PromisePolyfillConstructor extends PromiseConstructor { + _immediateFn?: (handler: (() => void) | string) => void; +} + +declare const PromisePolyfill: PromisePolyfillConstructor; + +async function test() { + const promise = new PromisePolyfill(() => {}); + + promise; + void promise.then(() => {}); + promise.catch(); +} + `, + }, + ], }, { line: 20, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/promise-polyfill/index.d.ts +// Type definitions for promise-polyfill 6.0 +// Project: https://github.com/taylorhakes/promise-polyfill +// Definitions by: Steve Jenkins +// Daniel Cassidy +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +interface PromisePolyfillConstructor extends PromiseConstructor { + _immediateFn?: (handler: (() => void) | string) => void; +} + +declare const PromisePolyfill: PromisePolyfillConstructor; + +async function test() { + const promise = new PromisePolyfill(() => {}); + + promise; + promise.then(() => {}); + void promise.catch(); +} + `, + }, + ], }, ], }, @@ -1361,6 +2178,16 @@ async function test() { { line: 2, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + void (async () => { + await something(); + })(); + `, + }, + ], }, ], }, @@ -1374,6 +2201,16 @@ async function test() { { line: 2, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + void (async () => { + something(); + })(); + `, + }, + ], }, ], }, @@ -1383,6 +2220,12 @@ async function test() { { line: 1, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: 'void (async function foo() {})();', + }, + ], }, ], }, @@ -1396,6 +2239,16 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + function foo() { + void (async function bar() {})(); + } + `, + }, + ], }, ], }, @@ -1412,6 +2265,19 @@ async function test() { { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + const foo = () => + new Promise(res => { + void (async function () { + await res(1); + })(); + }); + `, + }, + ], }, ], }, @@ -1425,6 +2291,16 @@ async function test() { { line: 2, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + void (async function () { + await res(1); + })(); + `, + }, + ], }, ], }, @@ -1439,6 +2315,16 @@ async function test() { { line: 3, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + (async function () { + void Promise.resolve(); + })(); + `, + }, + ], }, ], }, @@ -1457,18 +2343,74 @@ async function test() { { line: 4, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + (async function () { + declare const promiseIntersection: Promise & number; + void promiseIntersection; + promiseIntersection.then(() => {}); + promiseIntersection.catch(); + promiseIntersection.finally(); + })(); + `, + }, + ], }, { line: 5, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + (async function () { + declare const promiseIntersection: Promise & number; + promiseIntersection; + void promiseIntersection.then(() => {}); + promiseIntersection.catch(); + promiseIntersection.finally(); + })(); + `, + }, + ], }, { line: 6, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + (async function () { + declare const promiseIntersection: Promise & number; + promiseIntersection; + promiseIntersection.then(() => {}); + void promiseIntersection.catch(); + promiseIntersection.finally(); + })(); + `, + }, + ], }, { line: 7, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` + (async function () { + declare const promiseIntersection: Promise & number; + promiseIntersection; + promiseIntersection.then(() => {}); + promiseIntersection.catch(); + void promiseIntersection.finally(); + })(); + `, + }, + ], }, ], }, @@ -1711,28 +2653,194 @@ async function foo() { condition || condition || myPromise(); } `, - errors: [ + errors: [ + { + line: 6, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +async function foo() { + const myPromise = async () => void 0; + const condition = false; + + void (condition || condition || myPromise()); +} + `, + }, + ], + }, + ], + }, + { + code: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + errors: [ + { + line: 4, + messageId: 'floatingUselessRejectionHandlerVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +void Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], + }, + { + line: 5, + messageId: 'floatingUselessRejectionHandlerVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +void Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], + }, + { + line: 6, + messageId: 'floatingUselessRejectionHandlerVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +void Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], + }, + { + line: 7, + messageId: 'floatingUselessRejectionHandlerVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +void Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], + }, + { + line: 10, + messageId: 'floatingUselessRejectionHandlerVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +void Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], + }, { - line: 6, - messageId: 'floatingVoid', + line: 11, + messageId: 'floatingUselessRejectionHandlerVoid', suggestions: [ { messageId: 'floatingFixVoid', output: ` -async function foo() { - const myPromise = async () => void 0; - const condition = false; +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); - void (condition || condition || myPromise()); -} +Promise.resolve().catch(undefined); +void Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); `, }, ], }, - ], - }, - { - code: ` + { + line: 12, + messageId: 'floatingUselessRejectionHandlerVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` declare const maybeCallable: string | (() => void); declare const definitelyCallable: () => void; Promise.resolve().then(() => {}, undefined); @@ -1743,42 +2851,36 @@ Promise.resolve().then(() => {}, definitelyCallable); Promise.resolve().catch(undefined); Promise.resolve().catch(null); -Promise.resolve().catch(3); +void Promise.resolve().catch(3); Promise.resolve().catch(maybeCallable); Promise.resolve().catch(definitelyCallable); `, - errors: [ - { - line: 4, - messageId: 'floatingUselessRejectionHandlerVoid', - }, - { - line: 5, - messageId: 'floatingUselessRejectionHandlerVoid', - }, - { - line: 6, - messageId: 'floatingUselessRejectionHandlerVoid', - }, - { - line: 7, - messageId: 'floatingUselessRejectionHandlerVoid', - }, - { - line: 10, - messageId: 'floatingUselessRejectionHandlerVoid', - }, - { - line: 11, - messageId: 'floatingUselessRejectionHandlerVoid', - }, - { - line: 12, - messageId: 'floatingUselessRejectionHandlerVoid', + }, + ], }, { line: 13, messageId: 'floatingUselessRejectionHandlerVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +void Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], }, ], }, @@ -1790,6 +2892,14 @@ Promise.reject() || 3; { line: 2, messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +void (Promise.reject() || 3); + `, + }, + ], }, ], }, @@ -1802,6 +2912,14 @@ void Promise.resolve().then(() => {}, undefined); { line: 2, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +await Promise.resolve().then(() => {}, undefined); + `, + }, + ], }, ], }, @@ -1815,6 +2933,15 @@ Promise.resolve().then(() => {}, maybeCallable); { line: 3, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +declare const maybeCallable: string | (() => void); +await Promise.resolve().then(() => {}, maybeCallable); + `, + }, + ], }, ], }, @@ -1839,34 +2966,194 @@ Promise.resolve().catch(definitelyCallable); { line: 4, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +await Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], }, { line: 5, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +await Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], }, { line: 6, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +await Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], }, { line: 7, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +await Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], }, { line: 10, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +await Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], }, { line: 11, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +await Promise.resolve().catch(null); +Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], }, { line: 12, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +await Promise.resolve().catch(3); +Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], }, { line: 13, messageId: 'floatingUselessRejectionHandler', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +declare const maybeCallable: string | (() => void); +declare const definitelyCallable: () => void; +Promise.resolve().then(() => {}, undefined); +Promise.resolve().then(() => {}, null); +Promise.resolve().then(() => {}, 3); +Promise.resolve().then(() => {}, maybeCallable); +Promise.resolve().then(() => {}, definitelyCallable); + +Promise.resolve().catch(undefined); +Promise.resolve().catch(null); +Promise.resolve().catch(3); +await Promise.resolve().catch(maybeCallable); +Promise.resolve().catch(definitelyCallable); + `, + }, + ], }, ], }, @@ -1879,6 +3166,14 @@ Promise.reject() || 3; { line: 2, messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +await (Promise.reject() || 3); + `, + }, + ], }, ], }, @@ -1886,7 +3181,20 @@ Promise.reject() || 3; code: ` Promise.reject().finally(() => {}); `, - errors: [{ line: 2, messageId: 'floatingVoid' }], + errors: [ + { + line: 2, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +void Promise.reject().finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` @@ -1895,7 +3203,22 @@ Promise.reject() .finally(() => {}); `, options: [{ ignoreVoid: false }], - errors: [{ line: 2, messageId: 'floating' }], + errors: [ + { + line: 2, + messageId: 'floating', + suggestions: [ + { + messageId: 'floatingFixAwait', + output: ` +await Promise.reject() + .finally(() => {}) + .finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` @@ -1904,7 +3227,23 @@ Promise.reject() .finally(() => {}) .finally(() => {}); `, - errors: [{ line: 2, messageId: 'floatingVoid' }], + errors: [ + { + line: 2, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +void Promise.reject() + .finally(() => {}) + .finally(() => {}) + .finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` @@ -1912,39 +3251,121 @@ Promise.reject() .then(() => {}) .finally(() => {}); `, - errors: [{ line: 2, messageId: 'floatingVoid' }], + errors: [ + { + line: 2, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +void Promise.reject() + .then(() => {}) + .finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` declare const returnsPromise: () => Promise | null; returnsPromise()?.finally(() => {}); `, - errors: [{ line: 3, messageId: 'floatingVoid' }], + errors: [ + { + line: 3, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +declare const returnsPromise: () => Promise | null; +void returnsPromise()?.finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` const promiseIntersection: Promise & number; promiseIntersection.finally(() => {}); `, - errors: [{ line: 3, messageId: 'floatingVoid' }], + errors: [ + { + line: 3, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +const promiseIntersection: Promise & number; +void promiseIntersection.finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` Promise.resolve().finally(() => {}), 123; `, - errors: [{ line: 2, messageId: 'floatingVoid' }], + errors: [ + { + line: 2, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +void (Promise.resolve().finally(() => {}), 123); + `, + }, + ], + }, + ], }, { code: ` (async () => true)().finally(); `, - errors: [{ line: 2, messageId: 'floatingVoid' }], + errors: [ + { + line: 2, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +void (async () => true)().finally(); + `, + }, + ], + }, + ], }, { code: ` Promise.reject(new Error('message')).finally(() => {}); `, - errors: [{ line: 2, messageId: 'floatingVoid' }], + errors: [ + { + line: 2, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +void Promise.reject(new Error('message')).finally(() => {}); + `, + }, + ], + }, + ], }, { code: ` @@ -1954,7 +3375,24 @@ function _>>( maybePromiseArray?.[0]; } `, - errors: [{ line: 5, messageId: 'floatingVoid' }], + errors: [ + { + line: 5, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +function _>>( + maybePromiseArray: S | undefined, +): void { + void maybePromiseArray?.[0]; +} + `, + }, + ], + }, + ], }, { code: ` @@ -2094,7 +3532,33 @@ promise; allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }], }, ], - errors: [{ line: 15, messageId: 'floatingVoid' }], + errors: [ + { + line: 15, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +interface UnsafeThenable { + then( + onfulfilled?: + | ((value: T) => TResult1 | UnsafeThenable) + | undefined + | null, + onrejected?: + | ((reason: any) => TResult2 | UnsafeThenable) + | undefined + | null, + ): UnsafeThenable; +} +let promise: UnsafeThenable = Promise.resolve(5); +void promise; + `, + }, + ], + }, + ], }, { code: ` @@ -2105,7 +3569,22 @@ promise.catch(); options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, ], - errors: [{ line: 4, messageId: 'floatingVoid' }], + errors: [ + { + line: 4, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +class SafePromise extends Promise {} +let promise: SafePromise = Promise.resolve(5); +void promise.catch(); + `, + }, + ], + }, + ], }, { code: ` @@ -2116,7 +3595,22 @@ promise().finally(); options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, ], - errors: [{ line: 4, messageId: 'floatingVoid' }], + errors: [ + { + line: 4, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +class UnsafePromise extends Promise {} +let promise: () => UnsafePromise = async () => 5; +void promise().finally(); + `, + }, + ], + }, + ], }, { code: ` @@ -2127,7 +3621,22 @@ let promise: UnsafePromise = Promise.resolve(5); options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, ], - errors: [{ line: 4, messageId: 'floatingVoid' }], + errors: [ + { + line: 4, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +type UnsafePromise = Promise & { hey?: string }; +let promise: UnsafePromise = Promise.resolve(5); +void (0 ? promise.catch() : 2); + `, + }, + ], + }, + ], }, { code: ` @@ -2138,7 +3647,22 @@ null ?? promise().catch(); options: [ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] }, ], - errors: [{ line: 4, messageId: 'floatingVoid' }], + errors: [ + { + line: 4, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +type UnsafePromise = Promise & { hey?: string }; +let promise: () => UnsafePromise = async () => 5; +void (null ?? promise().catch()); + `, + }, + ], + }, + ], }, { code: ` @@ -2179,7 +3703,22 @@ declare const myTag: (strings: TemplateStringsArray) => SafePromise; myTag\`abc\`; `, options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }], - errors: [{ line: 4, messageId: 'floatingVoid' }], + errors: [ + { + line: 4, + messageId: 'floatingVoid', + suggestions: [ + { + messageId: 'floatingFixVoid', + output: ` +type SafePromise = Promise & { __linterBrands?: string }; +declare const myTag: (strings: TemplateStringsArray) => SafePromise; +void myTag\`abc\`; + `, + }, + ], + }, + ], }, ], }); diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts index b2d4eb907b33..9a7f49dc3ecc 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts @@ -409,7 +409,23 @@ ruleTester.run('strict-enums-comparison', rule, { } Fruit.Apple === 0; `, - errors: [{ messageId: 'mismatchedCondition' }], + errors: [ + { + messageId: 'mismatchedCondition', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum Fruit { + Apple = 0, + Banana = 'banana', + } + Fruit.Apple === Fruit.Apple; + `, + }, + ], + }, + ], }, { code: ` @@ -584,10 +600,126 @@ ruleTester.run('strict-enums-comparison', rule, { mixed === 1; `, errors: [ - { messageId: 'mismatchedCondition' }, - { messageId: 'mismatchedCondition' }, - { messageId: 'mismatchedCondition' }, - { messageId: 'mismatchedCondition' }, + { + messageId: 'mismatchedCondition', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum Str { + A = 'a', + } + enum Num { + B = 1, + } + enum Mixed { + A = 'a', + B = 1, + } + + declare const str: Str; + declare const num: Num; + declare const mixed: Mixed; + + // following are all errors because the value might be an enum value + str === Str.A; + num === 1; + mixed === 'a'; + mixed === 1; + `, + }, + ], + }, + { + messageId: 'mismatchedCondition', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum Str { + A = 'a', + } + enum Num { + B = 1, + } + enum Mixed { + A = 'a', + B = 1, + } + + declare const str: Str; + declare const num: Num; + declare const mixed: Mixed; + + // following are all errors because the value might be an enum value + str === 'a'; + num === Num.B; + mixed === 'a'; + mixed === 1; + `, + }, + ], + }, + { + messageId: 'mismatchedCondition', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum Str { + A = 'a', + } + enum Num { + B = 1, + } + enum Mixed { + A = 'a', + B = 1, + } + + declare const str: Str; + declare const num: Num; + declare const mixed: Mixed; + + // following are all errors because the value might be an enum value + str === 'a'; + num === 1; + mixed === Mixed.A; + mixed === 1; + `, + }, + ], + }, + { + messageId: 'mismatchedCondition', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum Str { + A = 'a', + } + enum Num { + B = 1, + } + enum Mixed { + A = 'a', + B = 1, + } + + declare const str: Str; + declare const num: Num; + declare const mixed: Mixed; + + // following are all errors because the value might be an enum value + str === 'a'; + num === 1; + mixed === 'a'; + mixed === Mixed.B; + `, + }, + ], + }, ], }, { diff --git a/packages/eslint-plugin/tests/rules/prefer-as-const.test.ts b/packages/eslint-plugin/tests/rules/prefer-as-const.test.ts index 19058d692b42..e0c917c88f21 100644 --- a/packages/eslint-plugin/tests/rules/prefer-as-const.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-as-const.test.ts @@ -121,6 +121,12 @@ ruleTester.run('prefer-as-const', rule, { messageId: 'variableConstAssertion', line: 1, column: 9, + suggestions: [ + { + messageId: 'variableSuggest', + output: "let [] = 'bar' as const;", + }, + ], }, ], }, diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 5affe9874a50..bd0cd13c4b5a 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -957,7 +957,20 @@ x || y; ignorePrimitives: { number: true, boolean: true, bigint: true }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: string | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -970,7 +983,20 @@ x || y; ignorePrimitives: { string: true, boolean: true, bigint: true }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: number | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -983,7 +1009,20 @@ x || y; ignorePrimitives: { string: true, number: true, bigint: true }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: boolean | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -996,7 +1035,20 @@ x || y; ignorePrimitives: { string: true, number: true, boolean: true }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: bigint | undefined; +x ?? y; + `, + }, + ], + }, + ], }, // falsy { @@ -1015,7 +1067,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: '' | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1033,7 +1098,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: \`\` | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1051,7 +1129,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 0 | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1069,7 +1160,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 0n | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1087,7 +1191,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: false | undefined; +x ?? y; + `, + }, + ], + }, + ], }, // truthy { @@ -1106,7 +1223,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 'a' | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1124,7 +1254,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: \`hello\${'string'}\` | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1142,7 +1285,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 1 | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1160,7 +1316,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 1n | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1178,7 +1347,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: true | undefined; +x ?? y; + `, + }, + ], + }, + ], }, // Unions of same primitive { @@ -1197,7 +1379,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 'a' | 'b' | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1215,7 +1410,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 'a' | \`b\` | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1233,7 +1441,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 0 | 1 | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1251,7 +1472,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 1 | 2 | 3 | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1269,7 +1503,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 0n | 1n | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1287,7 +1534,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 1n | 2n | 3n | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1305,7 +1565,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: true | false | undefined; +x ?? y; + `, + }, + ], + }, + ], }, // Mixed unions { @@ -1324,7 +1597,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: 0 | 1 | 0n | 1n | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1342,7 +1628,20 @@ x || y; }, }, ], - errors: [{ messageId: 'preferNullishOverOr' }], + errors: [ + { + messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: true | false | null | undefined; +x ?? y; + `, + }, + ], + }, + ], }, { code: ` @@ -1353,6 +1652,15 @@ x || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +declare const x: null; +x ?? y; + `, + }, + ], }, ], }, @@ -1365,6 +1673,15 @@ x || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +const x = undefined; +x ?? y; + `, + }, + ], }, ], }, @@ -1376,6 +1693,14 @@ null || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +null ?? y; + `, + }, + ], }, ], }, @@ -1387,6 +1712,14 @@ undefined || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +undefined ?? y; + `, + }, + ], }, ], }, @@ -1404,6 +1737,20 @@ x || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +enum Enum { + A = 0, + B = 1, + C = 2, +} +declare const x: Enum | undefined; +x ?? y; + `, + }, + ], }, ], }, @@ -1421,6 +1768,20 @@ x || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +enum Enum { + A = 0, + B = 1, + C = 2, +} +declare const x: Enum.A | Enum.B | undefined; +x ?? y; + `, + }, + ], }, ], }, @@ -1438,6 +1799,20 @@ x || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare const x: Enum | undefined; +x ?? y; + `, + }, + ], }, ], }, @@ -1455,6 +1830,20 @@ x || y; errors: [ { messageId: 'preferNullishOverOr', + suggestions: [ + { + messageId: 'suggestNullish', + output: ` +enum Enum { + A = 'a', + B = 'b', + C = 'c', +} +declare const x: Enum.A | Enum.B | undefined; +x ?? y; + `, + }, + ], }, ], }, diff --git a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts index bd25e35fe136..dd1bb0a92a38 100644 --- a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -523,9 +523,49 @@ if (y) { code: "'asd' && 123 && [] && null;", output: null, errors: [ - { messageId: 'conditionErrorString', line: 1, column: 1 }, - { messageId: 'conditionErrorNumber', line: 1, column: 10 }, - { messageId: 'conditionErrorObject', line: 1, column: 17 }, + { + messageId: 'conditionErrorString', + line: 1, + column: 1, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: "('asd'.length > 0) && 123 && [] && null;", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: '(\'asd\' !== "") && 123 && [] && null;', + }, + { + messageId: 'conditionFixCastBoolean', + output: "(Boolean('asd')) && 123 && [] && null;", + }, + ], + }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 10, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "'asd' && (123 !== 0) && [] && null;", + }, + { + messageId: 'conditionFixCompareNaN', + output: "'asd' && (!Number.isNaN(123)) && [] && null;", + }, + { + messageId: 'conditionFixCastBoolean', + output: "'asd' && (Boolean(123)) && [] && null;", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 17, + }, ], }, { @@ -533,9 +573,49 @@ if (y) { code: "'asd' || 123 || [] || null;", output: null, errors: [ - { messageId: 'conditionErrorString', line: 1, column: 1 }, - { messageId: 'conditionErrorNumber', line: 1, column: 10 }, - { messageId: 'conditionErrorObject', line: 1, column: 17 }, + { + messageId: 'conditionErrorString', + line: 1, + column: 1, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: "('asd'.length > 0) || 123 || [] || null;", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: '(\'asd\' !== "") || 123 || [] || null;', + }, + { + messageId: 'conditionFixCastBoolean', + output: "(Boolean('asd')) || 123 || [] || null;", + }, + ], + }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 10, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "'asd' || (123 !== 0) || [] || null;", + }, + { + messageId: 'conditionFixCompareNaN', + output: "'asd' || (!Number.isNaN(123)) || [] || null;", + }, + { + messageId: 'conditionFixCastBoolean', + output: "'asd' || (Boolean(123)) || [] || null;", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 17, + }, ], }, { @@ -543,11 +623,91 @@ if (y) { code: "let x = (1 && 'a' && null) || 0 || '' || {};", output: null, errors: [ - { messageId: 'conditionErrorNumber', line: 1, column: 10 }, - { messageId: 'conditionErrorString', line: 1, column: 15 }, - { messageId: 'conditionErrorNullish', line: 1, column: 22 }, - { messageId: 'conditionErrorNumber', line: 1, column: 31 }, - { messageId: 'conditionErrorString', line: 1, column: 36 }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 10, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "let x = ((1 !== 0) && 'a' && null) || 0 || '' || {};", + }, + { + messageId: 'conditionFixCompareNaN', + output: + "let x = ((!Number.isNaN(1)) && 'a' && null) || 0 || '' || {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "let x = ((Boolean(1)) && 'a' && null) || 0 || '' || {};", + }, + ], + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 15, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: + "let x = (1 && ('a'.length > 0) && null) || 0 || '' || {};", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: "let x = (1 && ('a' !== \"\") && null) || 0 || '' || {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "let x = (1 && (Boolean('a')) && null) || 0 || '' || {};", + }, + ], + }, + { + messageId: 'conditionErrorNullish', + line: 1, + column: 22, + }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 31, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "let x = (1 && 'a' && null) || (0 !== 0) || '' || {};", + }, + { + messageId: 'conditionFixCompareNaN', + output: + "let x = (1 && 'a' && null) || (!Number.isNaN(0)) || '' || {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "let x = (1 && 'a' && null) || (Boolean(0)) || '' || {};", + }, + ], + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 36, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: + "let x = (1 && 'a' && null) || 0 || (''.length > 0) || {};", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: "let x = (1 && 'a' && null) || 0 || ('' !== \"\") || {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "let x = (1 && 'a' && null) || 0 || (Boolean('')) || {};", + }, + ], + }, ], }, { @@ -555,11 +715,91 @@ if (y) { code: "return (1 || 'a' || null) && 0 && '' && {};", output: null, errors: [ - { messageId: 'conditionErrorNumber', line: 1, column: 9 }, - { messageId: 'conditionErrorString', line: 1, column: 14 }, - { messageId: 'conditionErrorNullish', line: 1, column: 21 }, - { messageId: 'conditionErrorNumber', line: 1, column: 30 }, - { messageId: 'conditionErrorString', line: 1, column: 35 }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 9, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "return ((1 !== 0) || 'a' || null) && 0 && '' && {};", + }, + { + messageId: 'conditionFixCompareNaN', + output: + "return ((!Number.isNaN(1)) || 'a' || null) && 0 && '' && {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "return ((Boolean(1)) || 'a' || null) && 0 && '' && {};", + }, + ], + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 14, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: + "return (1 || ('a'.length > 0) || null) && 0 && '' && {};", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: "return (1 || ('a' !== \"\") || null) && 0 && '' && {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "return (1 || (Boolean('a')) || null) && 0 && '' && {};", + }, + ], + }, + { + messageId: 'conditionErrorNullish', + line: 1, + column: 21, + }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 30, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "return (1 || 'a' || null) && (0 !== 0) && '' && {};", + }, + { + messageId: 'conditionFixCompareNaN', + output: + "return (1 || 'a' || null) && (!Number.isNaN(0)) && '' && {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "return (1 || 'a' || null) && (Boolean(0)) && '' && {};", + }, + ], + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 35, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: + "return (1 || 'a' || null) && 0 && (''.length > 0) && {};", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: "return (1 || 'a' || null) && 0 && ('' !== \"\") && {};", + }, + { + messageId: 'conditionFixCastBoolean', + output: "return (1 || 'a' || null) && 0 && (Boolean('')) && {};", + }, + ], + }, ], }, { @@ -567,9 +807,49 @@ if (y) { code: "console.log((1 && []) || ('a' && {}));", output: null, errors: [ - { messageId: 'conditionErrorNumber', line: 1, column: 14 }, - { messageId: 'conditionErrorObject', line: 1, column: 19 }, - { messageId: 'conditionErrorString', line: 1, column: 27 }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 14, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "console.log(((1 !== 0) && []) || ('a' && {}));", + }, + { + messageId: 'conditionFixCompareNaN', + output: "console.log(((!Number.isNaN(1)) && []) || ('a' && {}));", + }, + { + messageId: 'conditionFixCastBoolean', + output: "console.log(((Boolean(1)) && []) || ('a' && {}));", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 19, + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 27, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: "console.log((1 && []) || (('a'.length > 0) && {}));", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: 'console.log((1 && []) || ((\'a\' !== "") && {}));', + }, + { + messageId: 'conditionFixCastBoolean', + output: "console.log((1 && []) || ((Boolean('a')) && {}));", + }, + ], + }, ], }, @@ -579,10 +859,54 @@ if (y) { code: "if ((1 && []) || ('a' && {})) void 0;", output: null, errors: [ - { messageId: 'conditionErrorNumber', line: 1, column: 6 }, - { messageId: 'conditionErrorObject', line: 1, column: 11 }, - { messageId: 'conditionErrorString', line: 1, column: 19 }, - { messageId: 'conditionErrorObject', line: 1, column: 26 }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 6, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "if (((1 !== 0) && []) || ('a' && {})) void 0;", + }, + { + messageId: 'conditionFixCompareNaN', + output: "if (((!Number.isNaN(1)) && []) || ('a' && {})) void 0;", + }, + { + messageId: 'conditionFixCastBoolean', + output: "if (((Boolean(1)) && []) || ('a' && {})) void 0;", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 11, + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 19, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: "if ((1 && []) || (('a'.length > 0) && {})) void 0;", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: 'if ((1 && []) || ((\'a\' !== "") && {})) void 0;', + }, + { + messageId: 'conditionFixCastBoolean', + output: "if ((1 && []) || ((Boolean('a')) && {})) void 0;", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 26, + }, ], }, { @@ -590,10 +914,60 @@ if (y) { code: "let x = null || 0 || 'a' || [] ? {} : undefined;", output: null, errors: [ - { messageId: 'conditionErrorNullish', line: 1, column: 9 }, - { messageId: 'conditionErrorNumber', line: 1, column: 17 }, - { messageId: 'conditionErrorString', line: 1, column: 22 }, - { messageId: 'conditionErrorObject', line: 1, column: 29 }, + { + messageId: 'conditionErrorNullish', + line: 1, + column: 9, + }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 17, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: + "let x = null || (0 !== 0) || 'a' || [] ? {} : undefined;", + }, + { + messageId: 'conditionFixCompareNaN', + output: + "let x = null || (!Number.isNaN(0)) || 'a' || [] ? {} : undefined;", + }, + { + messageId: 'conditionFixCastBoolean', + output: + "let x = null || (Boolean(0)) || 'a' || [] ? {} : undefined;", + }, + ], + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 22, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: + "let x = null || 0 || ('a'.length > 0) || [] ? {} : undefined;", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: + 'let x = null || 0 || (\'a\' !== "") || [] ? {} : undefined;', + }, + { + messageId: 'conditionFixCastBoolean', + output: + "let x = null || 0 || (Boolean('a')) || [] ? {} : undefined;", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 29, + }, ], }, { @@ -601,10 +975,54 @@ if (y) { code: "return !(null || 0 || 'a' || []);", output: null, errors: [ - { messageId: 'conditionErrorNullish', line: 1, column: 10 }, - { messageId: 'conditionErrorNumber', line: 1, column: 18 }, - { messageId: 'conditionErrorString', line: 1, column: 23 }, - { messageId: 'conditionErrorObject', line: 1, column: 30 }, + { + messageId: 'conditionErrorNullish', + line: 1, + column: 10, + }, + { + messageId: 'conditionErrorNumber', + line: 1, + column: 18, + suggestions: [ + { + messageId: 'conditionFixCompareZero', + output: "return !(null || (0 !== 0) || 'a' || []);", + }, + { + messageId: 'conditionFixCompareNaN', + output: "return !(null || (!Number.isNaN(0)) || 'a' || []);", + }, + { + messageId: 'conditionFixCastBoolean', + output: "return !(null || (Boolean(0)) || 'a' || []);", + }, + ], + }, + { + messageId: 'conditionErrorString', + line: 1, + column: 23, + suggestions: [ + { + messageId: 'conditionFixCompareStringLength', + output: "return !(null || 0 || ('a'.length > 0) || []);", + }, + { + messageId: 'conditionFixCompareEmptyString', + output: 'return !(null || 0 || (\'a\' !== "") || []);', + }, + { + messageId: 'conditionFixCastBoolean', + output: "return !(null || 0 || (Boolean('a')) || []);", + }, + ], + }, + { + messageId: 'conditionErrorObject', + line: 1, + column: 30, + }, ], }, diff --git a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts index ff4e2a4199db..792533c9c2b3 100644 --- a/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts +++ b/packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts @@ -1561,6 +1561,37 @@ switch (day) { missingBranches: '"Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday"', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +type Day = + | 'Monday' + | 'Tuesday' + | 'Wednesday' + | 'Thursday' + | 'Friday' + | 'Saturday' + | 'Sunday'; + +const day = 'Monday' as Day; +let result = 0; + +switch (day) { + case 'Monday': { + result = 1; + break; + } + case "Tuesday": { throw new Error('Not implemented yet: "Tuesday" case') } + case "Wednesday": { throw new Error('Not implemented yet: "Wednesday" case') } + case "Thursday": { throw new Error('Not implemented yet: "Thursday" case') } + case "Friday": { throw new Error('Not implemented yet: "Friday" case') } + case "Saturday": { throw new Error('Not implemented yet: "Saturday" case') } + case "Sunday": { throw new Error('Not implemented yet: "Sunday" case') } +} + `, + }, + ], }, ], }, @@ -1587,6 +1618,25 @@ function test(value: Enum): number { data: { missingBranches: 'Enum.B', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +enum Enum { + A, + B, +} + +function test(value: Enum): number { + switch (value) { + case Enum.A: + return 1; + case Enum.B: { throw new Error('Not implemented yet: Enum.B case') } + } +} + `, + }, + ], }, ], }, @@ -1612,6 +1662,26 @@ function test(value: Union): number { data: { missingBranches: '"b" | "c"', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +type A = 'a'; +type B = 'b'; +type C = 'c'; +type Union = A | B | C; + +function test(value: Union): number { + switch (value) { + case 'a': + return 1; + case "b": { throw new Error('Not implemented yet: "b" case') } + case "c": { throw new Error('Not implemented yet: "c" case') } + } +} + `, + }, + ], }, ], }, @@ -1638,6 +1708,27 @@ function test(value: Union): number { data: { missingBranches: 'true | 1', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +const A = 'a'; +const B = 1; +const C = true; + +type Union = typeof A | typeof B | typeof C; + +function test(value: Union): number { + switch (value) { + case 'a': + return 1; + case true: { throw new Error('Not implemented yet: true case') } + case 1: { throw new Error('Not implemented yet: 1 case') } + } +} + `, + }, + ], }, ], }, @@ -1660,6 +1751,22 @@ function test(value: DiscriminatedUnion): number { data: { missingBranches: '"B"', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +type DiscriminatedUnion = { type: 'A'; a: 1 } | { type: 'B'; b: 2 }; + +function test(value: DiscriminatedUnion): number { + switch (value.type) { + case 'A': + return 1; + case "B": { throw new Error('Not implemented yet: "B" case') } + } +} + `, + }, + ], }, ], }, @@ -1689,6 +1796,33 @@ switch (day) { missingBranches: '"Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday"', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +type Day = + | 'Monday' + | 'Tuesday' + | 'Wednesday' + | 'Thursday' + | 'Friday' + | 'Saturday' + | 'Sunday'; + +const day = 'Monday' as Day; + +switch (day) { +case "Monday": { throw new Error('Not implemented yet: "Monday" case') } +case "Tuesday": { throw new Error('Not implemented yet: "Tuesday" case') } +case "Wednesday": { throw new Error('Not implemented yet: "Wednesday" case') } +case "Thursday": { throw new Error('Not implemented yet: "Thursday" case') } +case "Friday": { throw new Error('Not implemented yet: "Friday" case') } +case "Saturday": { throw new Error('Not implemented yet: "Saturday" case') } +case "Sunday": { throw new Error('Not implemented yet: "Sunday" case') } +} + `, + }, + ], }, ], }, @@ -1715,6 +1849,27 @@ function test(value: T): number { data: { missingBranches: 'typeof b | typeof c', }, + suggestions: [ + { + messageId: 'addMissingCases', + output: ` +const a = Symbol('a'); +const b = Symbol('b'); +const c = Symbol('c'); + +type T = typeof a | typeof b | typeof c; + +function test(value: T): number { + switch (value) { + case a: + return 1; + case b: { throw new Error('Not implemented yet: b case') } + case c: { throw new Error('Not implemented yet: c case') } + } +} + `, + }, + ], }, ], }, diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index 059702547dc1..ae926072ab03 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -29,6 +29,7 @@ import type { NormalizedRunTests, RuleTesterConfig, RunTests, + SuggestionOutput, TesterConfigWithDefaults, ValidTestCase, } from './types'; @@ -40,7 +41,6 @@ import { freezeDeeply } from './utils/freezeDeeply'; import { getRuleOptionsSchema } from './utils/getRuleOptionsSchema'; import { hasOwnProperty } from './utils/hasOwnProperty'; import { getPlaceholderMatcher, interpolate } from './utils/interpolate'; -import { isReadonlyArray } from './utils/isReadonlyArray'; import { isSerializable } from './utils/serialization'; import * as SourceCodeFixer from './utils/SourceCodeFixer'; import { @@ -528,7 +528,17 @@ export class RuleTester extends TestFramework { config = merge(config, itemConfig); } - if (item.filename) { + if (hasOwnProperty(item, 'only')) { + assert.ok( + typeof item.only === 'boolean', + "Optional test case property 'only' must be a boolean", + ); + } + if (hasOwnProperty(item, 'filename')) { + assert.ok( + typeof item.filename === 'string', + "Optional test case property 'filename' must be a string", + ); filename = item.filename; } @@ -825,6 +835,10 @@ export class RuleTester extends TestFramework { if (typeof error === 'string' || error instanceof RegExp) { // Just an error message. assertMessageMatches(message.message, error); + assert.ok( + message.suggestions === undefined, + `Error at index ${i} has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions.`, + ); } else if (typeof error === 'object' && error != null) { /* * Error object. @@ -902,15 +916,12 @@ export class RuleTester extends TestFramework { `Hydrated message "${rehydratedMessage}" does not match "${message.message}"`, ); } + } else { + assert.fail( + "Test error must specify either a 'messageId' or 'message'.", + ); } - assert.ok( - hasOwnProperty(error, 'data') - ? hasOwnProperty(error, 'messageId') - : true, - "Error must specify 'messageId' if 'data' is used.", - ); - if (error.type) { assert.strictEqual( message.nodeType, @@ -951,149 +962,180 @@ export class RuleTester extends TestFramework { ); } + assert.ok( + !message.suggestions || hasOwnProperty(error, 'suggestions'), + `Error at index ${i} has suggestions. Please specify 'suggestions' property on the test error object.`, + ); if (hasOwnProperty(error, 'suggestions')) { // Support asserting there are no suggestions - if ( - !error.suggestions || - (isReadonlyArray(error.suggestions) && - error.suggestions.length === 0) - ) { - if ( - Array.isArray(message.suggestions) && - message.suggestions.length > 0 - ) { - assert.fail( - `Error should have no suggestions on error with message: "${message.message}"`, - ); - } - } else { - assert( - Array.isArray(message.suggestions), - `Error should have an array of suggestions. Instead received "${String( - message.suggestions, - )}" on error with message: "${message.message}"`, + const expectsSuggestions = Array.isArray(error.suggestions) + ? error.suggestions.length > 0 + : Boolean(error.suggestions); + const hasSuggestions = message.suggestions !== void 0; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const messageSuggestions = message.suggestions!; + + if (!hasSuggestions && expectsSuggestions) { + assert.ok( + !error.suggestions, + `Error should have suggestions on error with message: "${message.message}"`, ); - const messageSuggestions = message.suggestions; - assert.strictEqual( - messageSuggestions.length, - error.suggestions.length, - `Error should have ${error.suggestions.length} suggestions. Instead found ${messageSuggestions.length} suggestions`, + } else if (hasSuggestions) { + assert.ok( + expectsSuggestions, + `Error should have no suggestions on error with message: "${message.message}"`, ); - - error.suggestions.forEach((expectedSuggestion, index) => { - assert.ok( - typeof expectedSuggestion === 'object' && - expectedSuggestion != null, - "Test suggestion in 'suggestions' array must be an object.", + if (typeof error.suggestions === 'number') { + assert.strictEqual( + messageSuggestions.length, + error.suggestions, + // It is possible that error.suggestions is a number + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Error should have ${error.suggestions} suggestions. Instead found ${messageSuggestions.length} suggestions`, + ); + } else if (Array.isArray(error.suggestions)) { + assert.strictEqual( + messageSuggestions.length, + error.suggestions.length, + `Error should have ${error.suggestions.length} suggestions. Instead found ${messageSuggestions.length} suggestions`, ); - Object.keys(expectedSuggestion).forEach(propertyName => { - assert.ok( - SUGGESTION_OBJECT_PARAMETERS.has(propertyName), - `Invalid suggestion property name '${propertyName}'. Expected one of ${FRIENDLY_SUGGESTION_OBJECT_PARAMETER_LIST}.`, - ); - }); - - const actualSuggestion = messageSuggestions[index]; - const suggestionPrefix = `Error Suggestion at index ${index} :`; - - // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` - if (hasOwnProperty(expectedSuggestion, 'desc')) { - assert.ok( - !hasOwnProperty(expectedSuggestion, 'data'), - `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`, - ); - // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` - const expectedDesc = expectedSuggestion.desc as string; - assert.strictEqual( - actualSuggestion.desc, - expectedDesc, - `${suggestionPrefix} desc should be "${expectedDesc}" but got "${actualSuggestion.desc}" instead.`, - ); - } - - if (hasOwnProperty(expectedSuggestion, 'messageId')) { - assert.ok( - ruleHasMetaMessages, - `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`, - ); - assert.ok( - hasOwnProperty( - rule.meta.messages, - expectedSuggestion.messageId, - ), - `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`, - ); - assert.strictEqual( - actualSuggestion.messageId, - expectedSuggestion.messageId, - `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`, - ); - - const unsubstitutedPlaceholders = - getUnsubstitutedMessagePlaceholders( - actualSuggestion.desc, - rule.meta.messages[expectedSuggestion.messageId], - expectedSuggestion.data, - ); - assert.ok( - unsubstitutedPlaceholders.length === 0, - `The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(', ')}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? 'values' : 'value'} via the 'data' property for the suggestion in the context.report() call.`, - ); - - if (hasOwnProperty(expectedSuggestion, 'data')) { - const unformattedMetaMessage = - rule.meta.messages[expectedSuggestion.messageId]; - const rehydratedDesc = interpolate( - unformattedMetaMessage, - expectedSuggestion.data, + error.suggestions.forEach( + (expectedSuggestion: SuggestionOutput, index) => { + assert.ok( + typeof expectedSuggestion === 'object' && + expectedSuggestion != null, + "Test suggestion in 'suggestions' array must be an object.", + ); + Object.keys(expectedSuggestion).forEach(propertyName => { + assert.ok( + SUGGESTION_OBJECT_PARAMETERS.has(propertyName), + `Invalid suggestion property name '${propertyName}'. Expected one of ${FRIENDLY_SUGGESTION_OBJECT_PARAMETER_LIST}.`, + ); + }); + + const actualSuggestion = messageSuggestions[index]; + const suggestionPrefix = `Error Suggestion at index ${index}:`; + + // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` + if (hasOwnProperty(expectedSuggestion, 'desc')) { + // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId` + const expectedDesc = expectedSuggestion.desc as string; + + assert.ok( + !hasOwnProperty(expectedSuggestion, 'data'), + `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`, + ); + assert.ok( + !hasOwnProperty(expectedSuggestion, 'messageId'), + `${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.`, + ); + assert.strictEqual( + actualSuggestion.desc, + expectedDesc, + `${suggestionPrefix} desc should be "${expectedDesc}" but got "${actualSuggestion.desc}" instead.`, + ); + } else if ( + hasOwnProperty(expectedSuggestion, 'messageId') + ) { + assert.ok( + ruleHasMetaMessages, + `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`, + ); + assert.ok( + hasOwnProperty( + rule.meta.messages, + expectedSuggestion.messageId, + ), + `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`, + ); + assert.strictEqual( + actualSuggestion.messageId, + expectedSuggestion.messageId, + `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`, + ); + + const unsubstitutedPlaceholders = + getUnsubstitutedMessagePlaceholders( + actualSuggestion.desc, + rule.meta.messages[expectedSuggestion.messageId], + expectedSuggestion.data, + ); + + assert.ok( + unsubstitutedPlaceholders.length === 0, + `The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(', ')}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? 'values' : 'value'} via the 'data' property for the suggestion in the context.report() call.`, + ); + + if (hasOwnProperty(expectedSuggestion, 'data')) { + const unformattedMetaMessage = + rule.meta.messages[expectedSuggestion.messageId]; + const rehydratedDesc = interpolate( + unformattedMetaMessage, + expectedSuggestion.data, + ); + + assert.strictEqual( + actualSuggestion.desc, + rehydratedDesc, + `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`, + ); + } + } else if (hasOwnProperty(expectedSuggestion, 'data')) { + assert.fail( + `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`, + ); + } else { + assert.fail( + `${suggestionPrefix} Test must specify either 'messageId' or 'desc'.`, + ); + } + + assert.ok( + hasOwnProperty(expectedSuggestion, 'output'), + `${suggestionPrefix} The "output" property is required.`, + ); + const codeWithAppliedSuggestion = + SourceCodeFixer.applyFixes(item.code, [ + actualSuggestion, + ]).output; + + // Verify if suggestion fix makes a syntax error or not. + const errorMessageInSuggestion = this.#linter + .verify( + codeWithAppliedSuggestion, + result.config, + result.filename, + ) + .find(m => m.fatal); + + assert( + !errorMessageInSuggestion, + [ + 'A fatal parsing error occurred in suggestion fix.', + `Error: ${errorMessageInSuggestion?.message}`, + 'Suggestion output:', + codeWithAppliedSuggestion, + ].join('\n'), ); assert.strictEqual( - actualSuggestion.desc, - rehydratedDesc, - `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`, - ); - } - } else { - assert.ok( - !hasOwnProperty(expectedSuggestion, 'data'), - `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`, - ); - } - - if (hasOwnProperty(expectedSuggestion, 'output')) { - const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes( - item.code, - [actualSuggestion], - ).output; - - // Verify if suggestion fix makes a syntax error or not. - const errorMessageInSuggestion = this.#linter - .verify( - codeWithAppliedSuggestion, - result.config, - result.filename, - ) - .find(m => m.fatal); - - assert( - !errorMessageInSuggestion, - [ - 'A fatal parsing error occurred in suggestion fix.', - `Error: ${errorMessageInSuggestion?.message}`, - 'Suggestion output:', codeWithAppliedSuggestion, - ].join('\n'), - ); - - assert.strictEqual( - codeWithAppliedSuggestion, - expectedSuggestion.output, - `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`, - ); - } - }); + expectedSuggestion.output, + `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`, + ); + assert.notStrictEqual( + expectedSuggestion.output, + item.code, + `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`, + ); + }, + ); + } else { + assert.fail( + "Test error object property 'suggestions' should be an array or a number", + ); + } } } } else { @@ -1123,6 +1165,11 @@ export class RuleTester extends TestFramework { item.code, "The rule fixed the code. Please add 'output' property.", ); + assert.notStrictEqual( + item.code, + item.output, + "Test property 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null.", + ); } assertASTDidntChange(result.beforeAST, result.afterAST); diff --git a/packages/rule-tester/tests/eslint-base/eslint-base.test.js b/packages/rule-tester/tests/eslint-base/eslint-base.test.js index a50654f922aa..e87ece634534 100644 --- a/packages/rule-tester/tests/eslint-base/eslint-base.test.js +++ b/packages/rule-tester/tests/eslint-base/eslint-base.test.js @@ -473,7 +473,7 @@ describe("RuleTester", () => { "bar = baz;" ], invalid: [ - { code: "var foo = bar; var baz = quux", errors: [{ type: "VariableDeclaration" }, null] } + { code: "var foo = bar; var baz = quux", errors: [{ message: "Bad var.", type: "VariableDeclaration" }, null] } ] }); }, /Error should be a string, object, or RegExp/u); @@ -529,6 +529,26 @@ describe("RuleTester", () => { }); }); + it("should not throw an error when the error is a string and the suggestion fixer is failing", () => { + ruleTester.run("no-var", require("./fixtures/suggestions").withFailingFixer, { + valid: [], + invalid: [ + { code: "foo", errors: ["some message"] } + ] + }); + }); + + it("throws an error when the error is a string and the suggestion fixer provides a fix", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/suggestions").basic, { + valid: [], + invalid: [ + { code: "foo", errors: ["Avoid using identifiers named 'foo'."] } + ] + }); + }, "Error at index 0 has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions."); + }); + it("should throw an error when the error is an object with an unknown property name", () => { assert.throws(() => { ruleTester.run("no-var", require("./fixtures/no-var"), { @@ -678,6 +698,17 @@ describe("RuleTester", () => { }, /Expected no autofixes to be suggested/u); }); + it("should throw an error when the expected output is not null and the output does not differ from the code", () => { + assert.throws(() => { + ruleTester.run("no-var", require("./fixtures/no-eval"), { + valid: [], + invalid: [ + { code: "eval('')", output: "eval('')", errors: 1 } + ] + }); + }, "Test property 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null."); + }); + it("should throw an error when the expected output isn't specified and problems produce output", () => { assert.throws(() => { ruleTester.run("no-var", require("./fixtures/no-var"), { @@ -913,14 +944,28 @@ describe("RuleTester", () => { }, /fatal parsing error/iu); }); - it("should not throw an error if invalid code has at least an expected empty error object", () => { - ruleTester.run("no-eval", require("./fixtures/no-eval"), { - valid: ["Eval(foo)"], - invalid: [{ - code: "eval(foo)", - errors: [{}] - }] - }); + it("should throw an error if an error object has no properties", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: ["Eval(foo)"], + invalid: [{ + code: "eval(foo)", + errors: [{}] + }] + }); + }, "Test error must specify either a 'messageId' or 'message'."); + }); + + it("should throw an error if an error has a property besides message or messageId", () => { + assert.throws(() => { + ruleTester.run("no-eval", require("./fixtures/no-eval"), { + valid: ["Eval(foo)"], + invalid: [{ + code: "eval(foo)", + errors: [{ line: 1 }] + }] + }); + }, "Test error must specify either a 'messageId' or 'message'."); }); it("should pass-through the globals config of valid tests to the to rule", () => { @@ -1048,7 +1093,7 @@ describe("RuleTester", () => { { code: "eval(foo)", parser: require.resolve("esprima"), - errors: [{ line: 1 }] + errors: [{ message: "eval sucks.", line: 1 }] } ] }); @@ -1687,60 +1732,60 @@ describe("RuleTester", () => { }); it("should throw if the message has a single unsubstituted placeholder when data is not specified", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withMissingData, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] - }); - }, "The reported message has an unsubstituted placeholder 'name'. Please provide the missing value via the 'data' property in the context.report() call."); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withMissingData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); + }, "The reported message has an unsubstituted placeholder 'name'. Please provide the missing value via the 'data' property in the context.report() call."); }); it("should throw if the message has a single unsubstituted placeholders when data is specified", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withMissingData, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "name" } }] }] - }); - }, "Hydrated message \"Avoid using variables named 'name'.\" does not match \"Avoid using variables named '{{ name }}'."); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withMissingData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "name" } }] }] + }); + }, "Hydrated message \"Avoid using variables named 'name'.\" does not match \"Avoid using variables named '{{ name }}'."); }); it("should throw if the message has multiple unsubstituted placeholders when data is not specified", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withMultipleMissingDataProperties, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] - }); - }, "The reported message has unsubstituted placeholders: 'type', 'name'. Please provide the missing values via the 'data' property in the context.report() call."); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withMultipleMissingDataProperties, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); + }, "The reported message has unsubstituted placeholders: 'type', 'name'. Please provide the missing values via the 'data' property in the context.report() call."); }); it("should not throw if the data in the message contains placeholders not present in the raw message", () => { - ruleTester.run("foo", require("./fixtures/messageId").withPlaceholdersInData, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] - }); + ruleTester.run("foo", require("./fixtures/messageId").withPlaceholdersInData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); }); it("should throw if the data in the message contains the same placeholder and data is not specified", () => { - assert.throws(() => { - ruleTester.run("foo", require("./fixtures/messageId").withSamePlaceholdersInData, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] - }); - }, "The reported message has an unsubstituted placeholder 'name'. Please provide the missing value via the 'data' property in the context.report() call."); + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/messageId").withSamePlaceholdersInData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }] + }); + }, "The reported message has an unsubstituted placeholder 'name'. Please provide the missing value via the 'data' property in the context.report() call."); }); it("should not throw if the data in the message contains the same placeholder and data is specified", () => { - ruleTester.run("foo", require("./fixtures/messageId").withSamePlaceholdersInData, { - valid: [], - invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "{{ name }}" } }] }] - }); + ruleTester.run("foo", require("./fixtures/messageId").withSamePlaceholdersInData, { + valid: [], + invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "{{ name }}" } }] }] + }); }); it("should not throw an error for specifying non-string data values", () => { - ruleTester.run("foo", require("./fixtures/messageId").withNonStringData, { - valid: [], - invalid: [{ code: "0", errors: [{ messageId: "avoid", data: { value: 0 } }] }] - }); + ruleTester.run("foo", require("./fixtures/messageId").withNonStringData, { + valid: [], + invalid: [{ code: "0", errors: [{ messageId: "avoid", data: { value: 0 } }] }] + }); }); // messageId/message misconfiguration cases @@ -1774,10 +1819,24 @@ describe("RuleTester", () => { valid: [], invalid: [{ code: "foo", errors: [{ data: "something" }] }] }); - }, "Error must specify 'messageId' if 'data' is used."); + }, "Test error must specify either a 'messageId' or 'message'."); }); describe("suggestions", () => { + it("should throw if suggestions are available but not specified", () => { + assert.throw(() => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [ + "var boo;" + ], + invalid: [{ + code: "var foo;", + errors: [{ message: "Avoid using identifiers named 'foo'." }] + }] + }); + }, "Error at index 0 has suggestions. Please specify 'suggestions' property on the test error object."); + }); + it("should pass with valid suggestions (tested using desc)", () => { ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { valid: [ @@ -1786,6 +1845,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var bar;" @@ -1802,11 +1862,13 @@ describe("RuleTester", () => { { code: "function foo() {\n var foo = 1;\n}", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "function bar() {\n var foo = 1;\n}" }] }, { + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "function foo() {\n var bar = 1;\n}" @@ -1823,6 +1885,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -1841,6 +1904,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -1853,24 +1917,27 @@ describe("RuleTester", () => { }); }); - it("should pass with valid suggestions (tested using both desc and messageIds for the same suggestion)", () => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", - output: "var bar;" - }, { - desc: "Rename identifier 'foo' to 'baz'", - messageId: "renameFoo", - output: "var baz;" + it("should fail with valid suggestions when testing using both desc and messageIds for the same suggestion", () => { + assert.throw(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + messageId: "renameFoo", + output: "var bar;" + }, { + desc: "Rename identifier 'foo' to 'baz'", + messageId: "renameFoo", + output: "var baz;" + }] }] }] - }] - }); + }); + }, "Error Suggestion at index 0: Test should not specify both 'desc' and 'messageId'."); }); it("should pass with valid suggestions (tested using only desc on a rule that utilizes meta.messages)", () => { @@ -1879,6 +1946,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var bar;" @@ -1897,6 +1965,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "bar" }, @@ -1912,71 +1981,89 @@ describe("RuleTester", () => { }); it("should fail with a single missing data placeholder when data is not specified", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMissingPlaceholderData, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - messageId: "avoidFoo", - suggestions: [{ - messageId: "renameFoo", - output: "var bar;" - }] - }] - }] - }); - }, "The message of the suggestion has an unsubstituted placeholder 'newName'. Please provide the missing value via the 'data' property for the suggestion in the context.report() call."); + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMissingPlaceholderData, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + messageId: "renameFoo", + output: "var bar;" + }] + }] + }] + }); + }, "The message of the suggestion has an unsubstituted placeholder 'newName'. Please provide the missing value via the 'data' property for the suggestion in the context.report() call."); }); it("should fail with a single missing data placeholder when data is specified", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMissingPlaceholderData, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - messageId: "avoidFoo", - suggestions: [{ - messageId: "renameFoo", - data: { other: "name" }, - output: "var bar;" - }] - }] - }] - }); - }, "The message of the suggestion has an unsubstituted placeholder 'newName'. Please provide the missing value via the 'data' property for the suggestion in the context.report() call."); + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMissingPlaceholderData, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + messageId: "renameFoo", + data: { other: "name" }, + output: "var bar;" + }] + }] + }] + }); + }, "The message of the suggestion has an unsubstituted placeholder 'newName'. Please provide the missing value via the 'data' property for the suggestion in the context.report() call."); }); it("should fail with multiple missing data placeholders when data is not specified", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMultipleMissingPlaceholderDataProperties, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - messageId: "avoidFoo", - suggestions: [{ - messageId: "rename", - output: "var bar;" - }] - }] - }] - }); - }, "The message of the suggestion has unsubstituted placeholders: 'currentName', 'newName'. Please provide the missing values via the 'data' property for the suggestion in the context.report() call."); + assert.throws(() => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMultipleMissingPlaceholderDataProperties, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + messageId: "rename", + output: "var bar;" + }] + }] + }] + }); + }, "The message of the suggestion has unsubstituted placeholders: 'currentName', 'newName'. Please provide the missing values via the 'data' property for the suggestion in the context.report() call."); }); - it("should pass when tested using empty suggestion test objects if the array length is correct", () => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{}, {}] + it("should fail when tested using empty suggestion test objects even if the array length is correct", () => { + assert.throw(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{}, {}] + }] }] - }] - }); + }); + }, "Error Suggestion at index 0: Test must specify either 'messageId' or 'desc'"); + }); + + it("should fail when tested using non-empty suggestion test objects without an output property", () => { + assert.throw(() => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ messageId: "renameFoo" }, {}] + }] + }] + }); + }, 'Error Suggestion at index 0: The "output" property is required.'); }); it("should support explicitly expecting no suggestions", () => { @@ -1986,6 +2073,7 @@ describe("RuleTester", () => { invalid: [{ code: "eval('var foo');", errors: [{ + message: "eval sucks.", suggestions }] }] @@ -2001,6 +2089,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions }] }] @@ -2017,12 +2106,26 @@ describe("RuleTester", () => { code: "var foo;", errors: [{ suggestions: [{ + message: "Bad var.", messageId: "this-does-not-exist" }] }] }] }); - }, "Error should have an array of suggestions. Instead received \"undefined\" on error with message: \"Bad var.\""); + }, 'Error should have suggestions on error with message: "Bad var."'); + }); + + it("should support specifying only the amount of suggestions", () => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + message: "Avoid using identifiers named 'foo'.", + suggestions: 1 + }] + }] + }); }); it("should fail when there are a different number of suggestions", () => { @@ -2032,6 +2135,22 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", + suggestions: 2 + }] + }] + }); + }, "Error should have 2 suggestions. Instead found 1 suggestions"); + }); + + it("should fail when there are a different number of suggestions for arrays", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var bar;" @@ -2045,6 +2164,21 @@ describe("RuleTester", () => { }, "Error should have 2 suggestions. Instead found 1 suggestions"); }); + it("should fail when the suggestion property is neither a number nor an array", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + message: "Avoid using identifiers named 'foo'.", + suggestions: "1" + }] + }] + }); + }, "Test error object property 'suggestions' should be an array or a number"); + }); + it("should throw if suggestion fix made a syntax error.", () => { assert.throw(() => { ruleTester.run( @@ -2104,26 +2238,23 @@ describe("RuleTester", () => { }, "Error Suggestion at index 0 : desc should be \"not right\" but got \"Rename identifier 'foo' to 'bar'\" instead."); }); - it("should throw if the suggestion description doesn't match (although messageIds match)", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", - output: "var bar;" - }, { - desc: "Rename id 'foo' to 'baz'", - messageId: "renameFoo", - output: "var baz;" - }] + it("should pass when different suggestion matchers use desc and messageId", () => { + ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + messageId: "avoidFoo", + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "var bar;" + }, { + messageId: "renameFoo", + output: "var baz;" }] }] - }); - }, "Error Suggestion at index 1 : desc should be \"Rename id 'foo' to 'baz'\" but got \"Rename identifier 'foo' to 'baz'\" instead."); + }] + }); }); it("should throw if the suggestion messageId doesn't match", () => { @@ -2133,6 +2264,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "unused", output: "var bar;" @@ -2143,29 +2275,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : messageId should be 'unused' but got 'renameFoo' instead."); - }); - - it("should throw if the suggestion messageId doesn't match (although descriptions match)", () => { - assert.throws(() => { - ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, { - valid: [], - invalid: [{ - code: "var foo;", - errors: [{ - suggestions: [{ - desc: "Rename identifier 'foo' to 'bar'", - messageId: "renameFoo", - output: "var bar;" - }, { - desc: "Rename identifier 'foo' to 'baz'", - messageId: "avoidFoo", - output: "var baz;" - }] - }] - }] - }); - }, "Error Suggestion at index 1 : messageId should be 'avoidFoo' but got 'renameFoo' instead."); + }, "Error Suggestion at index 0: messageId should be 'unused' but got 'renameFoo' instead."); }); it("should throw if test specifies messageId for a rule that doesn't have meta.messages", () => { @@ -2175,6 +2285,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -2182,7 +2293,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : Test can not use 'messageId' if rule under test doesn't define 'meta.messages'."); + }, "Error Suggestion at index 0: Test can not use 'messageId' if rule under test doesn't define 'meta.messages'."); }); it("should throw if test specifies messageId that doesn't exist in the rule's meta.messages", () => { @@ -2192,6 +2303,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", output: "var bar;" @@ -2202,7 +2314,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 1 : Test has invalid messageId 'removeFoo', the rule under test allows only one of ['avoidFoo', 'unused', 'renameFoo']."); + }, "Error Suggestion at index 1: Test has invalid messageId 'removeFoo', the rule under test allows only one of ['avoidFoo', 'unused', 'renameFoo']."); }); it("should throw if hydrated desc doesn't match (wrong data value)", () => { @@ -2212,6 +2324,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "car" }, @@ -2224,7 +2337,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : Hydrated test desc \"Rename identifier 'foo' to 'car'\" does not match received desc \"Rename identifier 'foo' to 'bar'\"."); + }, "Error Suggestion at index 0: Hydrated test desc \"Rename identifier 'foo' to 'car'\" does not match received desc \"Rename identifier 'foo' to 'bar'\"."); }); it("should throw if hydrated desc doesn't match (wrong data key)", () => { @@ -2234,6 +2347,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "bar" }, @@ -2246,7 +2360,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 1 : Hydrated test desc \"Rename identifier 'foo' to '{{ newName }}'\" does not match received desc \"Rename identifier 'foo' to 'baz'\"."); + }, "Error Suggestion at index 1: Hydrated test desc \"Rename identifier 'foo' to '{{ newName }}'\" does not match received desc \"Rename identifier 'foo' to 'baz'\"."); }); it("should throw if test specifies both desc and data", () => { @@ -2256,6 +2370,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", messageId: "renameFoo", @@ -2269,7 +2384,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 0 : Test should not specify both 'desc' and 'data'."); + }, "Error Suggestion at index 0: Test should not specify both 'desc' and 'data'."); }); it("should throw if test uses data but doesn't specify messageId", () => { @@ -2279,6 +2394,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + messageId: "avoidFoo", suggestions: [{ messageId: "renameFoo", data: { newName: "bar" }, @@ -2290,7 +2406,7 @@ describe("RuleTester", () => { }] }] }); - }, "Error Suggestion at index 1 : Test must specify 'messageId' if 'data' is used."); + }, "Error Suggestion at index 1: Test must specify 'messageId' if 'data' is used."); }); it("should throw if the resulting suggestion output doesn't match", () => { @@ -2300,6 +2416,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [{ desc: "Rename identifier 'foo' to 'bar'", output: "var baz;" @@ -2310,6 +2427,24 @@ describe("RuleTester", () => { }, "Expected the applied suggestion fix to match the test suggestion output"); }); + it("should throw if the resulting suggestion output is the same as the original source code", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("./fixtures/suggestions").withFixerWithoutChanges, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + message: "Avoid using identifiers named 'foo'.", + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "var foo;" + }] + }] + }] + }); + }, "The output of a suggestion should differ from the original source code for suggestion at index: 0 on error with message: \"Avoid using identifiers named 'foo'.\""); + }); + it("should fail when specified suggestion isn't an object", () => { assert.throws(() => { ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, { @@ -2317,6 +2452,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "Avoid using identifiers named 'foo'.", suggestions: [null] }] }] @@ -2329,6 +2465,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "avoidFoo", suggestions: [ { messageId: "renameFoo", @@ -2351,6 +2488,7 @@ describe("RuleTester", () => { invalid: [{ code: "var foo;", errors: [{ + message: "avoidFoo", suggestions: [{ message: "Rename identifier 'foo' to 'bar'" }] @@ -2381,37 +2519,37 @@ describe("RuleTester", () => { }); it("should fail if a rule produces two suggestions with the same description", () => { - assert.throws(() => { - ruleTester.run("suggestions-with-duplicate-descriptions", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateDescriptions, { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - }, "Suggestion message 'Rename 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); - }); - - it("should fail if a rule produces two suggestions with the same messageId without data", () => { - assert.throws(() => { - ruleTester.run("suggestions-with-duplicate-messageids-no-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsNoData, { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - }, "Suggestion message 'Rename identifier' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); - }); - - it("should fail if a rule produces two suggestions with the same messageId with data", () => { - assert.throws(() => { - ruleTester.run("suggestions-with-duplicate-messageids-with-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsWithData, { - valid: [], - invalid: [ - { code: "var foo = bar;", errors: 1 } - ] - }); - }, "Suggestion message 'Rename identifier 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); - }); + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-descriptions", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateDescriptions, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); + + it("should fail if a rule produces two suggestions with the same messageId without data", () => { + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-messageids-no-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsNoData, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename identifier' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); + + it("should fail if a rule produces two suggestions with the same messageId with data", () => { + assert.throws(() => { + ruleTester.run("suggestions-with-duplicate-messageids-with-data", require("../../fixtures/testers/rule-tester/suggestions").withDuplicateMessageIdsWithData, { + valid: [], + invalid: [ + { code: "var foo = bar;", errors: 1 } + ] + }); + }, "Suggestion message 'Rename identifier 'foo' to 'bar'' reported from suggestion 1 was previously reported by suggestion 0. Suggestion messages should be unique within an error."); + }); it("should throw an error if a rule that doesn't have `meta.hasSuggestions` enabled produces suggestions", () => { assert.throws(() => { @@ -2922,6 +3060,43 @@ describe("RuleTester", () => { }, /A fatal parsing error occurred in autofix.\nError: .+\nAutofix output:\n.+/u); }); + describe("type checking", () => { + it('should throw if "only" property is not a boolean', () => { + + // "only" has to be falsy as itOnly is not mocked for all test cases + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [{ code: "foo", only: "" }], + invalid: [] + }); + }, /Optional test case property 'only' must be a boolean/u); + + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [], + invalid: [{ code: "foo", only: 0, errors: 1 }] + }); + }, /Optional test case property 'only' must be a boolean/u); + }); + + it('should throw if "filename" property is not a string', () => { + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: [{ code: "foo", filename: false }], + invalid: [] + + }); + }, /Optional test case property 'filename' must be a string/u); + + assert.throws(() => { + ruleTester.run("foo", require("./fixtures/no-var"), { + valid: ["foo"], + invalid: [{ code: "foo", errors: 1, filename: 0 }] + }); + }, /Optional test case property 'filename' must be a string/u); + }); + }); + describe("sanitize test cases", () => { let originalRuleTesterIt; let spyRuleTesterIt; diff --git a/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js b/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js index 71781086f4b8..6310d0a2104a 100644 --- a/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js +++ b/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js @@ -167,6 +167,40 @@ module.exports.withoutHasSuggestionsProperty = { } }; +module.exports.withFixerWithoutChanges = { + meta: { hasSuggestions: true }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + message: "Avoid using identifiers named 'foo'.", + suggest: [{ + desc: "Rename identifier 'foo' to 'bar'", + fix: fixer => fixer.replaceText(node, 'foo') + }] + }); + } + } + }; + } +}; + +module.exports.withFailingFixer = { + create(context) { + return { + Identifier(node) { + context.report({ + node, + message: "some message", + suggest: [{ desc: "some suggestion", fix: fixer => null }] + }); + } + }; + } +}; + module.exports.withMissingPlaceholderData = { meta: { messages: { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy