diff --git a/src/rules/valid-expect-in-promise.ts b/src/rules/valid-expect-in-promise.ts new file mode 100644 index 0000000..9e27be6 --- /dev/null +++ b/src/rules/valid-expect-in-promise.ts @@ -0,0 +1,484 @@ +import { AST_NODE_TYPES, TSESTree, TSESLint } from '@typescript-eslint/utils' +import { createEslintRule, getAccessorValue, isSupportedAccessor,type KnownCallExpression, + getNodeName, + isFunction, + isIdentifier } from '../utils' +import { ModifierName } from '../utils/types' +import { + findTopMostCallExpression, + isTypeOfVitestFnCall, + parseVitestFnCall, + } from "../utils/parse-vitest-fn-call"; + +export const RULE_NAME = 'valid-expect-in-promise' +export type MESSAGE_IDS = + | 'expectInFloatingPromise' +const defaultAsyncMatchers = ['toRejectWith', 'toResolveWith'] + + + +type PromiseChainCallExpression = KnownCallExpression< + "then" | "catch" | "finally" +>; + +const isPromiseChainCall = ( + node: TSESTree.Node +): node is PromiseChainCallExpression => { + if ( + node.type === AST_NODE_TYPES.CallExpression && + node.callee.type === AST_NODE_TYPES.MemberExpression && + isSupportedAccessor(node.callee.property) + ) { + // promise methods should have at least 1 argument + if (node.arguments.length === 0) { + return false; + } + + switch (getAccessorValue(node.callee.property)) { + case "then": + return node.arguments.length < 3; + case "catch": + case "finally": + return node.arguments.length < 2; + } + } + + return false; +}; + +const isTestCaseCallWithCallbackArg = ( + node: TSESTree.CallExpression, + context: TSESLint.RuleContext +): boolean => { + const vitestCallFn = parseVitestFnCall(node, context); + + if (vitestCallFn?.type !== "test") { + return false; + } + + const isVitestEach = vitestCallFn.members.some( + (s) => getAccessorValue(s) === "each" + ); + + if ( + isVitestEach && + node.callee.type !== AST_NODE_TYPES.TaggedTemplateExpression + ) { + // isVitestEach but not a TaggedTemplateExpression, so this must be + // the `jest.each([])()` syntax which this rule doesn't support due + // to its complexity (see jest-community/eslint-plugin-jest#710) + // so we return true to trigger bailout + return true; + } + + const [, callback] = node.arguments; + + const callbackArgIndex = Number(isVitestEach); + + return ( + callback && + isFunction(callback) && + callback.params.length === 1 + callbackArgIndex + ); +}; + +const isPromiseMethodThatUsesValue = ( + node: TSESTree.AwaitExpression | TSESTree.ReturnStatement, + identifier: TSESTree.Identifier +): boolean => { + const { name } = identifier; + + if (node.argument === null) { + return false; + } + + if ( + node.argument.type === AST_NODE_TYPES.CallExpression && + node.argument.arguments.length > 0 + ) { + const nodeName = getNodeName(node.argument); + + if (["Promise.all", "Promise.allSettled"].includes(nodeName as string)) { + const [firstArg] = node.argument.arguments; + + if ( + firstArg.type === AST_NODE_TYPES.ArrayExpression && + firstArg.elements.some((nod) => nod && isIdentifier(nod, name)) + ) { + return true; + } + } + + if ( + ["Promise.resolve", "Promise.reject"].includes(nodeName as string) && + node.argument.arguments.length === 1 + ) { + return isIdentifier(node.argument.arguments[0], name); + } + } + + return isIdentifier(node.argument, name); +}; + +/** + * Attempts to determine if the runtime value represented by the given `identifier` + * is `await`ed within the given array of elements + */ +const isValueAwaitedInElements = ( + name: string, + elements: + | TSESTree.ArrayExpression["elements"] + | TSESTree.CallExpression["arguments"] +): boolean => { + for (const element of elements) { + if ( + element?.type === AST_NODE_TYPES.AwaitExpression && + isIdentifier(element.argument, name) + ) { + return true; + } + + if ( + element?.type === AST_NODE_TYPES.ArrayExpression && + isValueAwaitedInElements(name, element.elements) + ) { + return true; + } + } + + return false; +}; + +/** + * Attempts to determine if the runtime value represented by the given `identifier` + * is `await`ed as an argument along the given call expression + */ +const isValueAwaitedInArguments = ( + name: string, + call: TSESTree.CallExpression +): boolean => { + let node: TSESTree.Node = call; + + while (node) { + if (node.type === AST_NODE_TYPES.CallExpression) { + if (isValueAwaitedInElements(name, node.arguments)) { + return true; + } + + node = node.callee; + } + + if (node.type !== AST_NODE_TYPES.MemberExpression) { + break; + } + + node = node.object; + } + + return false; +}; + +const getLeftMostCallExpression = ( + call: TSESTree.CallExpression +): TSESTree.CallExpression => { + let leftMostCallExpression: TSESTree.CallExpression = call; + let node: TSESTree.Node = call; + + while (node) { + if (node.type === AST_NODE_TYPES.CallExpression) { + leftMostCallExpression = node; + node = node.callee; + } + + if (node.type !== AST_NODE_TYPES.MemberExpression) { + break; + } + + node = node.object; + } + + return leftMostCallExpression; +}; + +/** + * Attempts to determine if the runtime value represented by the given `identifier` + * is `await`ed or `return`ed within the given `body` of statements + */ +const isValueAwaitedOrReturned = ( + identifier: TSESTree.Identifier, + body: TSESTree.Statement[], + context: TSESLint.RuleContext +): boolean => { + const { name } = identifier; + + for (const node of body) { + // skip all nodes that are before this identifier, because they'd probably + // be affecting a different runtime value (e.g. due to reassignment) + if (node.range[0] <= identifier.range[0]) { + continue; + } + + if (node.type === AST_NODE_TYPES.ReturnStatement) { + return isPromiseMethodThatUsesValue(node, identifier); + } + + if (node.type === AST_NODE_TYPES.ExpressionStatement) { + // it's possible that we're awaiting the value as an argument + if (node.expression.type === AST_NODE_TYPES.CallExpression) { + if (isValueAwaitedInArguments(name, node.expression)) { + return true; + } + + const leftMostCall = getLeftMostCallExpression(node.expression); + const vitestFnCall = parseVitestFnCall(node.expression, context); + + if ( + vitestFnCall?.type === "expect" && + leftMostCall.arguments.length > 0 && + isIdentifier(leftMostCall.arguments[0], name) + ) { + if ( + vitestFnCall.members.some((m) => { + const v = getAccessorValue(m); + + return v === ModifierName.resolves || v === ModifierName.rejects; + }) + ) { + return true; + } + } + } + + if ( + node.expression.type === AST_NODE_TYPES.AwaitExpression && + isPromiseMethodThatUsesValue(node.expression, identifier) + ) { + return true; + } + + // (re)assignment changes the runtime value, so if we've not found an + // await or return already we act as if we've reached the end of the body + if (node.expression.type === AST_NODE_TYPES.AssignmentExpression) { + // unless we're assigning to the same identifier, in which case + // we might be chaining off the existing promise value + if ( + isIdentifier(node.expression.left, name) && + getNodeName(node.expression.right)?.startsWith(`${name}.`) && + isPromiseChainCall(node.expression.right) + ) { + continue; + } + + break; + } + } + + if ( + node.type === AST_NODE_TYPES.BlockStatement && + isValueAwaitedOrReturned(identifier, node.body, context) + ) { + return true; + } + } + + return false; +}; + +const findFirstBlockBodyUp = ( + node: TSESTree.Node +): TSESTree.BlockStatement["body"] => { + let parent: TSESTree.Node["parent"] = node; + + while (parent) { + if (parent.type === AST_NODE_TYPES.BlockStatement) { + return parent.body; + } + + parent = parent.parent; + } + + /* istanbul ignore next */ + throw new Error( + `Could not find BlockStatement - please file a github issue at https://github.com/vitest-dev/eslint-plugin-vitest` + ); +}; + +const isDirectlyWithinTestCaseCall = ( + node: TSESTree.Node, + context: TSESLint.RuleContext +): boolean => { + let parent: TSESTree.Node["parent"] = node; + + while (parent) { + if (isFunction(parent)) { + parent = parent.parent; + + return ( + parent?.type === AST_NODE_TYPES.CallExpression && + isTypeOfVitestFnCall(parent, context, ["test"]) + ); + } + + parent = parent.parent; + } + + return false; +}; + +const isVariableAwaitedOrReturned = ( + variable: TSESTree.VariableDeclarator, + context: TSESLint.RuleContext +): boolean => { + const body = findFirstBlockBodyUp(variable); + + // it's pretty much impossible for us to track destructuring assignments, + // so we return true to bailout gracefully + if (!isIdentifier(variable.id)) { + return true; + } + + return isValueAwaitedOrReturned(variable.id, body, context); +}; + +export default createEslintRule<[ + Partial<{ + alwaysAwait: boolean + asyncMatchers: string[] + minArgs: number + maxArgs: number + }> +], MESSAGE_IDS>({ + name: __filename, + meta: { + docs: { + description: + "Require promises that have expectations in their chain to be valid", + }, + messages: { + expectInFloatingPromise: + "This promise should either be returned or awaited to ensure the expects in its chain are called", + }, + type: "suggestion", + schema: [], + }, + defaultOptions: [{ + alwaysAwait: false, + asyncMatchers: defaultAsyncMatchers, + minArgs: 1, + maxArgs: 1 + }], + create(context) { + let inTestCaseWithDoneCallback = false; + // an array of booleans representing each promise chain we enter, with the + // boolean value representing if we think a given chain contains an expect + // in it's body. + // + // since we only care about the inner-most chain, we represent the state in + // reverse with the inner-most being the first item, as that makes it + // slightly less code to assign to by not needing to know the length + const chains: boolean[] = []; + + return { + CallExpression(node: TSESTree.CallExpression) { + // there are too many ways that the done argument could be used with + // promises that contain expect that would make the promise safe for us + if (isTestCaseCallWithCallbackArg(node, context)) { + inTestCaseWithDoneCallback = true; + + return; + } + + // if this call expression is a promise chain, add it to the stack with + // value of "false", as we assume there are no expect calls initially + if (isPromiseChainCall(node)) { + chains.unshift(false); + + return; + } + + // if we're within a promise chain, and this call expression looks like + // an expect call, mark the deepest chain as having an expect call + if ( + chains.length > 0 && + isTypeOfVitestFnCall(node, context, ["expect"]) + ) { + chains[0] = true; + } + }, + "CallExpression:exit"(node: TSESTree.CallExpression) { + // there are too many ways that the "done" argument could be used to + // make promises containing expects safe in a test for us to be able to + // accurately check, so we just bail out completely if it's present + if (inTestCaseWithDoneCallback) { + if (isTypeOfVitestFnCall(node, context, ["test"])) { + inTestCaseWithDoneCallback = false; + } + + return; + } + + if (!isPromiseChainCall(node)) { + return; + } + + // since we're exiting this call expression (which is a promise chain) + // we remove it from the stack of chains, since we're unwinding + const hasExpectCall = chains.shift(); + + // if the promise chain we're exiting doesn't contain an expect, + // then we don't need to check it for anything + if (!hasExpectCall) { + return; + } + + const { parent } = findTopMostCallExpression(node); + + // if we don't have a parent (which is technically impossible at runtime) + // or our parent is not directly within the test case, we stop checking + // because we're most likely in the body of a function being defined + // within the test, which we can't track + if (!parent || !isDirectlyWithinTestCaseCall(parent, context)) { + return; + } + + switch (parent.type) { + case AST_NODE_TYPES.VariableDeclarator: { + if (isVariableAwaitedOrReturned(parent, context)) { + return; + } + + break; + } + + case AST_NODE_TYPES.AssignmentExpression: { + if ( + parent.left.type === AST_NODE_TYPES.Identifier && + isValueAwaitedOrReturned( + parent.left, + findFirstBlockBodyUp(parent), + context + ) + ) { + return; + } + + break; + } + + case AST_NODE_TYPES.ExpressionStatement: + break; + + case AST_NODE_TYPES.ReturnStatement: + case AST_NODE_TYPES.AwaitExpression: + default: + return; + } + + context.report({ + messageId: "expectInFloatingPromise", + node: parent, + }); + }, + }; + }, +}); diff --git a/tests/valid-expect-in-promise.test.ts b/tests/valid-expect-in-promise.test.ts new file mode 100644 index 0000000..355acac --- /dev/null +++ b/tests/valid-expect-in-promise.test.ts @@ -0,0 +1,1642 @@ +import rule, { RULE_NAME } from '../src/rules/valid-expect-in-promise'; +import { ruleTester } from './ruleTester'; + +ruleTester.run(RULE_NAME, rule, { + valid: [ + "test('something', () => Promise.resolve().then(() => expect(1).toBe(2)));", + 'Promise.resolve().then(() => expect(1).toBe(2))', + 'const x = Promise.resolve().then(() => expect(1).toBe(2))', + ` + it('is valid', () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(promise).resolves.toBe(1); + }); + `, + ` + it('is valid', () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(promise).resolves.not.toBe(2); + }); + `, + ` + it('is valid', () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(promise).rejects.toBe(1); + }); + `, + ` + it('is valid', () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(promise).rejects.not.toBe(2); + }); + `, + ` + it('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(await promise).toBeGreaterThan(1); + }); + `, + ` + it('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(await promise).resolves.toBeGreaterThan(1); + }); + `, + ` + it('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(1).toBeGreaterThan(await promise); + }); + `, + ` + it('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect.this.that.is(await promise); + }); + `, + ` + it('is valid', async () => { + expect(await loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + })).toBeGreaterThan(1); + }); + `, + ` + it('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect([await promise]).toHaveLength(1); + }); + `, + ` + it('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect([,,await promise,,]).toHaveLength(1); + }); + `, + ` + it('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect([[await promise]]).toHaveLength(1); + }); + `, + ` + it('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + logValue(await promise); + }); + `, + ` + it('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return 1; + }); + + expect.assertions(await promise); + }); + `, + ` + it('is valid', async () => { + await loadNumber().then(number => { + expect(typeof number).toBe('number'); + }); + }); + `, + ` + it('it1', () => new Promise((done) => { + test() + .then(() => { + expect(someThing).toEqual(true); + done(); + }); + })); + `, + ` + it('it1', () => { + return new Promise(done => { + test().then(() => { + expect(someThing).toEqual(true); + done(); + }); + }); + }); + `, + ` + it('passes', () => { + Promise.resolve().then(() => { + grabber.grabSomething(); + }); + }); + `, + ` + it('passes', async () => { + const grabbing = Promise.resolve().then(() => { + grabber.grabSomething(); + }); + + await grabbing; + + expect(grabber.grabbedItems).toHaveLength(1); + }); + `, + ` + const myFn = () => { + Promise.resolve().then(() => { + expect(true).toBe(false); + }); + }; + `, + ` + const myFn = () => { + Promise.resolve().then(() => { + subject.invokeMethod(); + }); + }; + `, + ` + const myFn = () => { + Promise.resolve().then(() => { + expect(true).toBe(false); + }); + }; + + it('it1', () => { + return somePromise.then(() => { + expect(someThing).toEqual(true); + }); + }); + `, + ` + it('it1', () => new Promise((done) => { + test() + .finally(() => { + expect(someThing).toEqual(true); + done(); + }); + })); + `, + ` + it('it1', () => { + return somePromise.then(() => { + expect(someThing).toEqual(true); + }); + }); + `, + ` + it('it1', () => { + return somePromise.finally(() => { + expect(someThing).toEqual(true); + }); + }); + `, + ` + it('it1', function() { + return somePromise.catch(function() { + expect(someThing).toEqual(true); + }); + }); + `, + ` + xtest('it1', function() { + return somePromise.catch(function() { + expect(someThing).toEqual(true); + }); + }); + `, + ` + it('it1', function() { + return somePromise.then(function() { + doSomeThingButNotExpect(); + }); + }); + `, + ` + it('it1', function() { + return getSomeThing().getPromise().then(function() { + expect(someThing).toEqual(true); + }); + }); + `, + ` + it('it1', function() { + return Promise.resolve().then(function() { + expect(someThing).toEqual(true); + }); + }); + `, + ` + it('it1', function () { + return Promise.resolve().then(function () { + /*fulfillment*/ + expect(someThing).toEqual(true); + }, function () { + /*rejection*/ + expect(someThing).toEqual(true); + }); + }); + `, + ` + it('it1', function () { + Promise.resolve().then(/*fulfillment*/ function () { + }, undefined, /*rejection*/ function () { + expect(someThing).toEqual(true) + }) + }); + `, + ` + it('it1', function () { + return Promise.resolve().then(function () { + /*fulfillment*/ + }, function () { + /*rejection*/ + expect(someThing).toEqual(true); + }); + }); + `, + ` + it('it1', function () { + return somePromise.then() + }); + `, + ` + it('it1', async () => { + await Promise.resolve().then(function () { + expect(someThing).toEqual(true) + }); + }); + `, + ` + it('it1', async () => { + await somePromise.then(() => { + expect(someThing).toEqual(true) + }); + }); + `, + ` + it('it1', async () => { + await getSomeThing().getPromise().then(function () { + expect(someThing).toEqual(true) + }); + }); + `, + ` + it('it1', () => { + return somePromise.then(() => { + expect(someThing).toEqual(true); + }) + .then(() => { + expect(someThing).toEqual(true); + }) + }); + `, + ` + it('it1', () => { + return somePromise.then(() => { + return value; + }) + .then(value => { + expect(someThing).toEqual(value); + }) + }); + `, + ` + it('it1', () => { + return somePromise.then(() => { + expect(someThing).toEqual(true); + }) + .then(() => { + console.log('this is silly'); + }) + }); + `, + ` + it('it1', () => { + return somePromise.then(() => { + expect(someThing).toEqual(true); + }) + .catch(() => { + expect(someThing).toEqual(false); + }) + }); + `, + ` + test('later return', () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + return promise; + }); + `, + ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + await promise; + }); + `, + ` + test.only('later return', () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + return promise; + }); + `, + ` + test('that we bailout if destructuring is used', () => { + const [promise] = something().then(value => { + expect(value).toBe('red'); + }); + }); + `, + ` + test('that we bailout if destructuring is used', async () => { + const [promise] = await something().then(value => { + expect(value).toBe('red'); + }); + }); + `, + ` + test('that we bailout if destructuring is used', () => { + const [promise] = [ + something().then(value => { + expect(value).toBe('red'); + }) + ]; + }); + `, + ` + test('that we bailout if destructuring is used', () => { + const {promise} = { + promise: something().then(value => { + expect(value).toBe('red'); + }) + }; + }); + `, + ` + test('that we bailout in complex cases', () => { + promiseSomething({ + timeout: 500, + promise: something().then(value => { + expect(value).toBe('red'); + }) + }); + }); + `, + ` + it('shorthand arrow', () => + something().then(value => { + expect(() => { + value(); + }).toThrow(); + }) + ); + `, + ` + it('crawls for files based on patterns', () => { + const promise = nodeCrawl({}).then(data => { + expect(childProcess.spawn).lastCalledWith('find'); + }); + return promise; + }); + `, + ` + it('is a test', async () => { + const value = await somePromise().then(response => { + expect(response).toHaveProperty('data'); + + return response.data; + }); + + expect(value).toBe('hello world'); + }); + `, + ` + it('is a test', async () => { + return await somePromise().then(response => { + expect(response).toHaveProperty('data'); + + return response.data; + }); + }); + `, + ` + it('is a test', async () => { + return somePromise().then(response => { + expect(response).toHaveProperty('data'); + + return response.data; + }); + }); + `, + ` + it('is a test', async () => { + await somePromise().then(response => { + expect(response).toHaveProperty('data'); + + return response.data; + }); + }); + `, + ` + it( + 'test function', + () => { + return Builder + .getPromiseBuilder() + .get().build() + .then((data) => { + expect(data).toEqual('Hi'); + }); + } + ); + `, + ` + notATestFunction( + 'not a test function', + () => { + Builder + .getPromiseBuilder() + .get() + .build() + .then((data) => { + expect(data).toEqual('Hi'); + }); + } + ); + `, + ` + it('is valid', async () => { + const promiseOne = loadNumber().then(number => { + expect(typeof number).toBe('number'); + }); + const promiseTwo = loadNumber().then(number => { + expect(typeof number).toBe('number'); + }); + + await promiseTwo; + await promiseOne; + }); + `, + ` + it("it1", () => somePromise.then(() => { + expect(someThing).toEqual(true) + })) + `, + 'it("it1", () => somePromise.then(() => expect(someThing).toEqual(true)))', + ` + it('promise test with done', (done) => { + const promise = getPromise(); + promise.then(() => expect(someThing).toEqual(true)); + }); + `, + ` + it('name of done param does not matter', (nameDoesNotMatter) => { + const promise = getPromise(); + promise.then(() => expect(someThing).toEqual(true)); + }); + `, + ` + it.each([])('name of done param does not matter', (nameDoesNotMatter) => { + const promise = getPromise(); + promise.then(() => expect(someThing).toEqual(true)); + }); + `, + ` + it.each\`\`('name of done param does not matter', ({}, nameDoesNotMatter) => { + const promise = getPromise(); + promise.then(() => expect(someThing).toEqual(true)); + }); + `, + ` + test('valid-expect-in-promise', async () => { + const text = await fetch('url') + .then(res => res.text()) + .then(text => text); + + expect(text).toBe('text'); + }); + `, + ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }), x = 1; + + await somePromise; + }); + `, + ` + test('promise test', async function () { + let x = 1, somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + await somePromise; + }); + `, + ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + await somePromise; + + somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + await somePromise; + }); + `, + ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + await somePromise; + + somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + return somePromise; + }); + `, + ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + {} + + await somePromise; + }); + `, + ` + test('promise test', async function () { + const somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + { + await somePromise; + } + }); + `, + ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + { + await somePromise; + + somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + await somePromise; + } + }); + `, + ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + await somePromise; + + { + somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + await somePromise; + } + }); + `, + ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + somePromise = somePromise.then((data) => { + expect(data).toEqual('foo'); + }); + + await somePromise; + }); + `, + ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + somePromise = somePromise + .then((data) => data) + .then((data) => data) + .then((data) => { + expect(data).toEqual('foo'); + }); + + await somePromise; + }); + `, + ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + somePromise = somePromise + .then((data) => data) + .then((data) => data) + + await somePromise; + }); + `, + ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + await somePromise; + + { + somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + { + await somePromise; + } + } + }); + `, + ` + test('promise test', async function () { + const somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + await Promise.all([somePromise]); + }); + `, + ` + test('promise test', async function () { + const somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + return Promise.all([somePromise]); + }); + `, + ` + test('promise test', async function () { + const somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + return Promise.resolve(somePromise); + }); + `, + ` + test('promise test', async function () { + const somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + return Promise.reject(somePromise); + }); + `, + ` + test('promise test', async function () { + const somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + await Promise.resolve(somePromise); + }); + `, + ` + test('promise test', async function () { + const somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + await Promise.reject(somePromise); + }); + `, + ` + test('later return', async () => { + const onePromise = something().then(value => { + console.log(value); + }); + const twoPromise = something().then(value => { + expect(value).toBe('red'); + }); + + return Promise.all([onePromise, twoPromise]); + }); + `, + ` + test('later return', async () => { + const onePromise = something().then(value => { + console.log(value); + }); + const twoPromise = something().then(value => { + expect(value).toBe('red'); + }); + + return Promise.allSettled([onePromise, twoPromise]); + }); + `, + ], + invalid: [ + { + code: ` + const myFn = () => { + Promise.resolve().then(() => { + expect(true).toBe(false); + }); + }; + + it('it1', () => { + somePromise.then(() => { + expect(someThing).toEqual(true); + }); + }); + `, + errors: [ + { + column: 11, + endColumn: 14, + messageId: 'expectInFloatingPromise', + line: 9, + }, + ], + }, + { + code: ` + it('it1', () => { + somePromise.then(() => { + expect(someThing).toEqual(true); + }); + }); + `, + errors: [ + { column: 11, endColumn: 14, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('it1', () => { + somePromise.finally(() => { + expect(someThing).toEqual(true); + }); + }); + `, + errors: [ + { column: 11, endColumn: 14, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('it1', () => { + somePromise['then'](() => { + expect(someThing).toEqual(true); + }); + }); + `, + errors: [ + { column: 10, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('it1', function() { + getSomeThing().getPromise().then(function() { + expect(someThing).toEqual(true); + }); + }); + `, + errors: [ + { column: 11, endColumn: 14, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('it1', function() { + Promise.resolve().then(function() { + expect(someThing).toEqual(true); + }); + }); + `, + errors: [ + { column: 11, endColumn: 14, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('it1', function() { + somePromise.catch(function() { + expect(someThing).toEqual(true) + }) + }) + `, + errors: [ + { column: 11, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + xtest('it1', function() { + somePromise.catch(function() { + expect(someThing).toEqual(true) + }) + }) + `, + errors: [ + { column: 11, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('it1', function() { + somePromise.then(function() { + expect(someThing).toEqual(true) + }) + }) + `, + errors: [ + { column: 11, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('it1', function () { + Promise.resolve().then(/*fulfillment*/ function () { + expect(someThing).toEqual(true); + }, /*rejection*/ function () { + expect(someThing).toEqual(true); + }) + }) + `, + errors: [ + { column: 11, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('it1', function () { + Promise.resolve().then(/*fulfillment*/ function () { + }, /*rejection*/ function () { + expect(someThing).toEqual(true) + }) + }); + `, + errors: [ + { column: 11, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('test function', () => { + Builder.getPromiseBuilder() + .get() + .build() + .then(data => expect(data).toEqual('Hi')); + }); + `, + errors: [ + { column: 11, endColumn: 55, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('test function', async () => { + Builder.getPromiseBuilder() + .get() + .build() + .then(data => expect(data).toEqual('Hi')); + }); + `, + errors: [ + { column: 11, endColumn: 55, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('it1', () => { + somePromise.then(() => { + doSomeOperation(); + expect(someThing).toEqual(true); + }) + }); + `, + errors: [ + { column: 11, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('is a test', () => { + somePromise + .then(() => {}) + .then(() => expect(someThing).toEqual(value)) + }); + `, + errors: [ + { column: 11, endColumn: 58, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('is a test', () => { + somePromise + .then(() => expect(someThing).toEqual(value)) + .then(() => {}) + }); + `, + errors: [ + { column: 11, endColumn: 28, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('is a test', () => { + somePromise.then(() => { + return value; + }) + .then(value => { + expect(someThing).toEqual(value); + }) + }); + `, + errors: [ + { column: 11, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('is a test', () => { + somePromise.then(() => { + expect(someThing).toEqual(true); + }) + .then(() => { + console.log('this is silly'); + }) + }); + `, + errors: [ + { column: 11, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('is a test', () => { + somePromise.then(() => { + // return value; + }) + .then(value => { + expect(someThing).toEqual(value); + }) + }); + `, + errors: [ + { column: 11, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('is a test', () => { + somePromise.then(() => { + return value; + }) + .then(value => { + expect(someThing).toEqual(value); + }) + + return anotherPromise.then(() => expect(x).toBe(y)); + }); + `, + errors: [ + { column: 11, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('is a test', () => { + somePromise + .then(() => 1) + .then(x => x + 1) + .catch(() => -1) + .then(v => expect(v).toBe(2)); + + return anotherPromise.then(() => expect(x).toBe(y)); + }); + `, + errors: [ + { column: 11, endColumn: 43, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('is a test', () => { + somePromise + .then(() => 1) + .then(v => expect(v).toBe(2)) + .then(x => x + 1) + .catch(() => -1); + + return anotherPromise.then(() => expect(x).toBe(y)); + }); + `, + errors: [ + { column: 11, endColumn: 30, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('it1', () => { + somePromise.finally(() => { + doSomeOperation(); + expect(someThing).toEqual(true); + }) + }); + `, + errors: [ + { column: 11, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('invalid return', () => { + const promise = something().then(value => { + const foo = "foo"; + return expect(value).toBe('red'); + }); + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + fit('it1', () => { + somePromise.then(() => { + doSomeOperation(); + expect(someThing).toEqual(true); + }) + }); + `, + errors: [ + { column: 11, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it.skip('it1', () => { + somePromise.then(() => { + doSomeOperation(); + expect(someThing).toEqual(true); + }) + }); + `, + errors: [ + { column: 11, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + promise; + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + return; + + await promise; + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + return 1; + + await promise; + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + return []; + + await promise; + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + return Promise.all([anotherPromise]); + + await promise; + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + return {}; + + await promise; + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + return Promise.all([]); + + await promise; + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + await 1; + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + await []; + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + await Promise.all([anotherPromise]); + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + await {}; + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + await Promise.all([]); + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }), x = 1; + }); + `, + errors: [ + { column: 17, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('later return', async () => { + const x = 1, promise = something().then(value => { + expect(value).toBe('red'); + }); + }); + `, + errors: [ + { column: 24, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + import { test } from 'vitest'; + + test('later return', async () => { + const x = 1, promise = something().then(value => { + expect(value).toBe('red'); + }); + }); + `, + languageOptions: { + parserOptions: { sourceType: 'module' }, + }, + errors: [ + { column: 24, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + it('promise test', () => { + const somePromise = getThatPromise(); + somePromise.then((data) => { + expect(data).toEqual('foo'); + }); + expect(somePromise).toBeDefined(); + return somePromise; + }); + `, + errors: [ + { column: 11, endColumn: 14, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('promise test', function () { + let somePromise = getThatPromise(); + somePromise.then((data) => { + expect(data).toEqual('foo'); + }); + expect(somePromise).toBeDefined(); + return somePromise; + }); + `, + errors: [ + { column: 11, endColumn: 14, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + somePromise = null; + + await somePromise; + }); + `, + errors: [ + { column: 15, endColumn: 13, messageId: 'expectInFloatingPromise' }, + ], + }, + { + code: ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + await somePromise; + }); + `, + errors: [ + { + column: 15, + endColumn: 13, + line: 3, + messageId: 'expectInFloatingPromise', + }, + ], + }, + { + code: ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + ({ somePromise } = {}) + }); + `, + errors: [ + { + column: 15, + endColumn: 13, + line: 3, + messageId: 'expectInFloatingPromise', + }, + ], + }, + { + code: ` + test('promise test', async function () { + let somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + { + somePromise = getPromise().then((data) => { + expect(data).toEqual('foo'); + }); + + await somePromise; + } + }); + `, + errors: [ + { + column: 15, + endColumn: 13, + line: 3, + messageId: 'expectInFloatingPromise', + }, + ], + }, + { + code: ` + test('that we error on this destructuring', async () => { + [promise] = something().then(value => { + expect(value).toBe('red'); + }); + }); + `, + errors: [ + { + column: 11, + endColumn: 13, + line: 3, + messageId: 'expectInFloatingPromise', + }, + ], + }, + { + code: ` + test('that we error on this', () => { + const promise = something().then(value => { + expect(value).toBe('red'); + }); + + log(promise); + }); + `, + errors: [ + { + messageId: 'expectInFloatingPromise', + line: 3, + column: 17, + }, + ], + }, + { + code: ` + it('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(promise).toBeInstanceOf(Promise); + }); + `, + errors: [ + { + messageId: 'expectInFloatingPromise', + line: 3, + column: 17, + }, + ], + }, + { + code: ` + it('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(anotherPromise).resolves.toBe(1); + }); + `, + errors: [ + { + messageId: 'expectInFloatingPromise', + line: 3, + column: 17, + }, + ], + }, + { + code: ` + import { it as promiseThatThis } from 'vitest'; + + promiseThatThis('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(anotherPromise).resolves.toBe(1); + }); + `, + languageOptions: { parserOptions: { sourceType: 'module' }}, + errors: [ + { + messageId: 'expectInFloatingPromise', + line: 5, + column: 17, + }, + ], + }, + { + code: ` + promiseThatThis('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(anotherPromise).resolves.toBe(1); + }); + `, + errors: [ + { + messageId: 'expectInFloatingPromise', + line: 3, + column: 17, + }, + ], + settings: { vitest: { globalAliases: { xit: ['promiseThatThis'] } } }, + }, + ], +}); \ No newline at end of file