UNPKG

8.93 kBJavaScriptView Raw
1'use strict';
2
3const {visitIf} = require('enhance-visitors');
4const {getStaticValue, isOpeningParenToken, isCommaToken, findVariable} = require('eslint-utils');
5const util = require('../util');
6const createAvaRule = require('../create-ava-rule');
7
8const expectedNbArguments = {
9 assert: {
10 min: 1,
11 max: 2,
12 },
13 deepEqual: {
14 min: 2,
15 max: 3,
16 },
17 fail: {
18 min: 0,
19 max: 1,
20 },
21 false: {
22 min: 1,
23 max: 2,
24 },
25 falsy: {
26 min: 1,
27 max: 2,
28 },
29 ifError: {
30 min: 1,
31 max: 2,
32 },
33 is: {
34 min: 2,
35 max: 3,
36 },
37 like: {
38 min: 2,
39 max: 3,
40 },
41 not: {
42 min: 2,
43 max: 3,
44 },
45 notDeepEqual: {
46 min: 2,
47 max: 3,
48 },
49 notThrows: {
50 min: 1,
51 max: 2,
52 },
53 notThrowsAsync: {
54 min: 1,
55 max: 2,
56 },
57 pass: {
58 min: 0,
59 max: 1,
60 },
61 plan: {
62 min: 1,
63 max: 1,
64 },
65 regex: {
66 min: 2,
67 max: 3,
68 },
69 notRegex: {
70 min: 2,
71 max: 3,
72 },
73 snapshot: {
74 min: 1,
75 max: 2,
76 },
77 teardown: {
78 min: 1,
79 max: 1,
80 },
81 throws: {
82 min: 1,
83 max: 3,
84 },
85 throwsAsync: {
86 min: 1,
87 max: 3,
88 },
89 true: {
90 min: 1,
91 max: 2,
92 },
93 truthy: {
94 min: 1,
95 max: 2,
96 },
97 timeout: {
98 min: 1,
99 max: 2,
100 },
101};
102
103const actualExpectedAssertions = new Set([
104 'deepEqual',
105 'is',
106 'like',
107 'not',
108 'notDeepEqual',
109 'throws',
110 'throwsAsync',
111]);
112
113const relationalActualExpectedAssertions = new Set([
114 'assert',
115 'truthy',
116 'falsy',
117 'true',
118 'false',
119]);
120
121const comparisonOperators = new Map([
122 ['>', '<'],
123 ['>=', '<='],
124 ['==', '=='],
125 ['===', '==='],
126 ['!=', '!='],
127 ['!==', '!=='],
128 ['<=', '>='],
129 ['<', '>'],
130]);
131
132const flipOperator = operator => comparisonOperators.get(operator);
133
134function isStatic(node) {
135 const staticValue = getStaticValue(node);
136 return staticValue !== null && typeof staticValue.value !== 'function';
137}
138
139function * sourceRangesOfArguments(sourceCode, callExpression) {
140 const openingParen = sourceCode.getTokenAfter(
141 callExpression.callee,
142 {filter: token => isOpeningParenToken(token)},
143 );
144
145 const closingParen = sourceCode.getLastToken(callExpression);
146
147 for (const [index, argument] of callExpression.arguments.entries()) {
148 const previousToken = index === 0
149 ? openingParen
150 : sourceCode.getTokenBefore(
151 argument,
152 {filter: token => isCommaToken(token)},
153 );
154
155 const nextToken = index === callExpression.arguments.length - 1
156 ? closingParen
157 : sourceCode.getTokenAfter(
158 argument,
159 {filter: token => isCommaToken(token)},
160 );
161
162 const firstToken = sourceCode.getTokenAfter(
163 previousToken,
164 {includeComments: true},
165 );
166
167 const lastToken = sourceCode.getTokenBefore(
168 nextToken,
169 {includeComments: true},
170 );
171
172 yield [firstToken.range[0], lastToken.range[1]];
173 }
174}
175
176function sourceOfBinaryExpressionComponents(sourceCode, node) {
177 const {operator, left, right} = node;
178
179 const operatorToken = sourceCode.getFirstTokenBetween(
180 left,
181 right,
182 {filter: token => token.value === operator},
183 );
184
185 const previousToken = sourceCode.getTokenBefore(node);
186 const nextToken = sourceCode.getTokenAfter(node);
187
188 const leftRange = [
189 sourceCode.getTokenAfter(previousToken, {includeComments: true}).range[0],
190 sourceCode.getTokenBefore(operatorToken, {includeComments: true}).range[1],
191 ];
192
193 const rightRange = [
194 sourceCode.getTokenAfter(operatorToken, {includeComments: true}).range[0],
195 sourceCode.getTokenBefore(nextToken, {includeComments: true}).range[1],
196 ];
197
198 return [leftRange, operatorToken, rightRange];
199}
200
201function noComments(sourceCode, ...nodes) {
202 return nodes.every(node => sourceCode.getCommentsBefore(node).length === 0 && sourceCode.getCommentsAfter(node).length === 0);
203}
204
205function isString(node) {
206 const {type} = node;
207 return type === 'TemplateLiteral'
208 || type === 'TaggedTemplateExpression'
209 || (type === 'Literal' && typeof node.value === 'string');
210}
211
212const create = context => {
213 const ava = createAvaRule();
214 const options = context.options[0] ?? {};
215 const enforcesMessage = Boolean(options.message);
216 const shouldHaveMessage = options.message !== 'never';
217
218 function report(node, message) {
219 context.report({node, message});
220 }
221
222 return ava.merge({
223 CallExpression: visitIf([
224 ava.isInTestFile,
225 ava.isInTestNode,
226 ])(node => {
227 const {callee} = node;
228
229 if (
230 callee.type !== 'MemberExpression'
231 || !callee.property
232 || util.getNameOfRootNodeObject(callee) !== 't'
233 || util.isPropertyUnderContext(callee)
234 ) {
235 return;
236 }
237
238 const gottenArguments = node.arguments.length;
239 const firstNonSkipMember = util.getMembers(callee).find(name => name !== 'skip');
240
241 if (firstNonSkipMember === 'end') {
242 if (gottenArguments > 1) {
243 report(node, 'Too many arguments. Expected at most 1.');
244 }
245
246 return;
247 }
248
249 if (firstNonSkipMember === 'try') {
250 if (gottenArguments < 1) {
251 report(node, 'Not enough arguments. Expected at least 1.');
252 }
253
254 return;
255 }
256
257 const nArguments = expectedNbArguments[firstNonSkipMember];
258
259 if (!nArguments) {
260 return;
261 }
262
263 if (gottenArguments < nArguments.min) {
264 report(node, `Not enough arguments. Expected at least ${nArguments.min}.`);
265 } else if (node.arguments.length > nArguments.max) {
266 report(node, `Too many arguments. Expected at most ${nArguments.max}.`);
267 } else {
268 if (enforcesMessage && nArguments.min !== nArguments.max) {
269 const hasMessage = gottenArguments === nArguments.max;
270
271 if (!hasMessage && shouldHaveMessage) {
272 report(node, 'Expected an assertion message, but found none.');
273 } else if (hasMessage && !shouldHaveMessage) {
274 report(node, 'Expected no assertion message, but found one.');
275 }
276 }
277
278 checkArgumentOrder({node, assertion: firstNonSkipMember, context});
279 }
280
281 if (gottenArguments === nArguments.max && nArguments.min !== nArguments.max) {
282 let lastArgument = node.arguments.at(-1);
283
284 if (lastArgument.type === 'Identifier') {
285 const variable = findVariable(context.sourceCode.getScope(node), lastArgument);
286 let value;
287 for (const reference of variable.references) {
288 value = reference.writeExpr ?? value;
289 }
290
291 lastArgument = value;
292 }
293
294 if (!isString(lastArgument)) {
295 report(node, 'Assertion message should be a string.');
296 }
297 }
298 }),
299 });
300};
301
302function checkArgumentOrder({node, assertion, context}) {
303 const [first, second] = node.arguments;
304 if (actualExpectedAssertions.has(assertion) && second) {
305 const [leftNode, rightNode] = [first, second];
306 if (isStatic(leftNode) && !isStatic(rightNode)) {
307 context.report(
308 makeOutOfOrder2ArgumentReport({
309 node,
310 leftNode,
311 rightNode,
312 context,
313 }),
314 );
315 }
316 } else if (
317 relationalActualExpectedAssertions.has(assertion)
318 && first
319 && first.type === 'BinaryExpression'
320 && comparisonOperators.has(first.operator)
321 ) {
322 const [leftNode, rightNode] = [first.left, first.right];
323 if (isStatic(leftNode) && !isStatic(rightNode)) {
324 context.report(
325 makeOutOfOrder1ArgumentReport({
326 node: first,
327 leftNode,
328 rightNode,
329 context,
330 }),
331 );
332 }
333 }
334}
335
336function makeOutOfOrder2ArgumentReport({node, leftNode, rightNode, context}) {
337 const sourceCode = context.getSourceCode();
338 const [leftRange, rightRange] = sourceRangesOfArguments(sourceCode, node);
339 const report = {
340 message: 'Expected values should come after actual values.',
341 loc: {
342 start: sourceCode.getLocFromIndex(leftRange[0]),
343 end: sourceCode.getLocFromIndex(rightRange[1]),
344 },
345 };
346
347 if (noComments(sourceCode, leftNode, rightNode)) {
348 report.fix = fixer => {
349 const leftText = sourceCode.getText().slice(...leftRange);
350 const rightText = sourceCode.getText().slice(...rightRange);
351 return [
352 fixer.replaceTextRange(leftRange, rightText),
353 fixer.replaceTextRange(rightRange, leftText),
354 ];
355 };
356 }
357
358 return report;
359}
360
361function makeOutOfOrder1ArgumentReport({node, leftNode, rightNode, context}) {
362 const sourceCode = context.getSourceCode();
363 const [
364 leftRange,
365 operatorToken,
366 rightRange,
367 ] = sourceOfBinaryExpressionComponents(sourceCode, node);
368 const report = {
369 message: 'Expected values should come after actual values.',
370 loc: {
371 start: sourceCode.getLocFromIndex(leftRange[0]),
372 end: sourceCode.getLocFromIndex(rightRange[1]),
373 },
374 };
375
376 if (noComments(sourceCode, leftNode, rightNode, node)) {
377 report.fix = fixer => {
378 const leftText = sourceCode.getText().slice(...leftRange);
379 const rightText = sourceCode.getText().slice(...rightRange);
380 return [
381 fixer.replaceTextRange(leftRange, rightText),
382 fixer.replaceText(operatorToken, flipOperator(node.operator)),
383 fixer.replaceTextRange(rightRange, leftText),
384 ];
385 };
386 }
387
388 return report;
389}
390
391const schema = [{
392 type: 'object',
393 properties: {
394 message: {
395 enum: [
396 'always',
397 'never',
398 ],
399 default: undefined,
400 },
401 },
402}];
403
404module.exports = {
405 create,
406 meta: {
407 type: 'problem',
408 docs: {
409 description: 'Enforce passing correct arguments to assertions.',
410 url: util.getDocsUrl(__filename),
411 },
412 fixable: 'code',
413 schema,
414 },
415};