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: {