-
Notifications
You must be signed in to change notification settings - Fork 144
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
Move rules settings to ESLint shared config: refactor await-fire-event #265
Changes from 15 commits
619b8eb
0222ddd
7fa934d
1532804
4e0a897
4d65e1c
16253e5
66ffe9f
d78cd77
1b21106
f9b84f7
825cbe5
cc33199
ab6da9c
20174d0
2047b43
ba1628e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,16 +4,15 @@ import { | |
TSESTree, | ||
} from '@typescript-eslint/experimental-utils'; | ||
import { | ||
getImportModuleName, | ||
getAssertNodeInfo, | ||
isLiteral, | ||
getImportModuleName, | ||
ImportModuleNode, | ||
isImportDeclaration, | ||
isImportNamespaceSpecifier, | ||
isImportSpecifier, | ||
isLiteral, | ||
isMemberExpression, | ||
isProperty, | ||
isCallExpression, | ||
isObjectPattern, | ||
} from './node-utils'; | ||
import { ABSENCE_MATCHERS, ASYNC_UTILS, PRESENCE_MATCHERS } from './utils'; | ||
|
||
|
@@ -54,6 +53,7 @@ export type DetectionHelpers = { | |
isSyncQuery: (node: TSESTree.Identifier) => boolean; | ||
isAsyncQuery: (node: TSESTree.Identifier) => boolean; | ||
isAsyncUtil: (node: TSESTree.Identifier) => boolean; | ||
isFireEventMethod: (node: TSESTree.Identifier) => boolean; | ||
isPresenceAssert: (node: TSESTree.MemberExpression) => boolean; | ||
isAbsenceAssert: (node: TSESTree.MemberExpression) => boolean; | ||
canReportErrors: () => boolean; | ||
|
@@ -67,6 +67,8 @@ export type DetectionHelpers = { | |
|
||
const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$'; | ||
|
||
const FIRE_EVENT_NAME = 'fireEvent'; | ||
|
||
/** | ||
* Enhances a given rule `create` with helpers to detect Testing Library utils. | ||
*/ | ||
|
@@ -88,6 +90,15 @@ export function detectTestingLibraryUtils< | |
context.settings['testing-library/filename-pattern'] ?? | ||
DEFAULT_FILENAME_PATTERN; | ||
|
||
/** | ||
* Determines whether aggressive reporting is enabled or not. | ||
* | ||
* Aggressive reporting is considered as enabled when: | ||
* - custom module is not set (so we need to assume everything | ||
* matching TL utils is related to TL no matter where it was imported from) | ||
*/ | ||
const isAggressiveReportingEnabled = () => !customModule; | ||
|
||
// Helpers for Testing Library detection. | ||
const getTestingLibraryImportNode: DetectionHelpers['getTestingLibraryImportNode'] = () => { | ||
return importedTestingLibraryNode; | ||
|
@@ -118,7 +129,7 @@ export function detectTestingLibraryUtils< | |
* or custom module are imported. | ||
*/ | ||
const isTestingLibraryImported: DetectionHelpers['isTestingLibraryImported'] = () => { | ||
if (!customModule) { | ||
if (isAggressiveReportingEnabled()) { | ||
return true; | ||
} | ||
|
||
|
@@ -176,6 +187,58 @@ export function detectTestingLibraryUtils< | |
return ASYNC_UTILS.includes(node.name); | ||
}; | ||
|
||
/** | ||
* Determines whether a given node is fireEvent method or not | ||
*/ | ||
const isFireEventMethod: DetectionHelpers['isFireEventMethod'] = (node) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracted from |
||
const fireEventUtil = findImportedUtilSpecifier(FIRE_EVENT_NAME); | ||
let fireEventUtilName: string | undefined; | ||
|
||
if (fireEventUtil) { | ||
fireEventUtilName = ASTUtils.isIdentifier(fireEventUtil) | ||
? fireEventUtil.name | ||
: fireEventUtil.local.name; | ||
} else if (isAggressiveReportingEnabled()) { | ||
fireEventUtilName = FIRE_EVENT_NAME; | ||
} | ||
|
||
if (!fireEventUtilName) { | ||
return false; | ||
} | ||
|
||
const parentMemberExpression: | ||
| TSESTree.MemberExpression | ||
| undefined = isMemberExpression(node.parent) ? node.parent : undefined; | ||
|
||
if (!parentMemberExpression) { | ||
return false; | ||
} | ||
|
||
// make sure that given node it's not fireEvent object itself | ||
if ( | ||
[fireEventUtilName, FIRE_EVENT_NAME].includes(node.name) || | ||
(ASTUtils.isIdentifier(parentMemberExpression.object) && | ||
parentMemberExpression.object.name === node.name) | ||
) { | ||
return false; | ||
} | ||
|
||
// check fireEvent.click() usage | ||
const regularCall = | ||
ASTUtils.isIdentifier(parentMemberExpression.object) && | ||
parentMemberExpression.object.name === fireEventUtilName; | ||
|
||
// check testingLibraryUtils.fireEvent.click() usage | ||
const wildcardCall = | ||
isMemberExpression(parentMemberExpression.object) && | ||
ASTUtils.isIdentifier(parentMemberExpression.object.object) && | ||
parentMemberExpression.object.object.name === fireEventUtilName && | ||
ASTUtils.isIdentifier(parentMemberExpression.object.property) && | ||
parentMemberExpression.object.property.name === FIRE_EVENT_NAME; | ||
|
||
return regularCall || wildcardCall; | ||
}; | ||
|
||
/** | ||
* Determines whether a given MemberExpression node is a presence assert | ||
* | ||
|
@@ -215,7 +278,8 @@ export function detectTestingLibraryUtils< | |
}; | ||
|
||
/** | ||
* Gets a string and verifies if it was imported/required by our custom module node | ||
* Gets a string and verifies if it was imported/required by Testing Library | ||
* related module. | ||
*/ | ||
const findImportedUtilSpecifier: DetectionHelpers['findImportedUtilSpecifier'] = ( | ||
specifierName | ||
|
@@ -260,52 +324,24 @@ export function detectTestingLibraryUtils< | |
}; | ||
/** | ||
* Takes a MemberExpression or an Identifier and verifies if its name comes from the import in TL | ||
* @param node a MemberExpression (in "foo.property" it would be property) or an Identifier (it should be provided from a CallExpression, for example "foo()") | ||
* @param node a MemberExpression (in "foo.property" it would be property) or an Identifier | ||
*/ | ||
const isNodeComingFromTestingLibrary: DetectionHelpers['isNodeComingFromTestingLibrary'] = ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I realized this one was doing basically the same as |
||
node: TSESTree.MemberExpression | TSESTree.Identifier | ||
) => { | ||
const importOrRequire = | ||
getCustomModuleImportNode() ?? getTestingLibraryImportNode(); | ||
if (!importOrRequire) { | ||
return false; | ||
} | ||
let identifierName: string | undefined; | ||
|
||
if (ASTUtils.isIdentifier(node)) { | ||
if (isImportDeclaration(importOrRequire)) { | ||
return importOrRequire.specifiers.some( | ||
(s) => isImportSpecifier(s) && s.local.name === node.name | ||
); | ||
} else { | ||
return ( | ||
ASTUtils.isVariableDeclarator(importOrRequire.parent) && | ||
isObjectPattern(importOrRequire.parent.id) && | ||
importOrRequire.parent.id.properties.some( | ||
(p) => | ||
isProperty(p) && | ||
ASTUtils.isIdentifier(p.key) && | ||
ASTUtils.isIdentifier(p.value) && | ||
p.value.name === node.name | ||
) | ||
); | ||
} | ||
} else { | ||
if (!ASTUtils.isIdentifier(node.object)) { | ||
return false; | ||
} | ||
if (isImportDeclaration(importOrRequire)) { | ||
return ( | ||
isImportDeclaration(importOrRequire) && | ||
isImportNamespaceSpecifier(importOrRequire.specifiers[0]) && | ||
node.object.name === importOrRequire.specifiers[0].local.name | ||
); | ||
} | ||
return ( | ||
isCallExpression(importOrRequire) && | ||
ASTUtils.isVariableDeclarator(importOrRequire.parent) && | ||
ASTUtils.isIdentifier(importOrRequire.parent.id) && | ||
node.object.name === importOrRequire.parent.id.name | ||
); | ||
identifierName = node.name; | ||
} else if (ASTUtils.isIdentifier(node.object)) { | ||
identifierName = node.object.name; | ||
} | ||
|
||
if (!identifierName) { | ||
return; | ||
} | ||
|
||
return !!findImportedUtilSpecifier(identifierName); | ||
}; | ||
|
||
const helpers = { | ||
|
@@ -321,6 +357,7 @@ export function detectTestingLibraryUtils< | |
isSyncQuery, | ||
isAsyncQuery, | ||
isAsyncUtil, | ||
isFireEventMethod, | ||
isPresenceAssert, | ||
isAbsenceAssert, | ||
canReportErrors, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,6 @@ const ValidLeftHandSideExpressions = [ | |
AST_NODE_TYPES.ObjectExpression, | ||
AST_NODE_TYPES.ObjectPattern, | ||
AST_NODE_TYPES.Super, | ||
AST_NODE_TYPES.TemplateLiteral, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicated in the list |
||
AST_NODE_TYPES.ThisExpression, | ||
AST_NODE_TYPES.TSNullKeyword, | ||
AST_NODE_TYPES.TaggedTemplateExpression, | ||
|
@@ -132,7 +131,7 @@ export function findClosestCallExpressionNode( | |
return null; | ||
} | ||
|
||
return findClosestCallExpressionNode(node.parent); | ||
return findClosestCallExpressionNode(node.parent, shouldRestrictInnerScope); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug difficult to spot here! Took me a while to find it. |
||
} | ||
|
||
export function findClosestCallNode( | ||
|
@@ -232,15 +231,22 @@ export function isPromiseAllSettled(node: TSESTree.CallExpression): boolean { | |
); | ||
} | ||
|
||
/** | ||
* Determines whether a given node belongs to handled Promise.all or Promise.allSettled | ||
* array expression. | ||
*/ | ||
export function isPromisesArrayResolved(node: TSESTree.Node): boolean { | ||
const parent = node.parent; | ||
const closestCallExpression = findClosestCallExpressionNode(node, true); | ||
|
||
if (!closestCallExpression) { | ||
return false; | ||
} | ||
|
||
return ( | ||
isCallExpression(parent) && | ||
isArrayExpression(parent.parent) && | ||
isCallExpression(parent.parent.parent) && | ||
(isPromiseAll(parent.parent.parent) || | ||
isPromiseAllSettled(parent.parent.parent)) | ||
isArrayExpression(closestCallExpression.parent) && | ||
isCallExpression(closestCallExpression.parent.parent) && | ||
(isPromiseAll(closestCallExpression.parent.parent) || | ||
isPromiseAllSettled(closestCallExpression.parent.parent)) | ||
); | ||
} | ||
|
||
|
@@ -253,7 +259,7 @@ export function isPromisesArrayResolved(node: TSESTree.Node): boolean { | |
* - it belongs to the `Promise.allSettled` method | ||
* - it's chained with the `then` method | ||
* - it's returned from a function | ||
* - has `resolves` or `rejects` | ||
* - has `resolves` or `rejects` jest methods | ||
*/ | ||
export function isPromiseHandled(nodeIdentifier: TSESTree.Identifier): boolean { | ||
const closestCallExpressionNode = findClosestCallExpressionNode( | ||
|
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.
I extracted this into this simple function for adding some syntactic sugar around "aggressive reporting" concept. I'll create a separate PR soon where I'll use this in
isNodeComingFromTestingLibrary
.