From 95b9941b970d1c3be9e42f6075b6064b73cafaef Mon Sep 17 00:00:00 2001 From: ninevra Date: Tue, 14 Jul 2020 01:25:15 -0700 Subject: [PATCH 01/27] Test detecting out-of-ordering assertion-arguments --- test/assertion-arguments.js | 112 +++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index ed6c1cd4..b1969b49 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -5,6 +5,9 @@ const rule = require('../rules/assertion-arguments'); const ruleTester = avaRuleTester(test, { env: { es6: true + }, + parserOptions: { + ecmaVersion: 2020 } }); @@ -12,6 +15,7 @@ const missingError = 'Expected an assertion message, but found none.'; const foundError = 'Expected no assertion message, but found one.'; const tooFewError = n => `Not enough arguments. Expected at least ${n}.`; const tooManyError = n => `Too many arguments. Expected at most ${n}.`; +const outOfOrderError = 'Expected values should come after actual values.'; const header = 'const test = require(\'ava\');'; @@ -32,6 +36,84 @@ function testCase(message, content, errorMessage, useHeader) { }; } +const statics = [ + 'null', + 'true', + 'false', + '1.0', + '1n', + '"string"', + /* eslint-disable no-template-curly-in-string */ + '`template ${"string"}`', + /* eslint-enable no-template-curly-in-string */ + '/.*regex.*\\.js/ig', + '{}', + '{a: 1}', + '{a: {b: 1}}', + '{"c": 1}', + '{3: 1}', + '{["a" + "b"]: {c: 1}}', + '{...{a: 1}}', + '[]', + '[1, 2, {a: 3}]', + '[...[1, 2, 3]]', + 'void 0', + 'void a', + '~1', + '!""', + '+[]', + '-"a"', + '1 + "a"', + '1 && 0', + 'null ?? false', + 'true ? [] : [1]', + 'true ? [] : a', + '{a: 1}.a', + '{a: 1}["a"]', + '{a: 1}.a["a"]', + '[1][0]', + '[[1]][0][0]', + '{a: 1}?.a?.["b"]', + '[{a: 1}]?.a?.[0]', + 'a = 1' +]; + +const dynamics = [ + 'NaN', + 'undefined', + 'Infinity', + '-Infinity', + 'a', + 'a.b', + 'a["b"]', + '{}[a]', + '{}.a[a]', + '[][a]', + '[[1]][0][a]', + '(() => {}).a', + 'a()', + 'new A()', + 'class A {}', + 'function a() {}', + '() => {}', + 'tagged`template string`', + 'new RegExp(\'[dynamic]\')', + '~a', + '++a', + 'delete a.b', + '[delete a.b]', + '{...a}', + '{[a]: 1}', + '{ a() {} }', + '{ get a() {}}', + '{ set a(value) {} }', + '[...a]', + 'a ? [] : [1]', + 'true ? a : [1]', + '"a"?.()', + '{a: 1}?.[b]' +]; + ruleTester.run('assertion-arguments', rule, { valid: [ testCase(false, 't.plan(1);'), @@ -173,7 +255,20 @@ ruleTester.run('assertion-arguments', rule, { testCase('never', 't.end.skip();'), testCase('never', 't.end.skip(error);'), testCase('never', 't.skip.end();'), - testCase('never', 't.skip.end(error);') + testCase('never', 't.skip.end(error);'), + + // Assertion argument order + testCase(false, 't.deepEqual(\'static\', \'static\');'), + testCase(false, 't.deepEqual(dynamic, \'static\');'), + testCase(false, 't.deepEqual(dynamic, dynamic);'), + testCase(false, 't.notDeepEqual(dynamic, \'static\');'), + testCase(false, 't.throws(() => {}, expected);'), + testCase(false, 't.throws(() => {}, null, "message");'), + // No documented actual/expected distinction for t.regex() + testCase(false, 't.regex(\'static\', new RegExp(\'[dynamic]+\'));'), + testCase(false, 't.regex(dynamic, /[static]/);'), + testCase(false, 't.notRegex(\'static\', new RegExp(\'[dynamic]+\'));'), + testCase(false, 't.notRegex(dynamic, /[static]/);') ], invalid: [ // Not enough arguments @@ -265,6 +360,19 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.end(\'too many\', \'arguments\');', tooManyError(1)), testCase(false, 't.skip.end(\'too many\', \'arguments\');', tooManyError(1)), - testCase(false, 't.end.skip(\'too many\', \'arguments\');', tooManyError(1)) + testCase(false, 't.end.skip(\'too many\', \'arguments\');', tooManyError(1)), + + // Assertion argument order + testCase(false, 't.deepEqual(\'static\', dynamic);', outOfOrderError), + testCase(false, 't.notDeepEqual({static: true}, dynamic);', outOfOrderError), + testCase(false, 't.throws({name: \'TypeError\'}, () => {})', outOfOrderError), + testCase('always', 't.deepEqual({}, actual, \'message\');', outOfOrderError), + testCase('never', 't.deepEqual({}, actual)', outOfOrderError), + ...statics.map(expression => + testCase(false, `t.deepEqual(${expression}, dynamic);`, outOfOrderError) + ), + ...dynamics.map(expression => + testCase(false, `t.deepEqual('static', ${expression});`, outOfOrderError) + ) ] }); From 6542f06d4fc79324bb25bac59e4b7e88710765c9 Mon Sep 17 00:00:00 2001 From: ninevra Date: Tue, 14 Jul 2020 19:42:08 -0700 Subject: [PATCH 02/27] Allow specifying autofix output in testCase() --- test/assertion-arguments.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index b1969b49..9fae0be7 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -19,12 +19,18 @@ const outOfOrderError = 'Expected values should come after actual values.'; const header = 'const test = require(\'ava\');'; -function testCase(message, content, errorMessage, useHeader) { - const testFn = ` - test(t => { - ${content} - }); - `; +function testCase(message, content, errorMessage, { + useHeader, output = null +} = {}) { + function testCode(content, useHeader) { + const testFn = ` + test(t => { + ${content} + }); + `; + const code = (useHeader === false ? '' : header) + testFn; + return code; + } return { errors: errorMessage && [{ @@ -32,7 +38,8 @@ function testCase(message, content, errorMessage, useHeader) { message: errorMessage }], options: message ? [{message}] : [], - code: (useHeader === false ? '' : header) + testFn + code: testCode(content, useHeader), + output: output === null ? null : testCode(output, useHeader) }; } @@ -141,7 +148,7 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.timeout(100, \'message\');'), testCase(false, 'foo.t.plan();'), // Shouldn't be triggered since it's not a test file - testCase(false, 't.true(true);', false, false), + testCase(false, 't.true(true);', false, {useHeader: false}), testCase(false, 't.deepEqual({}, {});'), testCase(false, 't.fail();'), @@ -164,7 +171,7 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.truthy(\'unicorn\');'), testCase(false, 't.snapshot(value);'), // Shouldn't be triggered since it's not a test file - testCase(false, 't.true(true, \'message\');', [], false), + testCase(false, 't.true(true, \'message\');', [], {useHeader: false}), testCase(false, 't.context.a(1, 2, 3, 4);'), testCase(false, 't.context.is(1, 2, 3, 4);'), @@ -197,7 +204,7 @@ ruleTester.run('assertion-arguments', rule, { testCase('always', 't.try(\'title\', tt => tt.pass(), 1, 2);'), // Shouldn't be triggered since it's not a test file - testCase('always', 't.true(true);', [], false), + testCase('always', 't.true(true);', [], {useHeader: false}), testCase('always', 't.context.a(1, 2, 3, 4);'), testCase('always', 't.context.is(1, 2, 3, 4);'), @@ -231,7 +238,7 @@ ruleTester.run('assertion-arguments', rule, { testCase('never', 't.try(\'title\', tt => tt.pass(), 1, 2);'), // Shouldn't be triggered since it's not a test file - testCase('never', 't.true(true, \'message\');', [], false), + testCase('never', 't.true(true, \'message\');', [], {useHeader: false}), testCase('never', 't.context.a(1, 2, 3, 4);'), testCase('never', 't.context.is(1, 2, 3, 4);'), From cebdcbd8def4ca9d5ab742e11b001a8e31d8dd65 Mon Sep 17 00:00:00 2001 From: ninevra Date: Tue, 14 Jul 2020 19:42:30 -0700 Subject: [PATCH 03/27] Test autofix output --- test/assertion-arguments.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index 9fae0be7..06eaa696 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -370,16 +370,30 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.end.skip(\'too many\', \'arguments\');', tooManyError(1)), // Assertion argument order - testCase(false, 't.deepEqual(\'static\', dynamic);', outOfOrderError), - testCase(false, 't.notDeepEqual({static: true}, dynamic);', outOfOrderError), - testCase(false, 't.throws({name: \'TypeError\'}, () => {})', outOfOrderError), - testCase('always', 't.deepEqual({}, actual, \'message\');', outOfOrderError), - testCase('never', 't.deepEqual({}, actual)', outOfOrderError), + testCase(false, 't.deepEqual(\'static\', dynamic);', outOfOrderError, + {output: 't.deepEqual(dynamic, \'static\');'} + ), + testCase(false, 't.notDeepEqual({static: true}, dynamic);', outOfOrderError, + {output: 't.notDeepEqual(dynamic, {static: true});'} + ), + testCase(false, 't.throws({name: \'TypeError\'}, () => {});', outOfOrderError, + {output: 't.throws(() => {}, {name: \'TypeError\'});'} + ), + testCase('always', 't.deepEqual({}, actual, \'message\');', outOfOrderError, + {output: 't.deepEqual(actual, {}, \'message\');'} + ), + testCase('never', 't.deepEqual({}, actual);', outOfOrderError, + {output: 't.deepEqual(actual, {});'} + ), ...statics.map(expression => - testCase(false, `t.deepEqual(${expression}, dynamic);`, outOfOrderError) + testCase(false, `t.deepEqual(${expression}, dynamic);`, outOfOrderError, + {output: `t.deepEqual(dynamic, ${expression});`} + ) ), ...dynamics.map(expression => - testCase(false, `t.deepEqual('static', ${expression});`, outOfOrderError) + testCase(false, `t.deepEqual('static', ${expression});`, outOfOrderError, + {output: `t.deepEqual(${expression}, 'static');`} + ) ) ] }); From 5783fe0df4bed25d6f5b6e1a1e003f3449d0ea90 Mon Sep 17 00:00:00 2001 From: ninevra Date: Tue, 14 Jul 2020 14:21:57 -0700 Subject: [PATCH 04/27] Allow multiple errors, arbitrary error objects in testCase() --- test/assertion-arguments.js | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index 06eaa696..89312ea8 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -19,24 +19,29 @@ const outOfOrderError = 'Expected values should come after actual values.'; const header = 'const test = require(\'ava\');'; -function testCase(message, content, errorMessage, { +function testCode(content, useHeader) { + const testFn = ` + test(t => { + ${content} + }); + `; + const code = (useHeader === false ? '' : header) + testFn; + return code; +} + +function testCase(message, content, errors = [], { useHeader, output = null } = {}) { - function testCode(content, useHeader) { - const testFn = ` - test(t => { - ${content} - }); - `; - const code = (useHeader === false ? '' : header) + testFn; - return code; + if (!Array.isArray(errors)) { + errors = [errors]; } + errors = errors + .map(error => typeof error === 'string' ? {message: error} : error) + .map(error => ({ruleId: 'assertion-arguments', ...error})); + return { - errors: errorMessage && [{ - ruleId: 'assertion-arguments', - message: errorMessage - }], + errors, options: message ? [{message}] : [], code: testCode(content, useHeader), output: output === null ? null : testCode(output, useHeader) From 5369b12e9e26a5ac300c446bc4bdba5360977bae Mon Sep 17 00:00:00 2001 From: ninevra Date: Tue, 14 Jul 2020 20:14:53 -0700 Subject: [PATCH 05/27] Offset error loc in testCase() to account for wrapping --- test/assertion-arguments.js | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index 89312ea8..a4f57c6b 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -29,6 +29,28 @@ function testCode(content, useHeader) { return code; } +function offsetError(error, line, column) { + const offset = {...error}; + + if (error.line !== undefined) { + offset.line += line; + } + + if (error.column !== undefined && error.line === 1) { + offset.column += column; + } + + if (error.endLine !== undefined) { + offset.endLine += line; + } + + if (error.endColumn !== undefined && error.endLine === 1) { + offset.endColumn += column; + } + + return offset; +} + function testCase(message, content, errors = [], { useHeader, output = null } = {}) { @@ -36,9 +58,12 @@ function testCase(message, content, errors = [], { errors = [errors]; } + const offset = useHeader === false ? [1, 3] : [2, 3]; + errors = errors .map(error => typeof error === 'string' ? {message: error} : error) - .map(error => ({ruleId: 'assertion-arguments', ...error})); + .map(error => ({ruleId: 'assertion-arguments', ...error})) + .map(error => offsetError(error, ...offset)); return { errors, From 6bb927092628147b9ec6c5c5704f90a3ddaa3d7d Mon Sep 17 00:00:00 2001 From: ninevra Date: Tue, 14 Jul 2020 20:15:09 -0700 Subject: [PATCH 06/27] Test reported error location --- test/assertion-arguments.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index a4f57c6b..dc3ea66f 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -15,7 +15,10 @@ const missingError = 'Expected an assertion message, but found none.'; const foundError = 'Expected no assertion message, but found one.'; const tooFewError = n => `Not enough arguments. Expected at least ${n}.`; const tooManyError = n => `Too many arguments. Expected at most ${n}.`; -const outOfOrderError = 'Expected values should come after actual values.'; +const outOfOrderError = (line, column, endLine, endColumn) => ({ + message: 'Expected values should come after actual values.', + line, column, endLine, endColumn +}); const header = 'const test = require(\'ava\');'; @@ -400,28 +403,35 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.end.skip(\'too many\', \'arguments\');', tooManyError(1)), // Assertion argument order - testCase(false, 't.deepEqual(\'static\', dynamic);', outOfOrderError, + testCase(false, 't.deepEqual(\'static\', dynamic);', + outOfOrderError(1, 13, 1, 30), {output: 't.deepEqual(dynamic, \'static\');'} ), - testCase(false, 't.notDeepEqual({static: true}, dynamic);', outOfOrderError, + testCase(false, 't.notDeepEqual({static: true}, dynamic);', + outOfOrderError(1, 16, 1, 39), {output: 't.notDeepEqual(dynamic, {static: true});'} ), - testCase(false, 't.throws({name: \'TypeError\'}, () => {});', outOfOrderError, + testCase(false, 't.throws({name: \'TypeError\'}, () => {});', + outOfOrderError(1, 10, 1, 39), {output: 't.throws(() => {}, {name: \'TypeError\'});'} ), - testCase('always', 't.deepEqual({}, actual, \'message\');', outOfOrderError, + testCase('always', 't.deepEqual({}, actual, \'message\');', + outOfOrderError(1, 13, 1, 23), {output: 't.deepEqual(actual, {}, \'message\');'} ), - testCase('never', 't.deepEqual({}, actual);', outOfOrderError, + testCase('never', 't.deepEqual({}, actual);', + outOfOrderError(1, 13, 1, 23), {output: 't.deepEqual(actual, {});'} ), ...statics.map(expression => - testCase(false, `t.deepEqual(${expression}, dynamic);`, outOfOrderError, + testCase(false, `t.deepEqual(${expression}, dynamic);`, + outOfOrderError(1, 13, 1, 22 + expression.length), {output: `t.deepEqual(dynamic, ${expression});`} ) ), ...dynamics.map(expression => - testCase(false, `t.deepEqual('static', ${expression});`, outOfOrderError, + testCase(false, `t.deepEqual('static', ${expression});`, + outOfOrderError(1, 13, 1, 23 + expression.length), {output: `t.deepEqual(${expression}, 'static');`} ) ) From 24f639d69e7933e840d8d04baed31e3ed05c0c01 Mon Sep 17 00:00:00 2001 From: ninevra Date: Tue, 14 Jul 2020 15:33:19 -0700 Subject: [PATCH 07/27] Test in combination with other error types outOfOrderErrors make sense and remain fixable in combination with message errors. In the presence of too many assertion arguments, something weird is going on, so it makes little sense to try to predict which of the arguments is the expected value. outOfOrderErrors can't occur in combination with too few assertion arguments. --- test/assertion-arguments.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index dc3ea66f..bab86532 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -423,6 +423,17 @@ ruleTester.run('assertion-arguments', rule, { outOfOrderError(1, 13, 1, 23), {output: 't.deepEqual(actual, {});'} ), + testCase('always', 't.deepEqual({}, actual);', + [missingError, outOfOrderError(1, 13, 1, 23)], + {output: 't.deepEqual(actual, {});'} + ), + testCase('never', 't.deepEqual({}, actual, \'message\');', + [foundError, outOfOrderError(1, 13, 1, 23)], + {output: 't.deepEqual(actual, {}, \'message\');'} + ), + testCase(false, 't.deepEqual({}, actual, extra, \'message\');', + tooManyError(3) + ), ...statics.map(expression => testCase(false, `t.deepEqual(${expression}, dynamic);`, outOfOrderError(1, 13, 1, 22 + expression.length), From e1fab590f07b0190bb005bc5fcbc8368879db0a5 Mon Sep 17 00:00:00 2001 From: ninevra Date: Tue, 14 Jul 2020 15:56:03 -0700 Subject: [PATCH 08/27] Test with t.is() and t.not() --- test/assertion-arguments.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index bab86532..73fc817f 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -301,6 +301,8 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.deepEqual(\'static\', \'static\');'), testCase(false, 't.deepEqual(dynamic, \'static\');'), testCase(false, 't.deepEqual(dynamic, dynamic);'), + testCase(false, 't.is(dynamic, \'static\');'), + testCase(false, 't.not(dynamic, \'static\');'), testCase(false, 't.notDeepEqual(dynamic, \'static\');'), testCase(false, 't.throws(() => {}, expected);'), testCase(false, 't.throws(() => {}, null, "message");'), @@ -407,6 +409,14 @@ ruleTester.run('assertion-arguments', rule, { outOfOrderError(1, 13, 1, 30), {output: 't.deepEqual(dynamic, \'static\');'} ), + testCase(false, 't.is(\'static\', dynamic);', + outOfOrderError(1, 6, 1, 23), + {output: 't.is(dynamic, \'static\');'} + ), + testCase(false, 't.not(\'static\', dynamic);', + outOfOrderError(1, 7, 1, 24), + {output: 't.not(dynamic, \'static\');'} + ), testCase(false, 't.notDeepEqual({static: true}, dynamic);', outOfOrderError(1, 16, 1, 39), {output: 't.notDeepEqual(dynamic, {static: true});'} From e2b7990c9955751914f52550f45f8d72826b0ac1 Mon Sep 17 00:00:00 2001 From: ninevra Date: Wed, 15 Jul 2020 01:29:01 -0700 Subject: [PATCH 09/27] Test autofixing with parentheses --- test/assertion-arguments.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index 73fc817f..fdb8c6d9 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -444,6 +444,10 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.deepEqual({}, actual, extra, \'message\');', tooManyError(3) ), + testCase(false, 't.deepEqual({}, (actual));', + outOfOrderError(1, 13, 1, 25), + {output: 't.deepEqual((actual), {});'} + ), ...statics.map(expression => testCase(false, `t.deepEqual(${expression}, dynamic);`, outOfOrderError(1, 13, 1, 22 + expression.length), From 6af165a8d1f263df68378fa68efb41568cf3eb04 Mon Sep 17 00:00:00 2001 From: ninevra Date: Tue, 14 Jul 2020 01:25:43 -0700 Subject: [PATCH 10/27] Document detecting out-of-order assertion-arguments --- docs/rules/assertion-arguments.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/rules/assertion-arguments.md b/docs/rules/assertion-arguments.md index 32daff29..97f5b846 100644 --- a/docs/rules/assertion-arguments.md +++ b/docs/rules/assertion-arguments.md @@ -6,6 +6,8 @@ Enforces passing the right number of arguments to assertion methods like `t.is() Assertion messages are optional arguments that can be given to any assertion call to improve the error message, should the assertion fail. +This rule also attempts to enforce passing the actual and expected values in the correct order. If exactly one of the first two arguments to a two-argument assertion is a static expression such as `{a: 1}`, then this rule requires that the static expression come second. (`t.regex()` and `t.notRegex()` are excluded from this check, because either their `contents` argument or their `regex` argument could plausibly be the actual or expected value.) Errors of this kind are fixable. + ## Fail ```js From a565059527bcf65377bf5ae39c76eb6a80cf63ab Mon Sep 17 00:00:00 2001 From: ninevra Date: Wed, 15 Jul 2020 13:22:31 -0700 Subject: [PATCH 11/27] Detect and fix out-of-order assertion-arguments --- package.json | 1 + rules/assertion-arguments.js | 73 +++++++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b76650aa..5a395288 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "dependencies": { "deep-strict-equal": "^0.2.0", "enhance-visitors": "^1.0.0", + "eslint-utils": "^2.1.0", "espree": "^7.1.0", "espurify": "^2.0.1", "import-modules": "^2.0.0", diff --git a/rules/assertion-arguments.js b/rules/assertion-arguments.js index a6c14944..641f6ccb 100644 --- a/rules/assertion-arguments.js +++ b/rules/assertion-arguments.js @@ -1,5 +1,6 @@ 'use strict'; const {visitIf} = require('enhance-visitors'); +const {getStaticValue, isOpeningParenToken, isCommaToken} = require('eslint-utils'); const util = require('../util'); const createAvaRule = require('../create-ava-rule'); @@ -86,6 +87,40 @@ const expectedNbArguments = { } }; +const actualExpectedAssertions = new Set([ + 'deepEqual', 'is', 'like', 'not', 'notDeepEqual', 'throws', 'throwsAsync' +]); + +function isStatic(node) { + const staticValue = getStaticValue(node); + return staticValue !== null && typeof staticValue.value !== 'function'; +} + +function * sourceRangesOfArguments(sourceCode, callExpression) { + const openingParen = sourceCode.getTokenAfter( + callExpression.callee, + {filter: token => isOpeningParenToken(token)} + ); + const closingParen = sourceCode.getLastToken(callExpression); + for (let index = 0; index < callExpression.arguments.length; index++) { + const previousToken = index === 0 ? + openingParen : + sourceCode.getTokenBefore( + callExpression.arguments[index], + {filter: token => isCommaToken(token)} + ); + const nextToken = index === callExpression.arguments.length - 1 ? + closingParen : + sourceCode.getTokenAfter( + callExpression.arguments[index], + {filter: token => isCommaToken(token)} + ); + const firstToken = sourceCode.getTokenAfter(previousToken); + const lastToken = sourceCode.getTokenBefore(nextToken); + yield [firstToken.start, lastToken.end]; + } +} + const create = context => { const ava = createAvaRule(); const options = context.options[0] || {}; @@ -141,13 +176,38 @@ const create = context => { report(node, `Not enough arguments. Expected at least ${nArgs.min}.`); } else if (node.arguments.length > nArgs.max) { report(node, `Too many arguments. Expected at most ${nArgs.max}.`); - } else if (enforcesMessage && nArgs.min !== nArgs.max) { - const hasMessage = gottenArgs === nArgs.max; + } else { + if (enforcesMessage && nArgs.min !== nArgs.max) { + const hasMessage = gottenArgs === nArgs.max; + + if (!hasMessage && shouldHaveMessage) { + report(node, 'Expected an assertion message, but found none.'); + } else if (hasMessage && !shouldHaveMessage) { + report(node, 'Expected no assertion message, but found one.'); + } + } - if (!hasMessage && shouldHaveMessage) { - report(node, 'Expected an assertion message, but found none.'); - } else if (hasMessage && !shouldHaveMessage) { - report(node, 'Expected no assertion message, but found one.'); + if (actualExpectedAssertions.has(members[0]) && gottenArgs >= 2) { + const [leftNode, rightNode] = node.arguments; + if (isStatic(leftNode) && !isStatic(rightNode)) { + const sourceCode = context.getSourceCode(); + const [leftRange, rightRange] = sourceRangesOfArguments(sourceCode, node); + context.report({ + message: 'Expected values should come after actual values.', + loc: { + start: sourceCode.getLocFromIndex(leftRange[0]), + end: sourceCode.getLocFromIndex(rightRange[1]) + }, + fix(fixer) { + const leftText = sourceCode.getText().slice(...leftRange); + const rightText = sourceCode.getText().slice(...rightRange); + return [ + fixer.replaceTextRange(leftRange, rightText), + fixer.replaceTextRange(rightRange, leftText) + ]; + } + }); + } } } }) @@ -170,6 +230,7 @@ const schema = [{ module.exports = { create, meta: { + fixable: 'code', docs: { url: util.getDocsUrl(__filename) }, From 4031607f2a2f45a80d04cfb8ba99e14a5b86c172 Mon Sep 17 00:00:00 2001 From: ninevra Date: Wed, 15 Jul 2020 13:31:22 -0700 Subject: [PATCH 12/27] Test with single-argument assertions --- test/assertion-arguments.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index fdb8c6d9..32f9c555 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -306,6 +306,10 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.notDeepEqual(dynamic, \'static\');'), testCase(false, 't.throws(() => {}, expected);'), testCase(false, 't.throws(() => {}, null, "message");'), + testCase(false, 't.true(dynamic === \'static\');'), + testCase(false, 't.false(dynamic === \'static\');'), + testCase(false, 't.truthy(dynamic === \'static\');'), + testCase(false, 't.falsy(dynamic === \'static\');'), // No documented actual/expected distinction for t.regex() testCase(false, 't.regex(\'static\', new RegExp(\'[dynamic]+\'));'), testCase(false, 't.regex(dynamic, /[static]/);'), @@ -448,6 +452,22 @@ ruleTester.run('assertion-arguments', rule, { outOfOrderError(1, 13, 1, 25), {output: 't.deepEqual((actual), {});'} ), + testCase(false, 't.true(\'static\' <= dynamic);', + outOfOrderError(1, 8, 1, 27), + {output: 't.true(dynamic >= \'static\');'} + ), + testCase(false, 't.false(\'static\' < dynamic);', + outOfOrderError(1, 9, 1, 27), + {output: 't.false(dynamic > \'static\');'} + ), + testCase(false, 't.truthy(\'static\' > dynamic);', + outOfOrderError(1, 10, 1, 28), + {output: 't.truthy(dynamic < \'static\');'} + ), + testCase(false, 't.falsy(\'static\' >= dynamic);', + outOfOrderError(1, 9, 1, 28), + {output: 't.falsy(dynamic <= \'static\');'} + ), ...statics.map(expression => testCase(false, `t.deepEqual(${expression}, dynamic);`, outOfOrderError(1, 13, 1, 22 + expression.length), From 09af125272eda2390cc896c5aa9e23f6268aff23 Mon Sep 17 00:00:00 2001 From: ninevra Date: Wed, 15 Jul 2020 13:31:54 -0700 Subject: [PATCH 13/27] Support 1-arg assertions with comparison expressions --- rules/assertion-arguments.js | 69 ++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/rules/assertion-arguments.js b/rules/assertion-arguments.js index 641f6ccb..a3bc21ab 100644 --- a/rules/assertion-arguments.js +++ b/rules/assertion-arguments.js @@ -91,6 +91,25 @@ const actualExpectedAssertions = new Set([ 'deepEqual', 'is', 'like', 'not', 'notDeepEqual', 'throws', 'throwsAsync' ]); +const relationalActualExpectedAssertions = new Set([ + 'assert', 'truthy', 'falsy', 'true', 'false' +]); + +const comparisonOperators = new Map([ + ['>', '<'], + ['>=', '<='], + ['==', '=='], + ['===', '==='], + ['!=', '!='], + ['!==', '!=='], + ['<=', '>='], + ['<', '>'] +]); + +const flipOperator = operator => { + return comparisonOperators.get(operator); +}; + function isStatic(node) { const staticValue = getStaticValue(node); return staticValue !== null && typeof staticValue.value !== 'function'; @@ -121,6 +140,26 @@ function * sourceRangesOfArguments(sourceCode, callExpression) { } } +function sourceOfBinaryExpressionComponents(sourceCode, node) { + const {operator, left, right} = node; + const operatorToken = sourceCode.getFirstTokenBetween( + left, + right, + {filter: token => token.value === operator} + ); + const previousToken = sourceCode.getTokenBefore(node); + const nextToken = sourceCode.getTokenAfter(node); + const leftRange = [ + sourceCode.getTokenAfter(previousToken, {includeComments: true}).start, + sourceCode.getTokenBefore(operatorToken, {includeComments: true}).end + ]; + const rightRange = [ + sourceCode.getTokenAfter(operatorToken, {includeComments: true}).start, + sourceCode.getTokenBefore(nextToken, {includeComments: true}).end + ]; + return [leftRange, operatorToken, rightRange]; +} + const create = context => { const ava = createAvaRule(); const options = context.options[0] || {}; @@ -208,6 +247,36 @@ const create = context => { } }); } + } else if (relationalActualExpectedAssertions.has(members[0]) && gottenArgs >= 1) { + const argument = node.arguments[0]; + if (argument.type === 'BinaryExpression' && comparisonOperators.has(argument.operator)) { + const leftNode = argument.left; + const rightNode = argument.right; + if (isStatic(leftNode) && !isStatic(rightNode)) { + const sourceCode = context.getSourceCode(); + const [ + leftRange, + operatorToken, + rightRange + ] = sourceOfBinaryExpressionComponents(sourceCode, argument); + const rightText = sourceCode.getText().slice(...rightRange); + const leftText = sourceCode.getText().slice(...leftRange); + context.report({ + message: 'Expected values should come after actual values.', + loc: { + start: sourceCode.getLocFromIndex(leftRange[0]), + end: sourceCode.getLocFromIndex(rightRange[1]) + }, + fix(fixer) { + return [ + fixer.replaceTextRange(leftRange, rightText), + fixer.replaceText(operatorToken, flipOperator(argument.operator)), + fixer.replaceTextRange(rightRange, leftText) + ]; + } + }); + } + } } } }) From 197b0fdeb405cab6ecbfba42e0df23e4c29942f0 Mon Sep 17 00:00:00 2001 From: ninevra Date: Wed, 15 Jul 2020 14:34:32 -0700 Subject: [PATCH 14/27] Document support for 1-arg assertions --- docs/rules/assertion-arguments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/assertion-arguments.md b/docs/rules/assertion-arguments.md index 97f5b846..14fd84a8 100644 --- a/docs/rules/assertion-arguments.md +++ b/docs/rules/assertion-arguments.md @@ -6,7 +6,7 @@ Enforces passing the right number of arguments to assertion methods like `t.is() Assertion messages are optional arguments that can be given to any assertion call to improve the error message, should the assertion fail. -This rule also attempts to enforce passing the actual and expected values in the correct order. If exactly one of the first two arguments to a two-argument assertion is a static expression such as `{a: 1}`, then this rule requires that the static expression come second. (`t.regex()` and `t.notRegex()` are excluded from this check, because either their `contents` argument or their `regex` argument could plausibly be the actual or expected value.) Errors of this kind are fixable. +This rule also attempts to enforce passing the actual and expected values in the correct order. If exactly one of the first two arguments to a two-argument assertion is a static expression such as `{a: 1}`, then this rule requires that the static expression come second. (`t.regex()` and `t.notRegex()` are excluded from this check, because either their `contents` argument or their `regex` argument could plausibly be the actual or expected value.) If the argument to a one-argument assertion is a binary relation such as `'static' === dynamic`, a similar check is performed on its left- and right-hand sides. Errors of these kinds are fixable. ## Fail From 38a1b5aacacd6143088576a11c0eb3ea3ac9b599 Mon Sep 17 00:00:00 2001 From: ninevra Date: Tue, 14 Jul 2020 16:12:46 -0700 Subject: [PATCH 15/27] Support t.throwsAsync() --- rules/assertion-arguments.js | 4 ++++ test/assertion-arguments.js | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/rules/assertion-arguments.js b/rules/assertion-arguments.js index a3bc21ab..d0fa8fc6 100644 --- a/rules/assertion-arguments.js +++ b/rules/assertion-arguments.js @@ -73,6 +73,10 @@ const expectedNbArguments = { min: 1, max: 3 }, + throwsAsync: { + min: 1, + max: 3 + }, true: { min: 1, max: 2 diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index 32f9c555..d0f742ce 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -173,6 +173,7 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.notRegex(a, /a/, \'message\');'), testCase(false, 't.skip.is(\'same\', \'same\', \'message\');'), testCase(false, 't.throws(Promise.reject(), Error, \'message\');'), + testCase(false, 't.throwsAsync(Promise.reject(), Error, \'message\');'), testCase(false, 't.true(true, \'message\');'), testCase(false, 't.truthy(\'unicorn\', \'message\');'), testCase(false, 't.snapshot(value, \'message\');'), @@ -200,6 +201,8 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.skip.is(\'same\', \'same\');'), testCase(false, 't.throws(Promise.reject());'), testCase(false, 't.throws(Promise.reject(), Error);'), + testCase(false, 't.throwsAsync(Promise.reject());'), + testCase(false, 't.throwsAsync(Promise.reject(), Error);'), testCase(false, 't.true(true);'), testCase(false, 't.truthy(\'unicorn\');'), testCase(false, 't.snapshot(value);'), @@ -224,6 +227,7 @@ ruleTester.run('assertion-arguments', rule, { testCase('always', 't.like({}, {}, \'message\');'), testCase('always', 't.throws(Promise.reject(), Error, \'message\');'), testCase('always', 't.notThrows(Promise.resolve(), \'message\');'), + testCase('always', 't.throwsAsync(Promise.reject(), Error, \'message\');'), testCase('always', 't.regex(a, /a/, \'message\');'), testCase('always', 't.notRegex(a, /a/, \'message\');'), testCase('always', 't.ifError(new Error(), \'message\');'), @@ -258,6 +262,8 @@ ruleTester.run('assertion-arguments', rule, { testCase('never', 't.throws(Promise.reject());'), testCase('never', 't.throws(Promise.reject(), Error);'), testCase('never', 't.notThrows(Promise.resolve());'), + testCase('never', 't.throwsAsync(Promise.reject());'), + testCase('never', 't.throwsAsync(Promise.reject(), Error);'), testCase('never', 't.regex(a, /a/);'), testCase('never', 't.notRegex(a, /a/);'), testCase('never', 't.ifError(new Error());'), @@ -306,6 +312,8 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.notDeepEqual(dynamic, \'static\');'), testCase(false, 't.throws(() => {}, expected);'), testCase(false, 't.throws(() => {}, null, "message");'), + testCase(false, 't.throwsAsync(async () => {}, {name: \'TypeError\'});'), + testCase(false, 't.throwsAsync(async () => {}, null, "message");'), testCase(false, 't.true(dynamic === \'static\');'), testCase(false, 't.false(dynamic === \'static\');'), testCase(false, 't.truthy(dynamic === \'static\');'), @@ -330,6 +338,7 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.like({});', tooFewError(2)), testCase(false, 't.throws();', tooFewError(1)), testCase(false, 't.notThrows();', tooFewError(1)), + testCase(false, 't.throwsAsync();', tooFewError(1)), testCase(false, 't.regex(a);', tooFewError(2)), testCase(false, 't.notRegex(a);', tooFewError(2)), testCase(false, 't.ifError();', tooFewError(1)), @@ -355,6 +364,7 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.like({}, {}, \'message\', \'extra argument\');', tooManyError(3)), testCase(false, 't.throws(Promise.reject(), Error, \'message\', \'extra argument\');', tooManyError(3)), testCase(false, 't.notThrows(Promise.resolve(), \'message\', \'extra argument\');', tooManyError(2)), + testCase(false, 't.throwsAsync(Promise.reject(), Error, \'message\', \'extra argument\');', tooManyError(3)), testCase(false, 't.regex(a, /a/, \'message\', \'extra argument\');', tooManyError(3)), testCase(false, 't.notRegex(a, /a/, \'message\', \'extra argument\');', tooManyError(3)), testCase(false, 't.ifError(new Error(), \'message\', \'extra argument\');', tooManyError(2)), @@ -378,6 +388,8 @@ ruleTester.run('assertion-arguments', rule, { testCase('always', 't.throws(Promise.reject());', missingError), testCase('always', 't.throws(Promise.reject(), Error);', missingError), testCase('always', 't.notThrows(Promise.resolve());', missingError), + testCase('always', 't.throwsAsync(Promise.reject());', missingError), + testCase('always', 't.throwsAsync(Promise.reject(), Error);', missingError), testCase('always', 't.regex(a, /a/);', missingError), testCase('always', 't.notRegex(a, /a/);', missingError), testCase('always', 't.ifError(new Error());', missingError), @@ -397,6 +409,7 @@ ruleTester.run('assertion-arguments', rule, { testCase('never', 't.like({}, {}, \'message\');', foundError), testCase('never', 't.throws(Promise.reject(), Error, \'message\');', foundError), testCase('never', 't.notThrows(Promise.resolve(), \'message\');', foundError), + testCase('never', 't.throwsAsync(Promise.reject(), Error, \'message\');', foundError), testCase('never', 't.regex(a, /a/, \'message\');', foundError), testCase('never', 't.notRegex(a, /a/, \'message\');', foundError), testCase('never', 't.ifError(new Error(), \'message\');', foundError), @@ -429,6 +442,10 @@ ruleTester.run('assertion-arguments', rule, { outOfOrderError(1, 10, 1, 39), {output: 't.throws(() => {}, {name: \'TypeError\'});'} ), + testCase(false, 't.throwsAsync({name: \'TypeError\'}, async () => {});', + outOfOrderError(1, 15, 1, 50), + {output: 't.throwsAsync(async () => {}, {name: \'TypeError\'});'} + ), testCase('always', 't.deepEqual({}, actual, \'message\');', outOfOrderError(1, 13, 1, 23), {output: 't.deepEqual(actual, {}, \'message\');'} From 44e68f7f6257382278da009466e933a67111cc7d Mon Sep 17 00:00:00 2001 From: ninevra Date: Tue, 14 Jul 2020 16:18:40 -0700 Subject: [PATCH 16/27] Support t.notThrowsAsync() --- rules/assertion-arguments.js | 4 ++++ test/assertion-arguments.js | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/rules/assertion-arguments.js b/rules/assertion-arguments.js index d0fa8fc6..25d243e7 100644 --- a/rules/assertion-arguments.js +++ b/rules/assertion-arguments.js @@ -45,6 +45,10 @@ const expectedNbArguments = { min: 1, max: 2 }, + notThrowsAsync: { + min: 1, + max: 2 + }, pass: { min: 0, max: 1 diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index d0f742ce..c34fbaf3 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -168,6 +168,7 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.not(\'not\', \'same\', \'message\');'), testCase(false, 't.notDeepEqual({}, {a: true}, \'message\');'), testCase(false, 't.notThrows(Promise.resolve(), \'message\');'), + testCase(false, 't.notThrowsAsync(Promise.resolve(), \'message\');'), testCase(false, 't.pass(\'message\');'), testCase(false, 't.regex(a, /a/, \'message\');'), testCase(false, 't.notRegex(a, /a/, \'message\');'), @@ -195,6 +196,7 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.not(\'not\', \'same\');'), testCase(false, 't.notDeepEqual({}, {a: true});'), testCase(false, 't.notThrows(Promise.resolve());'), + testCase(false, 't.notThrowsAsync(Promise.resolve());'), testCase(false, 't.pass();'), testCase(false, 't.regex(a, /a/);'), testCase(false, 't.notRegex(a, /a/);'), @@ -228,6 +230,7 @@ ruleTester.run('assertion-arguments', rule, { testCase('always', 't.throws(Promise.reject(), Error, \'message\');'), testCase('always', 't.notThrows(Promise.resolve(), \'message\');'), testCase('always', 't.throwsAsync(Promise.reject(), Error, \'message\');'), + testCase('always', 't.notThrowsAsync(Promise.resolve(), \'message\');'), testCase('always', 't.regex(a, /a/, \'message\');'), testCase('always', 't.notRegex(a, /a/, \'message\');'), testCase('always', 't.ifError(new Error(), \'message\');'), @@ -264,6 +267,7 @@ ruleTester.run('assertion-arguments', rule, { testCase('never', 't.notThrows(Promise.resolve());'), testCase('never', 't.throwsAsync(Promise.reject());'), testCase('never', 't.throwsAsync(Promise.reject(), Error);'), + testCase('never', 't.notThrowsAsync(Promise.resolve());'), testCase('never', 't.regex(a, /a/);'), testCase('never', 't.notRegex(a, /a/);'), testCase('never', 't.ifError(new Error());'), @@ -339,6 +343,7 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.throws();', tooFewError(1)), testCase(false, 't.notThrows();', tooFewError(1)), testCase(false, 't.throwsAsync();', tooFewError(1)), + testCase(false, 't.notThrowsAsync();', tooFewError(1)), testCase(false, 't.regex(a);', tooFewError(2)), testCase(false, 't.notRegex(a);', tooFewError(2)), testCase(false, 't.ifError();', tooFewError(1)), @@ -365,6 +370,7 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.throws(Promise.reject(), Error, \'message\', \'extra argument\');', tooManyError(3)), testCase(false, 't.notThrows(Promise.resolve(), \'message\', \'extra argument\');', tooManyError(2)), testCase(false, 't.throwsAsync(Promise.reject(), Error, \'message\', \'extra argument\');', tooManyError(3)), + testCase(false, 't.notThrowsAsync(Promise.resolve(), \'message\', \'extra argument\');', tooManyError(2)), testCase(false, 't.regex(a, /a/, \'message\', \'extra argument\');', tooManyError(3)), testCase(false, 't.notRegex(a, /a/, \'message\', \'extra argument\');', tooManyError(3)), testCase(false, 't.ifError(new Error(), \'message\', \'extra argument\');', tooManyError(2)), @@ -390,6 +396,7 @@ ruleTester.run('assertion-arguments', rule, { testCase('always', 't.notThrows(Promise.resolve());', missingError), testCase('always', 't.throwsAsync(Promise.reject());', missingError), testCase('always', 't.throwsAsync(Promise.reject(), Error);', missingError), + testCase('always', 't.notThrowsAsync(Promise.resolve());', missingError), testCase('always', 't.regex(a, /a/);', missingError), testCase('always', 't.notRegex(a, /a/);', missingError), testCase('always', 't.ifError(new Error());', missingError), @@ -410,6 +417,7 @@ ruleTester.run('assertion-arguments', rule, { testCase('never', 't.throws(Promise.reject(), Error, \'message\');', foundError), testCase('never', 't.notThrows(Promise.resolve(), \'message\');', foundError), testCase('never', 't.throwsAsync(Promise.reject(), Error, \'message\');', foundError), + testCase('never', 't.notThrowsAsync(Promise.resolve(), \'message\');', foundError), testCase('never', 't.regex(a, /a/, \'message\');', foundError), testCase('never', 't.notRegex(a, /a/, \'message\');', foundError), testCase('never', 't.ifError(new Error(), \'message\');', foundError), From 88a5a917647a60ca8d2fb930f65359fafa043f8a Mon Sep 17 00:00:00 2001 From: ninevra Date: Wed, 15 Jul 2020 14:18:55 -0700 Subject: [PATCH 17/27] Support t.assert() --- rules/assertion-arguments.js | 4 ++++ test/assertion-arguments.js | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/rules/assertion-arguments.js b/rules/assertion-arguments.js index 25d243e7..a5d179d2 100644 --- a/rules/assertion-arguments.js +++ b/rules/assertion-arguments.js @@ -5,6 +5,10 @@ const util = require('../util'); const createAvaRule = require('../create-ava-rule'); const expectedNbArguments = { + assert: { + min: 1, + max: 2 + }, deepEqual: { min: 2, max: 3 diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index c34fbaf3..288219cb 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -157,6 +157,7 @@ const dynamics = [ ruleTester.run('assertion-arguments', rule, { valid: [ testCase(false, 't.plan(1);'), + testCase(false, 't.assert(true, \'message\');'), testCase(false, 't.deepEqual({}, {}, \'message\');'), testCase(false, 't.fail(\'message\');'), testCase(false, 't.false(false, \'message\');'), @@ -185,6 +186,7 @@ ruleTester.run('assertion-arguments', rule, { // Shouldn't be triggered since it's not a test file testCase(false, 't.true(true);', false, {useHeader: false}), + testCase(false, 't.assert(true);'), testCase(false, 't.deepEqual({}, {});'), testCase(false, 't.fail();'), testCase(false, 't.false(false);'), @@ -216,6 +218,7 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.foo(1, 2, 3, 4);'), testCase('always', 't.plan(1);'), + testCase('always', 't.assert(true, \'message\');'), testCase('always', 't.pass(\'message\');'), testCase('always', 't.fail(\'message\');'), testCase('always', 't.truthy(\'unicorn\', \'message\');'), @@ -251,6 +254,7 @@ ruleTester.run('assertion-arguments', rule, { testCase('always', 't.foo(1, 2, 3, 4);'), testCase('never', 't.plan(1);'), + testCase('never', 't.assert(true);'), testCase('never', 't.pass();'), testCase('never', 't.fail();'), testCase('never', 't.truthy(\'unicorn\');'), @@ -318,6 +322,7 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.throws(() => {}, null, "message");'), testCase(false, 't.throwsAsync(async () => {}, {name: \'TypeError\'});'), testCase(false, 't.throwsAsync(async () => {}, null, "message");'), + testCase(false, 't.assert(dynamic === \'static\');'), testCase(false, 't.true(dynamic === \'static\');'), testCase(false, 't.false(dynamic === \'static\');'), testCase(false, 't.truthy(dynamic === \'static\');'), @@ -331,6 +336,7 @@ ruleTester.run('assertion-arguments', rule, { invalid: [ // Not enough arguments testCase(false, 't.plan();', tooFewError(1)), + testCase(false, 't.assert();', tooFewError(1)), testCase(false, 't.truthy();', tooFewError(1)), testCase(false, 't.falsy();', tooFewError(1)), testCase(false, 't.true();', tooFewError(1)), @@ -356,6 +362,7 @@ ruleTester.run('assertion-arguments', rule, { // Too many arguments testCase(false, 't.plan(1, \'extra argument\');', tooManyError(1)), + testCase(false, 't.assert(true, \'message\', \'extra argument\');', tooManyError(2)), testCase(false, 't.pass(\'message\', \'extra argument\');', tooManyError(1)), testCase(false, 't.fail(\'message\', \'extra argument\');', tooManyError(1)), testCase(false, 't.truthy(\'unicorn\', \'message\', \'extra argument\');', tooManyError(2)), @@ -380,6 +387,7 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.teardown(() => {}, \'extra argument\');', tooManyError(1)), testCase(false, 't.timeout(1, \'message\', \'extra argument\');', tooManyError(2)), + testCase('always', 't.assert(true);', missingError), testCase('always', 't.pass();', missingError), testCase('always', 't.fail();', missingError), testCase('always', 't.truthy(\'unicorn\');', missingError), @@ -403,6 +411,8 @@ ruleTester.run('assertion-arguments', rule, { testCase('always', 't.skip.is(\'same\', \'same\');', missingError), testCase('always', 't.is.skip(\'same\', \'same\');', missingError), testCase('always', 't.snapshot(value);', missingError), + + testCase('never', 't.assert(true, \'message\');', foundError), testCase('never', 't.pass(\'message\');', foundError), testCase('never', 't.fail(\'message\');', foundError), testCase('never', 't.truthy(\'unicorn\', \'message\');', foundError), @@ -477,6 +487,10 @@ ruleTester.run('assertion-arguments', rule, { outOfOrderError(1, 13, 1, 25), {output: 't.deepEqual((actual), {});'} ), + testCase(false, 't.assert(\'static\' !== dynamic);', + outOfOrderError(1, 10, 1, 30), + {output: 't.assert(dynamic !== \'static\');'} + ), testCase(false, 't.true(\'static\' <= dynamic);', outOfOrderError(1, 8, 1, 27), {output: 't.true(dynamic >= \'static\');'} From 9724e19e1eb577e74b7c994f5edd39356369288c Mon Sep 17 00:00:00 2001 From: ninevra Date: Wed, 15 Jul 2020 14:04:43 -0700 Subject: [PATCH 18/27] Test not autofixing nodes with comments --- test/assertion-arguments.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index 288219cb..c5e65e02 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -487,6 +487,19 @@ ruleTester.run('assertion-arguments', rule, { outOfOrderError(1, 13, 1, 25), {output: 't.deepEqual((actual), {});'} ), + testCase(false, 't.deepEqual({}, actual/*: type */);', + outOfOrderError(1, 13, 1, 34) + ), + testCase( + false, + `t.deepEqual(// Line comment 1 + 'static' // Line Comment 2 + , // Line Comment 3 + dynamic // Line Comment 4 + // Line Comment 5 + ); // Line Comment 6`, + outOfOrderError(1, 13, 5, 22) + ), testCase(false, 't.assert(\'static\' !== dynamic);', outOfOrderError(1, 10, 1, 30), {output: 't.assert(dynamic !== \'static\');'} @@ -507,6 +520,9 @@ ruleTester.run('assertion-arguments', rule, { outOfOrderError(1, 9, 1, 28), {output: 't.falsy(dynamic <= \'static\');'} ), + testCase(false, 't.true(\'static\' === actual/*: type */);', + outOfOrderError(1, 8, 1, 38) + ), ...statics.map(expression => testCase(false, `t.deepEqual(${expression}, dynamic);`, outOfOrderError(1, 13, 1, 22 + expression.length), From 61f328d76ae59175622ff2de2aae2f6794dc4f21 Mon Sep 17 00:00:00 2001 From: ninevra Date: Wed, 15 Jul 2020 14:05:03 -0700 Subject: [PATCH 19/27] Don't autofix nodes with comments --- rules/assertion-arguments.js | 49 +++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/rules/assertion-arguments.js b/rules/assertion-arguments.js index a5d179d2..5c43e31d 100644 --- a/rules/assertion-arguments.js +++ b/rules/assertion-arguments.js @@ -146,8 +146,14 @@ function * sourceRangesOfArguments(sourceCode, callExpression) { callExpression.arguments[index], {filter: token => isCommaToken(token)} ); - const firstToken = sourceCode.getTokenAfter(previousToken); - const lastToken = sourceCode.getTokenBefore(nextToken); + const firstToken = sourceCode.getTokenAfter( + previousToken, + {includeComments: true} + ); + const lastToken = sourceCode.getTokenBefore( + nextToken, + {includeComments: true} + ); yield [firstToken.start, lastToken.end]; } } @@ -172,6 +178,13 @@ function sourceOfBinaryExpressionComponents(sourceCode, node) { return [leftRange, operatorToken, rightRange]; } +function noComments(sourceCode, ...nodes) { + return nodes.every(node => { + const {leading, trailing} = sourceCode.getComments(node); + return leading.length === 0 && trailing.length === 0; + }); +} + const create = context => { const ava = createAvaRule(); const options = context.options[0] || {}; @@ -240,32 +253,36 @@ const create = context => { if (actualExpectedAssertions.has(members[0]) && gottenArgs >= 2) { const [leftNode, rightNode] = node.arguments; + const sourceCode = context.getSourceCode(); if (isStatic(leftNode) && !isStatic(rightNode)) { - const sourceCode = context.getSourceCode(); const [leftRange, rightRange] = sourceRangesOfArguments(sourceCode, node); - context.report({ + const report = { message: 'Expected values should come after actual values.', loc: { start: sourceCode.getLocFromIndex(leftRange[0]), end: sourceCode.getLocFromIndex(rightRange[1]) - }, - fix(fixer) { + } + }; + if (noComments(sourceCode, leftNode, rightNode)) { + report.fix = fixer => { const leftText = sourceCode.getText().slice(...leftRange); const rightText = sourceCode.getText().slice(...rightRange); return [ fixer.replaceTextRange(leftRange, rightText), fixer.replaceTextRange(rightRange, leftText) ]; - } - }); + }; + } + + context.report(report); } } else if (relationalActualExpectedAssertions.has(members[0]) && gottenArgs >= 1) { const argument = node.arguments[0]; if (argument.type === 'BinaryExpression' && comparisonOperators.has(argument.operator)) { const leftNode = argument.left; const rightNode = argument.right; + const sourceCode = context.getSourceCode(); if (isStatic(leftNode) && !isStatic(rightNode)) { - const sourceCode = context.getSourceCode(); const [ leftRange, operatorToken, @@ -273,20 +290,24 @@ const create = context => { ] = sourceOfBinaryExpressionComponents(sourceCode, argument); const rightText = sourceCode.getText().slice(...rightRange); const leftText = sourceCode.getText().slice(...leftRange); - context.report({ + const report = { message: 'Expected values should come after actual values.', loc: { start: sourceCode.getLocFromIndex(leftRange[0]), end: sourceCode.getLocFromIndex(rightRange[1]) - }, - fix(fixer) { + } + }; + if (noComments(sourceCode, leftNode, rightNode, argument)) { + report.fix = fixer => { return [ fixer.replaceTextRange(leftRange, rightText), fixer.replaceText(operatorToken, flipOperator(argument.operator)), fixer.replaceTextRange(rightRange, leftText) ]; - } - }); + }; + } + + context.report(report); } } } From ee539aa48a119c7d77f6dbc21fd8b6c02930fa5e Mon Sep 17 00:00:00 2001 From: ninevra Date: Wed, 15 Jul 2020 16:06:49 -0700 Subject: [PATCH 20/27] Document not autofixing nodes with comments --- docs/rules/assertion-arguments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/assertion-arguments.md b/docs/rules/assertion-arguments.md index 14fd84a8..62c046e3 100644 --- a/docs/rules/assertion-arguments.md +++ b/docs/rules/assertion-arguments.md @@ -6,7 +6,7 @@ Enforces passing the right number of arguments to assertion methods like `t.is() Assertion messages are optional arguments that can be given to any assertion call to improve the error message, should the assertion fail. -This rule also attempts to enforce passing the actual and expected values in the correct order. If exactly one of the first two arguments to a two-argument assertion is a static expression such as `{a: 1}`, then this rule requires that the static expression come second. (`t.regex()` and `t.notRegex()` are excluded from this check, because either their `contents` argument or their `regex` argument could plausibly be the actual or expected value.) If the argument to a one-argument assertion is a binary relation such as `'static' === dynamic`, a similar check is performed on its left- and right-hand sides. Errors of these kinds are fixable. +This rule also attempts to enforce passing the actual and expected values in the correct order. If exactly one of the first two arguments to a two-argument assertion is a static expression such as `{a: 1}`, then this rule requires that the static expression come second. (`t.regex()` and `t.notRegex()` are excluded from this check, because either their `contents` argument or their `regex` argument could plausibly be the actual or expected value.) If the argument to a one-argument assertion is a binary relation such as `'static' === dynamic`, a similar check is performed on its left- and right-hand sides. Errors of these kinds are usually fixable. ## Fail From b9f29f66ccb7431b216e023157ca562d11f47238 Mon Sep 17 00:00:00 2001 From: ninevra Date: Wed, 15 Jul 2020 19:46:06 -0700 Subject: [PATCH 21/27] Refactor to reduce cyclomatic complexity Fixes eslint complexity warning on create(). --- rules/assertion-arguments.js | 140 ++++++++++++++++++++--------------- 1 file changed, 80 insertions(+), 60 deletions(-) diff --git a/rules/assertion-arguments.js b/rules/assertion-arguments.js index 5c43e31d..85b95409 100644 --- a/rules/assertion-arguments.js +++ b/rules/assertion-arguments.js @@ -251,71 +251,91 @@ const create = context => { } } - if (actualExpectedAssertions.has(members[0]) && gottenArgs >= 2) { - const [leftNode, rightNode] = node.arguments; - const sourceCode = context.getSourceCode(); - if (isStatic(leftNode) && !isStatic(rightNode)) { - const [leftRange, rightRange] = sourceRangesOfArguments(sourceCode, node); - const report = { - message: 'Expected values should come after actual values.', - loc: { - start: sourceCode.getLocFromIndex(leftRange[0]), - end: sourceCode.getLocFromIndex(rightRange[1]) - } - }; - if (noComments(sourceCode, leftNode, rightNode)) { - report.fix = fixer => { - const leftText = sourceCode.getText().slice(...leftRange); - const rightText = sourceCode.getText().slice(...rightRange); - return [ - fixer.replaceTextRange(leftRange, rightText), - fixer.replaceTextRange(rightRange, leftText) - ]; - }; - } - - context.report(report); - } - } else if (relationalActualExpectedAssertions.has(members[0]) && gottenArgs >= 1) { - const argument = node.arguments[0]; - if (argument.type === 'BinaryExpression' && comparisonOperators.has(argument.operator)) { - const leftNode = argument.left; - const rightNode = argument.right; - const sourceCode = context.getSourceCode(); - if (isStatic(leftNode) && !isStatic(rightNode)) { - const [ - leftRange, - operatorToken, - rightRange - ] = sourceOfBinaryExpressionComponents(sourceCode, argument); - const rightText = sourceCode.getText().slice(...rightRange); - const leftText = sourceCode.getText().slice(...leftRange); - const report = { - message: 'Expected values should come after actual values.', - loc: { - start: sourceCode.getLocFromIndex(leftRange[0]), - end: sourceCode.getLocFromIndex(rightRange[1]) - } - }; - if (noComments(sourceCode, leftNode, rightNode, argument)) { - report.fix = fixer => { - return [ - fixer.replaceTextRange(leftRange, rightText), - fixer.replaceText(operatorToken, flipOperator(argument.operator)), - fixer.replaceTextRange(rightRange, leftText) - ]; - }; - } - - context.report(report); - } - } - } + checkArgumentOrder({node, assertion: members[0], context}); } }) }); }; +function checkArgumentOrder({node, assertion, context}) { + const [first, second] = node.arguments; + if (actualExpectedAssertions.has(assertion) && second) { + const [leftNode, rightNode] = [first, second]; + if (isStatic(leftNode) && !isStatic(rightNode)) { + context.report( + makeOutOfOrder2ArgumentReport({node, leftNode, rightNode, context}) + ); + } + } else if ( + relationalActualExpectedAssertions.has(assertion) && + first && + first.type === 'BinaryExpression' && + comparisonOperators.has(first.operator) + ) { + const [leftNode, rightNode] = [first.left, first.right]; + if (isStatic(leftNode) && !isStatic(rightNode)) { + context.report( + makeOutOfOrder1ArgumentReport({node: first, leftNode, rightNode, context}) + ); + } + } +} + +function makeOutOfOrder2ArgumentReport({node, leftNode, rightNode, context}) { + const sourceCode = context.getSourceCode(); + const [leftRange, rightRange] = sourceRangesOfArguments(sourceCode, node); + const report = { + message: 'Expected values should come after actual values.', + loc: { + start: sourceCode.getLocFromIndex(leftRange[0]), + end: sourceCode.getLocFromIndex(rightRange[1]) + } + }; + + if (noComments(sourceCode, leftNode, rightNode)) { + report.fix = fixer => { + const leftText = sourceCode.getText().slice(...leftRange); + const rightText = sourceCode.getText().slice(...rightRange); + return [ + fixer.replaceTextRange(leftRange, rightText), + fixer.replaceTextRange(rightRange, leftText) + ]; + }; + } + + return report; +} + +function makeOutOfOrder1ArgumentReport({node, leftNode, rightNode, context}) { + const sourceCode = context.getSourceCode(); + const [ + leftRange, + operatorToken, + rightRange + ] = sourceOfBinaryExpressionComponents(sourceCode, node); + const report = { + message: 'Expected values should come after actual values.', + loc: { + start: sourceCode.getLocFromIndex(leftRange[0]), + end: sourceCode.getLocFromIndex(rightRange[1]) + } + }; + + if (noComments(sourceCode, leftNode, rightNode, node)) { + report.fix = fixer => { + const leftText = sourceCode.getText().slice(...leftRange); + const rightText = sourceCode.getText().slice(...rightRange); + return [ + fixer.replaceTextRange(leftRange, rightText), + fixer.replaceText(operatorToken, flipOperator(node.operator)), + fixer.replaceTextRange(rightRange, leftText) + ]; + }; + } + + return report; +} + const schema = [{ type: 'object', properties: { From bd959e173a0f23b766750b5eb4261db3c5ab665b Mon Sep 17 00:00:00 2001 From: ninevra Date: Thu, 16 Jul 2020 15:08:18 -0700 Subject: [PATCH 22/27] Reword documentation --- docs/rules/assertion-arguments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/assertion-arguments.md b/docs/rules/assertion-arguments.md index 62c046e3..686f2ff3 100644 --- a/docs/rules/assertion-arguments.md +++ b/docs/rules/assertion-arguments.md @@ -6,7 +6,7 @@ Enforces passing the right number of arguments to assertion methods like `t.is() Assertion messages are optional arguments that can be given to any assertion call to improve the error message, should the assertion fail. -This rule also attempts to enforce passing the actual and expected values in the correct order. If exactly one of the first two arguments to a two-argument assertion is a static expression such as `{a: 1}`, then this rule requires that the static expression come second. (`t.regex()` and `t.notRegex()` are excluded from this check, because either their `contents` argument or their `regex` argument could plausibly be the actual or expected value.) If the argument to a one-argument assertion is a binary relation such as `'static' === dynamic`, a similar check is performed on its left- and right-hand sides. Errors of these kinds are usually fixable. +This rule also attempts to enforce passing actual values before expected values. If exactly one of the first two arguments to a two-argument assertion is a static expression such as `{a: 1}`, then the static expression must come second. (`t.regex()` and `t.notRegex()` are excluded from this check, because either their `contents` argument or their `regex` argument could plausibly be the actual or expected value.) If the argument to a one-argument assertion is a binary relation such as `'static' === dynamic`, a similar check is performed on its left- and right-hand sides. Errors of these kinds are usually fixable. ## Fail From c08dea6732ac825043154f1799f44d406d80069b Mon Sep 17 00:00:00 2001 From: ninevra Date: Tue, 14 Jul 2020 21:58:58 -0700 Subject: [PATCH 23/27] Test t.like() support --- test/assertion-arguments.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js index c5e65e02..5c256cce 100644 --- a/test/assertion-arguments.js +++ b/test/assertion-arguments.js @@ -316,6 +316,7 @@ ruleTester.run('assertion-arguments', rule, { testCase(false, 't.deepEqual(dynamic, \'static\');'), testCase(false, 't.deepEqual(dynamic, dynamic);'), testCase(false, 't.is(dynamic, \'static\');'), + testCase(false, 't.like(dynamic, \'static\');'), testCase(false, 't.not(dynamic, \'static\');'), testCase(false, 't.notDeepEqual(dynamic, \'static\');'), testCase(false, 't.throws(() => {}, expected);'), @@ -448,6 +449,10 @@ ruleTester.run('assertion-arguments', rule, { outOfOrderError(1, 6, 1, 23), {output: 't.is(dynamic, \'static\');'} ), + testCase(false, 't.like({a: {b: 1}}, dynamic);', + outOfOrderError(1, 8, 1, 28), + {output: 't.like(dynamic, {a: {b: 1}});'} + ), testCase(false, 't.not(\'static\', dynamic);', outOfOrderError(1, 7, 1, 24), {output: 't.not(dynamic, \'static\');'} From 729d5bd9ad26cb2c5a1b0649836ddfd92a2c0ce8 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Mon, 10 Aug 2020 14:08:22 +0200 Subject: [PATCH 24/27] Update assertion-arguments.js --- rules/assertion-arguments.js | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/rules/assertion-arguments.js b/rules/assertion-arguments.js index 85b95409..edc04b47 100644 --- a/rules/assertion-arguments.js +++ b/rules/assertion-arguments.js @@ -100,11 +100,21 @@ const expectedNbArguments = { }; const actualExpectedAssertions = new Set([ - 'deepEqual', 'is', 'like', 'not', 'notDeepEqual', 'throws', 'throwsAsync' + 'deepEqual', + 'is', + 'like', + 'not', + 'notDeepEqual', + 'throws', + 'throwsAsync' ]); const relationalActualExpectedAssertions = new Set([ - 'assert', 'truthy', 'falsy', 'true', 'false' + 'assert', + 'truthy', + 'falsy', + 'true', + 'false' ]); const comparisonOperators = new Map([ @@ -118,9 +128,7 @@ const comparisonOperators = new Map([ ['<', '>'] ]); -const flipOperator = operator => { - return comparisonOperators.get(operator); -}; +const flipOperator = operator => comparisonOperators.get(operator); function isStatic(node) { const staticValue = getStaticValue(node); @@ -132,7 +140,9 @@ function * sourceRangesOfArguments(sourceCode, callExpression) { callExpression.callee, {filter: token => isOpeningParenToken(token)} ); + const closingParen = sourceCode.getLastToken(callExpression); + for (let index = 0; index < callExpression.arguments.length; index++) { const previousToken = index === 0 ? openingParen : @@ -140,41 +150,50 @@ function * sourceRangesOfArguments(sourceCode, callExpression) { callExpression.arguments[index], {filter: token => isCommaToken(token)} ); + const nextToken = index === callExpression.arguments.length - 1 ? closingParen : sourceCode.getTokenAfter( callExpression.arguments[index], {filter: token => isCommaToken(token)} ); + const firstToken = sourceCode.getTokenAfter( previousToken, {includeComments: true} ); + const lastToken = sourceCode.getTokenBefore( nextToken, {includeComments: true} ); + yield [firstToken.start, lastToken.end]; } } function sourceOfBinaryExpressionComponents(sourceCode, node) { const {operator, left, right} = node; + const operatorToken = sourceCode.getFirstTokenBetween( left, right, {filter: token => token.value === operator} ); + const previousToken = sourceCode.getTokenBefore(node); const nextToken = sourceCode.getTokenAfter(node); + const leftRange = [ sourceCode.getTokenAfter(previousToken, {includeComments: true}).start, sourceCode.getTokenBefore(operatorToken, {includeComments: true}).end ]; + const rightRange = [ sourceCode.getTokenAfter(operatorToken, {includeComments: true}).start, sourceCode.getTokenBefore(nextToken, {includeComments: true}).end ]; + return [leftRange, operatorToken, rightRange]; } From 93425a37882bdd326eeca2d620055e8f0c0c1852 Mon Sep 17 00:00:00 2001 From: ninevra Date: Mon, 10 Aug 2020 09:57:03 -0700 Subject: [PATCH 25/27] Replace uses of token#start, token#end --- rules/assertion-arguments.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rules/assertion-arguments.js b/rules/assertion-arguments.js index edc04b47..6ea0cf39 100644 --- a/rules/assertion-arguments.js +++ b/rules/assertion-arguments.js @@ -168,7 +168,7 @@ function * sourceRangesOfArguments(sourceCode, callExpression) { {includeComments: true} ); - yield [firstToken.start, lastToken.end]; + yield [firstToken.range[0], lastToken.range[1]]; } } @@ -185,13 +185,13 @@ function sourceOfBinaryExpressionComponents(sourceCode, node) { const nextToken = sourceCode.getTokenAfter(node); const leftRange = [ - sourceCode.getTokenAfter(previousToken, {includeComments: true}).start, - sourceCode.getTokenBefore(operatorToken, {includeComments: true}).end + sourceCode.getTokenAfter(previousToken, {includeComments: true}).range[0], + sourceCode.getTokenBefore(operatorToken, {includeComments: true}).range[1] ]; const rightRange = [ - sourceCode.getTokenAfter(operatorToken, {includeComments: true}).start, - sourceCode.getTokenBefore(nextToken, {includeComments: true}).end + sourceCode.getTokenAfter(operatorToken, {includeComments: true}).range[0], + sourceCode.getTokenBefore(nextToken, {includeComments: true}).range[1] ]; return [leftRange, operatorToken, rightRange]; From 997096ef47baf40a8cd7d4f6c0f3189743d0e943 Mon Sep 17 00:00:00 2001 From: ninevra Date: Mon, 10 Aug 2020 10:26:23 -0700 Subject: [PATCH 26/27] Use for...of --- rules/assertion-arguments.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rules/assertion-arguments.js b/rules/assertion-arguments.js index 6ea0cf39..1e7ff1b4 100644 --- a/rules/assertion-arguments.js +++ b/rules/assertion-arguments.js @@ -143,18 +143,20 @@ function * sourceRangesOfArguments(sourceCode, callExpression) { const closingParen = sourceCode.getLastToken(callExpression); - for (let index = 0; index < callExpression.arguments.length; index++) { + for (const [index, argument] of callExpression.arguments.map( + (argument, index) => [index, argument]) + ) { const previousToken = index === 0 ? openingParen : sourceCode.getTokenBefore( - callExpression.arguments[index], + argument, {filter: token => isCommaToken(token)} ); const nextToken = index === callExpression.arguments.length - 1 ? closingParen : sourceCode.getTokenAfter( - callExpression.arguments[index], + argument, {filter: token => isCommaToken(token)} ); From fbc95b5cbcf0343227c926822311367aacd390ef Mon Sep 17 00:00:00 2001 From: ninevra Date: Mon, 10 Aug 2020 11:07:03 -0700 Subject: [PATCH 27/27] Refactor for...of --- rules/assertion-arguments.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rules/assertion-arguments.js b/rules/assertion-arguments.js index 1e7ff1b4..f668e780 100644 --- a/rules/assertion-arguments.js +++ b/rules/assertion-arguments.js @@ -143,9 +143,7 @@ function * sourceRangesOfArguments(sourceCode, callExpression) { const closingParen = sourceCode.getLastToken(callExpression); - for (const [index, argument] of callExpression.arguments.map( - (argument, index) => [index, argument]) - ) { + for (const [index, argument] of callExpression.arguments.entries()) { const previousToken = index === 0 ? openingParen : sourceCode.getTokenBefore(