-
Notifications
You must be signed in to change notification settings - Fork 238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(valid-expect-in-promise): re-implement rule #916
Conversation
2a88848
to
c795613
Compare
I've included some new tests that fail with the current version of the rule due to the poor checking of
|
daf2af8
to
5ae02ab
Compare
@SimenB I've still got to re-implement tracking assignment to variables, but might have to be tomorrow/next-week thing - the rest can be reviewed :) |
a1b103c
to
da54732
Compare
@SimenB this is ready for review - fixes a bunch of bugs and edge-cases that we weren't testing for, and has a bunch of improvements too, including:
I've tried to cover as many cases as I could, but wouldn't be surprised if there's a couple I've not thought of so let me know if you think of any that we should test :) |
2863ac2
to
16fad4e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a looooot of code here, but since the tests have mostly just been expanded this seems safe enough 🙂 Have you tried to benchmark this rule? It has quite a bit of code enow
!isPromiseChainCall(node) || | ||
(node.parent && node.parent.type === AST_NODE_TYPES.AwaitExpression) | ||
) { | ||
CallExpression(node: TSESTree.CallExpression) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why the explicit type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No strong reason - it is symmetrical with the CallExpression:exit
(which requires it otherwise its an any
) and ensures WebStorm isn't dumb (for some reason a few of its features sometimes don't resolve types as well if you omit this 🤷)
Yup, ran it against the jest codebase - it's actually faster by a few seconds (at least for me):
(ran it a couple of times - those numbers are about +/- 1 second, with the gap of a few seconds being constant) It also doesn't flag a false positive 🎉 |
Method call chains are represented in AST in a nested fashion, meaning it's not enough to just separately track if we're both in a promise chain call and then if we've found an `expect` call, because when we exit another CallExpression in the same chain it'll look like that has an `expect` call. Instead, we need to track our depth as we enter CallExpression nodes so that when we exit those nodes we can check if we encountered an `expect` call at that same depth.
We only call it in one place, and so it lets us also remove some minor types that can be otherwise inferred
8885551
to
72bc5d6
Compare
src/rules/valid-expect-in-promise.ts
Outdated
) { | ||
const nodeName = getNodeName(node.argument); | ||
|
||
if (nodeName === 'Promise.all') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SimenB I'm wondering if we should flag Promise.all
, since it rejects as soon as any of the promises reject, so other promises with expect
s might not all resolve - but that might not be a big deal since you're still returning a promise that failed? 🤔
We should also be checking for allSettled
, but (at least for now) not any
or race
I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems reasonable to me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following up on this: I decided to leave Promise.all
in there for now because allSettled
is Node 12+ and we've only just stopped supporting Node 10
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
merge at will, lgtm 🙂
I realised we don't support direct promises in array expressions, e.g.
Annoyingly we can get really close without a lot of extra work, except that it then starts falling to bits with trying to bail out for cases like:
For now I'm just going to move on because I want to ship this + the new major. Here's the patch if anyone wants to pick it up laterIndex: src/rules/__tests__/valid-expect-in-promise.test.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/rules/__tests__/valid-expect-in-promise.test.ts b/src/rules/__tests__/valid-expect-in-promise.test.ts
--- a/src/rules/__tests__/valid-expect-in-promise.test.ts (revision c664be1eab66e69d8eed78bcb1404a98dd2ca57e)
+++ b/src/rules/__tests__/valid-expect-in-promise.test.ts (date 1633806713585)
@@ -665,6 +665,20 @@
return Promise.allSettled([onePromise, twoPromise]);
});
`,
+ {
+ code: dedent`
+ it('promises multiple people', () => {
+ return Promise.allSettled([
+ api.getPersonByName('bob').then(person => {
+ expect(person).toHaveProperty('name', 'Bob');
+ }),
+ api.getPersonByName('alice').then(person => {
+ expect(person).toHaveProperty('name', 'Alice');
+ })
+ ]);
+ });
+ `,
+ },
],
invalid: [
{
@@ -1353,6 +1367,161 @@
endColumn: 5,
line: 2,
messageId: 'expectInFloatingPromise',
+ },
+ ],
+ },
+ {
+ code: dedent`
+ it('races multiple people', () => {
+ return Promise.race([
+ api.getPersonByName('bob').then(person => {
+ expect(person).toHaveProperty('name', 'Bob');
+ }),
+ api.getPersonByName('alice').then(person => {
+ expect(person).toHaveProperty('name', 'Alice');
+ })
+ ]);
+ });
+ `,
+ errors: [
+ {
+ line: 2,
+ column: 23,
+ messageId: 'expectInFloatingPromise',
+ endLine: 9,
+ endColumn: 4,
+ },
+ {
+ line: 2,
+ column: 23,
+ messageId: 'expectInFloatingPromise',
+ endLine: 9,
+ endColumn: 4,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ it('promises any person', async () => {
+ await Promise.any([
+ api.getPersonByName('bob').then(person => {
+ expect(person).toHaveProperty('name', 'Bob');
+ }),
+ api.getPersonByName('alice').then(person => {
+ expect(person).toHaveProperty('name', 'Alice');
+ })
+ ]);
+ });
+ `,
+ errors: [
+ {
+ line: 2,
+ column: 21,
+ messageId: 'expectInFloatingPromise',
+ endLine: 9,
+ endColumn: 4,
+ },
+ {
+ line: 2,
+ column: 21,
+ messageId: 'expectInFloatingPromise',
+ endLine: 9,
+ endColumn: 4,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ it('promises multiple people', () => {
+ Promise.allSettled([
+ api.getPersonByName('bob').then(person => {
+ expect(person).toHaveProperty('name', 'Bob');
+ }),
+ api.getPersonByName('alice').then(person => {
+ expect(person).toHaveProperty('name', 'Alice');
+ })
+ ]);
+ });
+ `,
+ errors: [
+ {
+ line: 2,
+ column: 22,
+ messageId: 'expectInFloatingPromise',
+ endLine: 9,
+ endColumn: 4,
+ },
+ {
+ line: 2,
+ column: 22,
+ messageId: 'expectInFloatingPromise',
+ endLine: 9,
+ endColumn: 4,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ it('promises multiple people', async () => {
+ const promises = [
+ api.getPersonByName('bob').then(person => {
+ expect(person).toHaveProperty('name', 'Bob');
+ }),
+ api.getPersonByName('alice').then(person => {
+ expect(person).toHaveProperty('name', 'Alice');
+ })
+ ];
+
+ await promises;
+ });
+ `,
+ errors: [
+ {
+ line: 2,
+ column: 20,
+ messageId: 'expectInFloatingPromise',
+ endLine: 9,
+ endColumn: 4,
+ },
+ {
+ line: 2,
+ column: 20,
+ messageId: 'expectInFloatingPromise',
+ endLine: 9,
+ endColumn: 4,
+ },
+ ],
+ },
+ // we currently cannot track assignment of an array of promises
+ {
+ code: dedent`
+ it('promises multiple people', async () => {
+ const promises = [
+ api.getPersonByName('bob').then(person => {
+ expect(person).toHaveProperty('name', 'Bob');
+ }),
+ api.getPersonByName('alice').then(person => {
+ expect(person).toHaveProperty('name', 'Alice');
+ })
+ ];
+
+ return Promise.allSettled([]);
+ });
+ `,
+ errors: [
+ {
+ line: 2,
+ column: 20,
+ messageId: 'expectInFloatingPromise',
+ endLine: 9,
+ endColumn: 4,
+ },
+ {
+ line: 2,
+ column: 20,
+ messageId: 'expectInFloatingPromise',
+ endLine: 9,
+ endColumn: 4,
},
],
},
Index: src/rules/valid-expect-in-promise.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/rules/valid-expect-in-promise.ts b/src/rules/valid-expect-in-promise.ts
--- a/src/rules/valid-expect-in-promise.ts (revision c664be1eab66e69d8eed78bcb1404a98dd2ca57e)
+++ b/src/rules/valid-expect-in-promise.ts (date 1633806935035)
@@ -249,6 +249,48 @@
return isValueAwaitedOrReturned(variable.id, body);
};
+/**
+ * Checks if the given Array Expression is `await`ed or `return`ed by being
+ * passed as an argument to a `Promise.all` or `Promise.allSettled` call
+ */
+const arePromisesAwaitedOrReturned = (
+ node: TSESTree.ArrayExpression,
+): boolean => {
+ if (
+ node.parent?.type !== AST_NODE_TYPES.CallExpression ||
+ node.parent.arguments.length === 0
+ ) {
+ return false;
+ }
+
+ const nodeName = getNodeName(node.parent);
+
+ if (['Promise.all', 'Promise.allSettled'].includes(nodeName as string)) {
+ const [firstArg] = node.parent.arguments;
+
+ if (firstArg !== node) {
+ return false;
+ }
+
+ if (!node.parent.parent) {
+ return false;
+ }
+
+ if (node.parent.parent.type === AST_NODE_TYPES.ReturnStatement) {
+ return true;
+ }
+
+ if (
+ node.parent.parent.type === AST_NODE_TYPES.ExpressionStatement &&
+ node.parent.parent.parent?.type === AST_NODE_TYPES.AwaitExpression
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+};
+
export default createRule({
name: __filename,
meta: {
@@ -356,6 +398,14 @@
) {
return;
}
+
+ break;
+ }
+
+ case AST_NODE_TYPES.ArrayExpression: {
+ if (arePromisesAwaitedOrReturned(parent)) {
+ return;
+ }
break;
} (seriously, I'm annoyed at how nicely this is working vs how ugly the edge cases are) |
# [24.6.0](v24.5.2...v24.6.0) (2021-10-09) ### Features * **valid-expect-in-promise:** re-implement rule ([#916](#916)) ([7a49c58](7a49c58))
🎉 This PR is included in version 24.6.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
# [25.0.0-next.6](v25.0.0-next.5...v25.0.0-next.6) (2021-10-10) ### Bug Fixes * **lowercase-name:** consider skip and only prefixes for ignores ([#923](#923)) ([8716c24](8716c24)) * **prefer-to-be:** don't consider RegExp literals as `toBe`-able ([#922](#922)) ([99b6d42](99b6d42)) ### Features * create `require-hook` rule ([#929](#929)) ([6204b31](6204b31)) * deprecate `prefer-to-be-null` rule ([4db9161](4db9161)) * deprecate `prefer-to-be-undefined` rule ([fa08f09](fa08f09)) * **valid-expect-in-promise:** re-implement rule ([#916](#916)) ([7a49c58](7a49c58))
🎉 This PR is included in version 25.0.0-next.6 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This is an attempt at re-implementing the
valid-expect-in-promise
rule to hopefully make it less buggy - primarily this is focused around having it useisExpectCall
to more accurately determine if a node is what we conventionally consider a usage of the Jestexpect
method, since the current method being used is far too lax, checking only if the given node is a CallExpression and then if its parent is a MemberExpression (e.g. it doesn't even attempt to check if it's namedexpect
).While the current implementation isn't super bad, it sort of approaches the problem from the wrong end of the stick (imo at least) by searching for promise method calls and then both moving up the AST to find if we're in a test call and down to find if there's an expect in the body of the promise method (using the aforementioned too lax check to do so).
I've gone somewhat the other way using a more standard approach that's common in our other rules: looking for the
expect
call, and marking info as we enter and exit CallExpressions (leveraging the way the AST is navigated) on if we're in something that looks like a call to a promise method.While this is technically bug fixing, I've committed it as a feature for now because it's a significant re-write that I think will fix a number of bugs (including some we don't even know about).
Fixes #405
Fixes #219
Closes #406