UNPKG

5.71 kBJavaScriptView Raw
1'use strict';
2
3const {visitIf} = require('enhance-visitors');
4const createAvaRule = require('../create-ava-rule');
5const util = require('../util');
6
7const create = context => {
8 const ava = createAvaRule();
9
10 const booleanTests = new Set([
11 'true',
12 'false',
13 'truthy',
14 'falsy',
15 ]);
16
17 const equalityTests = new Set([
18 'is',
19 'deepEqual',
20 ]);
21
22 // Find the latest reference to the given identifier's name.
23 const findReference = node => {
24 const {sourceCode} = context;
25 const reference = sourceCode.getScope(node).references.find(reference => reference.identifier.name === node.name);
26
27 if (reference?.resolved) {
28 const definitions = reference.resolved.defs;
29
30 if (definitions.length === 0) {
31 return;
32 }
33
34 return definitions.at(-1).node;
35 }
36 };
37
38 /*
39 Recursively find the "origin" node of the given node.
40
41 Note: `sourceCode.getScope()` doesn't contain information about the outer scope so in most cases this function will only find the reference directly above the current scope. So the following code will only find the reference in this order: y -> x, and it will have no knowledge of the number `0`. (assuming we run this function on the identifier `y`)
42
43 ```
44 const test = require('ava');
45
46 let x = 0;
47 let y = x;
48
49 test(t => {
50 t.is(y, 0);
51 });
52 ```
53 */
54 const findRootReference = node => {
55 if (!node) {
56 return;
57 }
58
59 if (node.type === 'Identifier') {
60 const reference = findReference(node);
61
62 if (reference?.init) {
63 return findRootReference(reference.init);
64 }
65
66 return node;
67 }
68
69 if (node.type === 'CallExpression' || node.type === 'NewExpression') {
70 return findRootReference(node.callee);
71 }
72
73 if (node.type === 'MemberExpression') {
74 return findRootReference(node.object);
75 }
76
77 return node;
78 };
79
80 /*
81 Determine if the given node is a regex expression.
82
83 There are two ways to create regex expressions in JavaScript: Regex literal and `RegExp` class.
84 1. Regex literal can be easily looked up using the `.regex` property on the node.
85 2. `RegExp` class can't be looked up so the function just checks for the name `RegExp`.
86 */
87 const isRegExp = lookup => {
88 if (!lookup) {
89 return false;
90 }
91
92 if (lookup.regex) {
93 return true;
94 }
95
96 // Look up references in case it's a variable or RegExp declaration.
97 const reference = findRootReference(lookup);
98
99 if (!reference) {
100 return false;
101 }
102
103 return reference.regex ?? reference.name === 'RegExp';
104 };
105
106 const booleanHandler = node => {
107 const firstArgument = node.arguments[0];
108
109 if (!firstArgument) {
110 return;
111 }
112
113 const isFunctionCall = firstArgument.type === 'CallExpression';
114 if (!isFunctionCall || !firstArgument.callee.property) {
115 return;
116 }
117
118 const {name} = firstArgument.callee.property;
119 let lookup = {};
120 let variable = {};
121
122 if (name === 'test') {
123 // Represent: `lookup.test(variable)`
124 lookup = firstArgument.callee.object;
125 variable = firstArgument.arguments[0];
126 } else if (['search', 'match'].includes(name)) {
127 // Represent: `variable.match(lookup)`
128 lookup = firstArgument.arguments[0];
129 variable = firstArgument.callee.object;
130 }
131
132 if (!isRegExp(lookup)) {
133 return;
134 }
135
136 const assertion = ['true', 'truthy'].includes(node.callee.property.name) ? 'regex' : 'notRegex';
137
138 const fix = fixer => {
139 const source = context.getSourceCode();
140 return [
141 fixer.replaceText(node.callee.property, assertion),
142 fixer.replaceText(firstArgument, `${source.getText(variable)}, ${source.getText(lookup)}`),
143 ];
144 };
145
146 context.report({
147 node,
148 message: `Prefer using the \`t.${assertion}()\` assertion.`,
149 fix,
150 });
151 };
152
153 const equalityHandler = node => {
154 const [firstArgument, secondArgument] = node.arguments;
155
156 const firstArgumentIsRegex = isRegExp(firstArgument);
157 const secondArgumentIsRegex = isRegExp(secondArgument);
158
159 // If both are regex, or neither are, the expression is ok.
160 if (firstArgumentIsRegex === secondArgumentIsRegex) {
161 return;
162 }
163
164 const matchee = secondArgumentIsRegex ? firstArgument : secondArgument;
165
166 if (!matchee) {
167 return;
168 }
169
170 const regex = secondArgumentIsRegex ? secondArgument : firstArgument;
171
172 const booleanFixer = assertion => fixer => {
173 const source = context.getSourceCode();
174 return [
175 fixer.replaceText(node.callee.property, assertion),
176 fixer.replaceText(firstArgument, `${source.getText(regex.arguments[0])}`),
177 fixer.replaceText(secondArgument, `${source.getText(regex.callee.object)}`),
178 ];
179 };
180
181 // Only fix a statically verifiable equality.
182 if (regex && matchee.type === 'Literal') {
183 let assertion;
184
185 if (matchee.raw === 'true') {
186 assertion = 'regex';
187 } else if (matchee.raw === 'false') {
188 assertion = 'notRegex';
189 } else {
190 return;
191 }
192
193 context.report({
194 node,
195 message: `Prefer using the \`t.${assertion}()\` assertion.`,
196 fix: booleanFixer(assertion),
197 });
198 }
199 };
200
201 return ava.merge({
202 CallExpression: visitIf([
203 ava.isInTestFile,
204 ava.isInTestNode,
205 ],
206 )(node => {
207 if (!node?.callee?.property) {
208 return;
209 }
210
211 const isAssertion = node.callee.type === 'MemberExpression'
212 && util.getNameOfRootNodeObject(node.callee) === 't';
213
214 const isBooleanAssertion = isAssertion
215 && booleanTests.has(node.callee.property.name);
216
217 const isEqualityAssertion = isAssertion
218 && equalityTests.has(node.callee.property.name);
219
220 if (isBooleanAssertion) {
221 booleanHandler(node);
222 } else if (isEqualityAssertion) {
223 equalityHandler(node);
224 }
225 }),
226 });
227};
228
229module.exports = {
230 create,
231 meta: {
232 type: 'suggestion',
233 docs: {
234 description: 'Prefer using `t.regex()` to test regular expressions.',
235 url: util.getDocsUrl(__filename),
236 },
237 fixable: 'code',
238 schema: [],
239 },
240};