Skip to content

Commit 07a7e05

Browse files
authored
refactor(node-utils): use the AST_NODE_TYPES enum in type checks (#205)
Closes #203 * refactor(node-utils): use the `AST_NODE_TYPES` enum in type checks Signed-off-by: Josh Kelly <[email protected]> * chore(formatting): prettier auto fixes Signed-off-by: Josh Kelly <[email protected]> * chore(eslint): fix outstanding lint errors Signed-off-by: Josh Kelly <[email protected]>
1 parent c7097d8 commit 07a7e05

File tree

4 files changed

+143
-85
lines changed

4 files changed

+143
-85
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
[![Tweet][tweet-badge]][tweet-url]
2424

2525
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
26+
2627
[![All Contributors](https://img.shields.io/badge/all_contributors-28-orange.svg?style=flat-square)](#contributors-)
28+
2729
<!-- ALL-CONTRIBUTORS-BADGE:END -->
2830

2931
## Installation
@@ -216,6 +218,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
216218

217219
<!-- markdownlint-enable -->
218220
<!-- prettier-ignore-end -->
221+
219222
<!-- ALL-CONTRIBUTORS-LIST:END -->
220223

221224
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

docs/rules/prefer-wait-for.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const foo = async () => {
2323
await waitForElement(() => {});
2424
await waitForDomChange();
2525
await waitForDomChange(mutationObserverOptions);
26-
await waitForDomChange({ timeout: 100});
26+
await waitForDomChange({ timeout: 100 });
2727
};
2828
```
2929

lib/node-utils.ts

+14-14
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,69 @@
1-
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/experimental-utils';
1+
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/experimental-utils';
22

33
export function isCallExpression(
44
node: TSESTree.Node
55
): node is TSESTree.CallExpression {
6-
return node && node.type === 'CallExpression';
6+
return node && node.type === AST_NODE_TYPES.CallExpression;
77
}
88

99
export function isAwaitExpression(
1010
node: TSESTree.Node
1111
): node is TSESTree.AwaitExpression {
12-
return node && node.type === 'AwaitExpression';
12+
return node && node.type === AST_NODE_TYPES.AwaitExpression;
1313
}
1414

1515
export function isIdentifier(node: TSESTree.Node): node is TSESTree.Identifier {
16-
return node && node.type === 'Identifier';
16+
return node && node.type === AST_NODE_TYPES.Identifier;
1717
}
1818

1919
export function isMemberExpression(
2020
node: TSESTree.Node
2121
): node is TSESTree.MemberExpression {
22-
return node && node.type === 'MemberExpression';
22+
return node && node.type === AST_NODE_TYPES.MemberExpression;
2323
}
2424

2525
export function isLiteral(node: TSESTree.Node): node is TSESTree.Literal {
26-
return node && node.type === 'Literal';
26+
return node && node.type === AST_NODE_TYPES.Literal;
2727
}
2828

2929
export function isImportSpecifier(
3030
node: TSESTree.Node
3131
): node is TSESTree.ImportSpecifier {
32-
return node && node.type === 'ImportSpecifier';
32+
return node && node.type === AST_NODE_TYPES.ImportSpecifier;
3333
}
3434

3535
export function isImportDefaultSpecifier(
3636
node: TSESTree.Node
3737
): node is TSESTree.ImportDefaultSpecifier {
38-
return node && node.type === 'ImportDefaultSpecifier';
38+
return node && node.type === AST_NODE_TYPES.ImportDefaultSpecifier;
3939
}
4040

4141
export function isBlockStatement(
4242
node: TSESTree.Node
4343
): node is TSESTree.BlockStatement {
44-
return node && node.type === 'BlockStatement';
44+
return node && node.type === AST_NODE_TYPES.BlockStatement;
4545
}
4646

4747
export function isVariableDeclarator(
4848
node: TSESTree.Node
4949
): node is TSESTree.VariableDeclarator {
50-
return node && node.type === 'VariableDeclarator';
50+
return node && node.type === AST_NODE_TYPES.VariableDeclarator;
5151
}
5252

5353
export function isObjectPattern(
5454
node: TSESTree.Node
5555
): node is TSESTree.ObjectPattern {
56-
return node && node.type === 'ObjectPattern';
56+
return node && node.type === AST_NODE_TYPES.ObjectPattern;
5757
}
5858

5959
export function isProperty(node: TSESTree.Node): node is TSESTree.Property {
60-
return node && node.type === 'Property';
60+
return node && node.type === AST_NODE_TYPES.Property;
6161
}
6262

6363
export function isJSXAttribute(
6464
node: TSESTree.Node
6565
): node is TSESTree.JSXAttribute {
66-
return node && node.type === 'JSXAttribute';
66+
return node && node.type === AST_NODE_TYPES.JSXAttribute;
6767
}
6868

6969
export function findClosestCallExpressionNode(
@@ -104,7 +104,7 @@ export function hasThenProperty(node: TSESTree.Node) {
104104
}
105105

106106
export function isArrowFunctionExpression(node: TSESTree.Node): node is TSESTree.ArrowFunctionExpression {
107-
return node && node.type === 'ArrowFunctionExpression'
107+
return node && node.type === AST_NODE_TYPES.ArrowFunctionExpression
108108
}
109109

110110
export function isObjectExpression(node: TSESTree.Expression): node is TSESTree.ObjectExpression {

lib/rules/prefer-find-by.ts

+125-70
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
2-
import { ReportFixFunction, Scope, RuleFix } from '@typescript-eslint/experimental-utils/dist/ts-eslint'
32
import {
4-
isIdentifier,
3+
ReportFixFunction,
4+
RuleFix,
5+
Scope,
6+
} from '@typescript-eslint/experimental-utils/dist/ts-eslint';
7+
import {
8+
isArrowFunctionExpression,
59
isCallExpression,
10+
isIdentifier,
611
isMemberExpression,
7-
isArrowFunctionExpression,
812
isObjectPattern,
913
isProperty,
1014
} from '../node-utils';
@@ -15,138 +19,189 @@ export const RULE_NAME = 'prefer-find-by';
1519
type Options = [];
1620
export type MessageIds = 'preferFindBy';
1721

18-
export const WAIT_METHODS = ['waitFor', 'waitForElement', 'wait']
22+
export const WAIT_METHODS = ['waitFor', 'waitForElement', 'wait'];
23+
24+
export function getFindByQueryVariant(queryMethod: string) {
25+
return queryMethod.includes('All') ? 'findAllBy' : 'findBy';
26+
}
27+
28+
function findRenderDefinitionDeclaration(
29+
scope: Scope.Scope | null,
30+
query: string
31+
): TSESTree.Identifier | null {
32+
if (!scope) {
33+
return null;
34+
}
35+
36+
const variable = scope.variables.find(
37+
(v: Scope.Variable) => v.name === query
38+
);
39+
40+
if (variable) {
41+
const def = variable.defs.find(({ name }) => name.name === query);
42+
return def.name;
43+
}
44+
45+
return findRenderDefinitionDeclaration(scope.upper, query);
46+
}
1947

2048
export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
2149
name: RULE_NAME,
2250
meta: {
2351
type: 'suggestion',
2452
docs: {
25-
description: 'Suggest using find* instead of waitFor to wait for elements',
53+
description:
54+
'Suggest using find* instead of waitFor to wait for elements',
2655
category: 'Best Practices',
2756
recommended: 'warn',
2857
},
2958
messages: {
30-
preferFindBy: 'Prefer {{queryVariant}}{{queryMethod}} method over using await {{fullQuery}}'
59+
preferFindBy:
60+
'Prefer {{queryVariant}}{{queryMethod}} method over using await {{fullQuery}}',
3161
},
3262
fixable: 'code',
33-
schema: []
63+
schema: [],
3464
},
3565
defaultOptions: [],
3666

3767
create(context) {
3868
const sourceCode = context.getSourceCode();
3969

40-
41-
4270
/**
4371
* Reports the invalid usage of wait* plus getBy/QueryBy methods and automatically fixes the scenario
4472
* @param {TSESTree.CallExpression} node - The CallExpresion node that contains the wait* method
4573
* @param {'findBy' | 'findAllBy'} replacementParams.queryVariant - The variant method used to query: findBy/findByAll.
4674
* @param {string} replacementParams.queryMethod - Suffix string to build the query method (the query-part that comes after the "By"): LabelText, Placeholder, Text, Role, Title, etc.
4775
* @param {ReportFixFunction} replacementParams.fix - Function that applies the fix to correct the code
4876
*/
49-
function reportInvalidUsage(node: TSESTree.CallExpression, { queryVariant, queryMethod, fix }: { queryVariant: 'findBy' | 'findAllBy', queryMethod: string, fix: ReportFixFunction }) {
50-
77+
function reportInvalidUsage(
78+
node: TSESTree.CallExpression,
79+
{
80+
queryVariant,
81+
queryMethod,
82+
fix,
83+
}: {
84+
queryVariant: 'findBy' | 'findAllBy';
85+
queryMethod: string;
86+
fix: ReportFixFunction;
87+
}
88+
) {
5189
context.report({
5290
node,
53-
messageId: "preferFindBy",
54-
data: { queryVariant, queryMethod, fullQuery: sourceCode.getText(node) },
91+
messageId: 'preferFindBy',
92+
data: {
93+
queryVariant,
94+
queryMethod,
95+
fullQuery: sourceCode.getText(node),
96+
},
5597
fix,
5698
});
5799
}
58100

59101
return {
60102
'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) {
61-
if (!isIdentifier(node.callee) || !WAIT_METHODS.includes(node.callee.name)) {
62-
return
103+
if (
104+
!isIdentifier(node.callee) ||
105+
!WAIT_METHODS.includes(node.callee.name)
106+
) {
107+
return;
63108
}
64109
// ensure the only argument is an arrow function expression - if the arrow function is a block
65110
// we skip it
66-
const argument = node.arguments[0]
111+
const argument = node.arguments[0];
67112
if (!isArrowFunctionExpression(argument)) {
68-
return
113+
return;
69114
}
70115
if (!isCallExpression(argument.body)) {
71-
return
116+
return;
72117
}
73118
// ensure here it's one of the sync methods that we are calling
74-
if (isMemberExpression(argument.body.callee) && isIdentifier(argument.body.callee.property) && isIdentifier(argument.body.callee.object) && SYNC_QUERIES_COMBINATIONS.includes(argument.body.callee.property.name)) {
119+
if (
120+
isMemberExpression(argument.body.callee) &&
121+
isIdentifier(argument.body.callee.property) &&
122+
isIdentifier(argument.body.callee.object) &&
123+
SYNC_QUERIES_COMBINATIONS.includes(argument.body.callee.property.name)
124+
) {
75125
// shape of () => screen.getByText
76-
const fullQueryMethod = argument.body.callee.property.name
77-
const caller = argument.body.callee.object.name
78-
const queryVariant = getFindByQueryVariant(fullQueryMethod)
79-
const callArguments = argument.body.arguments
80-
const queryMethod = fullQueryMethod.split('By')[1]
126+
const fullQueryMethod = argument.body.callee.property.name;
127+
const caller = argument.body.callee.object.name;
128+
const queryVariant = getFindByQueryVariant(fullQueryMethod);
129+
const callArguments = argument.body.arguments;
130+
const queryMethod = fullQueryMethod.split('By')[1];
81131

82132
reportInvalidUsage(node, {
83133
queryMethod,
84134
queryVariant,
85135
fix(fixer) {
86-
const newCode = `${caller}.${queryVariant}${queryMethod}(${callArguments.map((node) => sourceCode.getText(node)).join(', ')})`
87-
return fixer.replaceText(node, newCode)
88-
}
89-
})
90-
return
136+
const newCode = `${caller}.${queryVariant}${queryMethod}(${callArguments
137+
.map(node => sourceCode.getText(node))
138+
.join(', ')})`;
139+
return fixer.replaceText(node, newCode);
140+
},
141+
});
142+
return;
91143
}
92-
if (isIdentifier(argument.body.callee) && SYNC_QUERIES_COMBINATIONS.includes(argument.body.callee.name)) {
144+
if (
145+
isIdentifier(argument.body.callee) &&
146+
SYNC_QUERIES_COMBINATIONS.includes(argument.body.callee.name)
147+
) {
93148
// shape of () => getByText
94-
const fullQueryMethod = argument.body.callee.name
95-
const queryMethod = fullQueryMethod.split('By')[1]
96-
const queryVariant = getFindByQueryVariant(fullQueryMethod)
97-
const callArguments = argument.body.arguments
149+
const fullQueryMethod = argument.body.callee.name;
150+
const queryMethod = fullQueryMethod.split('By')[1];
151+
const queryVariant = getFindByQueryVariant(fullQueryMethod);
152+
const callArguments = argument.body.arguments;
98153

99154
reportInvalidUsage(node, {
100155
queryMethod,
101156
queryVariant,
102157
fix(fixer) {
103-
const findByMethod = `${queryVariant}${queryMethod}`
104-
const allFixes: RuleFix[] = []
158+
const findByMethod = `${queryVariant}${queryMethod}`;
159+
const allFixes: RuleFix[] = [];
105160
// this updates waitFor with findBy*
106-
const newCode = `${findByMethod}(${callArguments.map((node) => sourceCode.getText(node)).join(', ')})`
107-
allFixes.push(fixer.replaceText(node, newCode))
161+
const newCode = `${findByMethod}(${callArguments
162+
.map(node => sourceCode.getText(node))
163+
.join(', ')})`;
164+
allFixes.push(fixer.replaceText(node, newCode));
108165

109166
// this adds the findBy* declaration - adding it to the list of destructured variables { findBy* } = render()
110-
const definition = findRenderDefinitionDeclaration(context.getScope(), fullQueryMethod)
167+
const definition = findRenderDefinitionDeclaration(
168+
context.getScope(),
169+
fullQueryMethod
170+
);
111171
// I think it should always find it, otherwise code should not be valid (it'd be using undeclared variables)
112172
if (!definition) {
113-
return allFixes
173+
return allFixes;
114174
}
115175
// check the declaration is part of a destructuring
116176
if (isObjectPattern(definition.parent.parent)) {
117-
const allVariableDeclarations = definition.parent.parent
177+
const allVariableDeclarations = definition.parent.parent;
118178
// verify if the findBy* method was already declared
119-
if (allVariableDeclarations.properties.some((p) => isProperty(p) && isIdentifier(p.key) && p.key.name === findByMethod)) {
120-
return allFixes
179+
if (
180+
allVariableDeclarations.properties.some(
181+
p =>
182+
isProperty(p) &&
183+
isIdentifier(p.key) &&
184+
p.key.name === findByMethod
185+
)
186+
) {
187+
return allFixes;
121188
}
122189
// the last character of a destructuring is always a "}", so we should replace it with the findBy* declaration
123-
const textDestructuring = sourceCode.getText(allVariableDeclarations)
124-
const text = textDestructuring.substring(0, textDestructuring.length - 2) + `, ${findByMethod} }`
125-
allFixes.push(fixer.replaceText(allVariableDeclarations, text))
190+
const textDestructuring = sourceCode.getText(
191+
allVariableDeclarations
192+
);
193+
const text =
194+
textDestructuring.substring(0, textDestructuring.length - 2) +
195+
`, ${findByMethod} }`;
196+
allFixes.push(fixer.replaceText(allVariableDeclarations, text));
126197
}
127198

128-
return allFixes
129-
}
130-
})
131-
return
199+
return allFixes;
200+
},
201+
});
202+
return;
132203
}
133-
}
134-
}
135-
}
136-
})
137-
138-
export function getFindByQueryVariant(queryMethod: string) {
139-
return queryMethod.includes('All') ? 'findAllBy' : 'findBy'
140-
}
141-
142-
function findRenderDefinitionDeclaration(scope: Scope.Scope | null, query: string): TSESTree.Identifier | null {
143-
if (!scope) {
144-
return null
145-
}
146-
let variable = scope.variables.find((v: Scope.Variable) => v.name === query)
147-
if (variable) {
148-
const def = variable.defs.find(({ name }) => name.name === query)
149-
return def.name
150-
}
151-
return findRenderDefinitionDeclaration(scope.upper, query)
152-
}
204+
},
205+
};
206+
},
207+
});

0 commit comments

Comments
 (0)