eslint-plugin-ava
Advanced tools
Comparing version 11.0.0 to 12.0.0
@@ -13,3 +13,3 @@ 'use strict'; | ||
parserOptions: { | ||
ecmaVersion: 2020, | ||
ecmaVersion: 2021, | ||
sourceType: 'module' | ||
@@ -16,0 +16,0 @@ }, |
{ | ||
"name": "eslint-plugin-ava", | ||
"version": "11.0.0", | ||
"version": "12.0.0", | ||
"description": "ESLint rules for AVA", | ||
@@ -36,27 +36,27 @@ "license": "MIT", | ||
"eslint-utils": "^2.1.0", | ||
"espree": "^7.2.0", | ||
"espree": "^7.3.1", | ||
"espurify": "^2.0.1", | ||
"import-modules": "^2.0.0", | ||
"import-modules": "^2.1.0", | ||
"micro-spelling-correcter": "^1.1.1", | ||
"pkg-dir": "^4.2.0", | ||
"pkg-dir": "^5.0.0", | ||
"resolve-from": "^5.0.0" | ||
}, | ||
"devDependencies": { | ||
"ava": "^3.11.1", | ||
"ava": "^3.15.0", | ||
"babel-eslint": "^10.1.0", | ||
"c8": "^7.3.0", | ||
"c8": "^7.6.0", | ||
"chalk": "^4.1.0", | ||
"del": "^5.1.0", | ||
"eslint": "7.7.0", | ||
"del": "^6.0.0", | ||
"eslint": "^7.8.1", | ||
"eslint-ava-rule-tester": "^4.0.0", | ||
"eslint-plugin-eslint-plugin": "^2.3.0", | ||
"execa": "^4.0.3", | ||
"execa": "^5.0.0", | ||
"listr": "^0.14.3", | ||
"outdent": "^0.7.1", | ||
"outdent": "^0.8.0", | ||
"pify": "^5.0.0", | ||
"tempy": "^0.6.0", | ||
"xo": "^0.33.0" | ||
"tempy": "^1.0.1", | ||
"xo": "^0.38.2" | ||
}, | ||
"peerDependencies": { | ||
"eslint": ">=7.7.0" | ||
"eslint": ">=7.22.0" | ||
}, | ||
@@ -63,0 +63,0 @@ "ava": { |
@@ -1,6 +0,6 @@ | ||
# eslint-plugin-ava [![Build Status](https://travis-ci.org/avajs/eslint-plugin-ava.svg?branch=master)](https://travis-ci.org/avajs/eslint-plugin-ava) [![Coverage Status](https://coveralls.io/repos/github/avajs/eslint-plugin-ava/badge.svg?branch=master)](https://coveralls.io/github/avajs/eslint-plugin-ava?branch=master) | ||
# eslint-plugin-ava [![Coverage Status](https://coveralls.io/repos/github/avajs/eslint-plugin-ava/badge.svg?branch=main)](https://coveralls.io/github/avajs/eslint-plugin-ava?branch=main) | ||
> ESLint rules for [AVA](https://ava.li) | ||
> ESLint rules for [AVA](https://avajs.dev) | ||
Translations: [Français](https://github.com/avajs/ava-docs/blob/master/fr_FR/related/eslint-plugin-ava/readme.md) | ||
Translations: [Français](https://github.com/avajs/ava-docs/blob/main/fr_FR/related/eslint-plugin-ava/readme.md) | ||
@@ -11,3 +11,2 @@ This plugin is bundled in [XO](https://github.com/xojs/xo). No need to do anything if you're using it. | ||
## Install | ||
@@ -19,3 +18,2 @@ | ||
## Usage | ||
@@ -33,3 +31,3 @@ | ||
"parserOptions": { | ||
"ecmaVersion": 2020, | ||
"ecmaVersion": 2021, | ||
"sourceType": "module" | ||
@@ -80,3 +78,2 @@ }, | ||
## Rules | ||
@@ -99,5 +96,5 @@ | ||
- [no-nested-tests](docs/rules/no-nested-tests.md) - Ensure no tests are nested. | ||
- [no-only-test](docs/rules/no-only-test.md) - Ensure no `test.only()` are present. *(fixable)* | ||
- [no-only-test](docs/rules/no-only-test.md) - Ensure no `test.only()` are present. | ||
- [no-skip-assert](docs/rules/no-skip-assert.md) - Ensure no assertions are skipped. | ||
- [no-skip-test](docs/rules/no-skip-test.md) - Ensure no tests are skipped. *(fixable)* | ||
- [no-skip-test](docs/rules/no-skip-test.md) - Ensure no tests are skipped. | ||
- [no-statement-after-end](docs/rules/no-statement-after-end.md) - Ensure `t.end()` is the last statement executed. | ||
@@ -119,3 +116,2 @@ - [no-todo-implementation](docs/rules/no-todo-implementation.md) - Ensure `test.todo()` is not given an implementation function. | ||
## Recommended config | ||
@@ -122,0 +118,0 @@ |
'use strict'; | ||
const {visitIf} = require('enhance-visitors'); | ||
const {getStaticValue, isOpeningParenToken, isCommaToken} = require('eslint-utils'); | ||
const {getStaticValue, isOpeningParenToken, isCommaToken, findVariable} = require('eslint-utils'); | ||
const util = require('../util'); | ||
@@ -206,2 +206,9 @@ const createAvaRule = require('../create-ava-rule'); | ||
function isString(node) { | ||
const {type} = node; | ||
return type === 'TemplateLiteral' || | ||
type === 'TaggedTemplateExpression' || | ||
(type === 'Literal' && typeof node.value === 'string'); | ||
} | ||
const create = context => { | ||
@@ -275,2 +282,20 @@ const ava = createAvaRule(); | ||
} | ||
if (gottenArgs === nArgs.max && nArgs.min !== nArgs.max) { | ||
let lastArg = node.arguments[node.arguments.length - 1]; | ||
if (lastArg.type === 'Identifier') { | ||
const variable = findVariable(context.getScope(), lastArg); | ||
let value; | ||
for (const ref of variable.references) { | ||
value = ref.writeExpr || value; | ||
} | ||
lastArg = value; | ||
} | ||
if (!isString(lastArg)) { | ||
report(node, 'Assertion message should be a string.'); | ||
} | ||
} | ||
}) | ||
@@ -277,0 +302,0 @@ }); |
@@ -92,3 +92,3 @@ 'use strict'; | ||
// TODO: Remove `.reduce()` usage. | ||
// eslint-disable-next-line unicorn/no-reduce | ||
// eslint-disable-next-line unicorn/no-array-reduce | ||
const selectors = checks.reduce((result, check) => { | ||
@@ -95,0 +95,0 @@ result[check.selector] = visitIf([ |
@@ -33,3 +33,3 @@ 'use strict'; | ||
// TODO: Remove `.reduce()` usage. | ||
// eslint-disable-next-line unicorn/no-reduce | ||
// eslint-disable-next-line unicorn/no-array-reduce | ||
testModifiers.reduce((previous, current) => { | ||
@@ -36,0 +36,0 @@ if (previous.name === current.name) { |
@@ -19,9 +19,12 @@ 'use strict'; | ||
message: '`test.only()` should not be used.', | ||
fix: fixer => { | ||
return fixer.replaceTextRange.apply(null, util.removeTestModifier({ | ||
modifier: 'only', | ||
node, | ||
context | ||
})); | ||
} | ||
suggest: [{ | ||
desc: 'Remove the `.only`', | ||
fix: fixer => { | ||
return fixer.replaceTextRange.apply(null, util.removeTestModifier({ | ||
modifier: 'only', | ||
node, | ||
context | ||
})); | ||
} | ||
}] | ||
}); | ||
@@ -28,0 +31,0 @@ } |
@@ -19,9 +19,12 @@ 'use strict'; | ||
message: 'No tests should be skipped.', | ||
fix: fixer => { | ||
return fixer.replaceTextRange.apply(null, util.removeTestModifier({ | ||
modifier: 'skip', | ||
node, | ||
context | ||
})); | ||
} | ||
suggest: [{ | ||
desc: 'Remove the `.skip`', | ||
fix: fixer => { | ||
return fixer.replaceTextRange.apply(null, util.removeTestModifier({ | ||
modifier: 'skip', | ||
node, | ||
context | ||
})); | ||
} | ||
}] | ||
}); | ||
@@ -28,0 +31,0 @@ } |
@@ -24,6 +24,15 @@ 'use strict'; | ||
function pathStart() { | ||
if (currentSegmentInfo !== undefined) { | ||
segmentInfoStack.push(currentSegmentInfo); | ||
currentSegmentInfo = undefined; | ||
} | ||
} | ||
function pathEnd() { | ||
currentSegmentInfo = segmentInfoStack.pop(); | ||
} | ||
function segmentStart(segment) { | ||
// A new CodePathSegment has started, create an "info" object to track this segments state. | ||
segmentInfoStack.push(currentSegmentInfo); | ||
currentSegmentInfo = { | ||
@@ -38,7 +47,7 @@ ended: false, | ||
function segmentEnd() { | ||
currentSegmentInfo = segmentInfoStack.pop(); | ||
currentSegmentInfo = undefined; | ||
} | ||
function checkForEndExpression(node) { | ||
if (isEndExpression(node)) { | ||
if (isEndExpression(node) && currentSegmentInfo !== undefined) { | ||
currentSegmentInfo.ended = true; | ||
@@ -53,6 +62,10 @@ } | ||
const ended = [currentSegmentInfo] | ||
.concat(currentSegmentInfo.prev) | ||
.filter(info => info.ended); | ||
// If there is no current segment (this occurs in unreachable code), then we | ||
// can't check whether `t.end()` was called | ||
if (currentSegmentInfo === undefined) { | ||
return; | ||
} | ||
const ended = [currentSegmentInfo, ...currentSegmentInfo.prev].filter(info => info.ended); | ||
// If this segment or any previous segment is already ended, further statements are not allowed, report as an error. | ||
@@ -91,2 +104,4 @@ if (ended.length > 0) { | ||
}, | ||
onCodePathStart: pathStart, | ||
onCodePathEnd: pathEnd, | ||
onCodePathSegmentStart: segmentStart, | ||
@@ -93,0 +108,0 @@ onCodePathSegmentEnd: segmentEnd, |
@@ -17,3 +17,3 @@ 'use strict'; | ||
node, | ||
message: '`test.todo()` should be not be used.' | ||
message: '`test.todo()` should not be used.' | ||
}); | ||
@@ -20,0 +20,0 @@ } |
@@ -33,3 +33,3 @@ 'use strict'; | ||
if (unknown.length !== 0) { | ||
if (unknown.length > 0) { | ||
context.report({ | ||
@@ -36,0 +36,0 @@ node: unknown[0], |
@@ -16,71 +16,182 @@ 'use strict'; | ||
const equalityTests = new Set([ | ||
'is', | ||
'deepEqual' | ||
]); | ||
// Find the latest reference to the given identifier's name. | ||
const findReference = name => { | ||
const reference = context.getScope().references.find(reference => reference.identifier.name === name); | ||
const definitions = reference.resolved.defs; | ||
return definitions[definitions.length - 1].node; | ||
}; | ||
return ava.merge({ | ||
CallExpression: visitIf([ | ||
ava.isInTestFile, | ||
ava.isInTestNode | ||
])(node => { | ||
// Call a boolean assertion, for example, `t.true`, `t.false`, … | ||
const isBooleanAssertion = node.callee.type === 'MemberExpression' && | ||
booleanTests.has(node.callee.property.name) && | ||
util.getNameOfRootNodeObject(node.callee) === 't'; | ||
if (reference && reference.resolved) { | ||
const definitions = reference.resolved.defs; | ||
if (!isBooleanAssertion) { | ||
if (definitions.length === 0) { | ||
return; | ||
} | ||
const firstArg = node.arguments[0]; | ||
return definitions[definitions.length - 1].node; | ||
} | ||
}; | ||
// First argument is a call expression | ||
const isFunctionCall = firstArg.type === 'CallExpression'; | ||
if (!isFunctionCall || !firstArg.callee.property) { | ||
return; | ||
} | ||
/* | ||
Recursively find the "origin" node of the given node. | ||
const {name} = firstArg.callee.property; | ||
let lookup = {}; | ||
let variable = {}; | ||
Note: `context.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`) | ||
if (name === 'test') { | ||
// `lookup.test(variable)` | ||
lookup = firstArg.callee.object; | ||
variable = firstArg.arguments[0]; | ||
} else if (['search', 'match'].includes(name)) { | ||
// `variable.match(lookup)` | ||
lookup = firstArg.arguments[0]; | ||
variable = firstArg.callee.object; | ||
} | ||
``` | ||
const test = require('ava'); | ||
let isRegExp = lookup.regex; | ||
let x = 0; | ||
let y = x; | ||
// It's not a regexp but an identifier | ||
if (!isRegExp && lookup.type === 'Identifier') { | ||
const reference = findReference(lookup.name); | ||
isRegExp = reference.init.regex; | ||
test(t => { | ||
t.is(y, 0); | ||
}); | ||
``` | ||
*/ | ||
const findRootReference = node => { | ||
if (node.type === 'Identifier') { | ||
const reference = findReference(node.name); | ||
if (reference && reference.init) { | ||
return findRootReference(reference.init); | ||
} | ||
if (!isRegExp) { | ||
return node; | ||
} | ||
if (node.type === 'CallExpression' || node.type === 'NewExpression') { | ||
return findRootReference(node.callee); | ||
} | ||
if (node.type === 'MemberExpression') { | ||
return findRootReference(node.object); | ||
} | ||
return node; | ||
}; | ||
/* | ||
Determine if the given node is a regex expression. | ||
There are two ways to create regex expressions in JavaScript: Regex literal and `RegExp` class. | ||
1. Regex literal can be easily looked up using the `.regex` property on the node. | ||
2. `RegExp` class can't be looked up so the function just checks for the name `RegExp`. | ||
*/ | ||
const isRegExp = lookup => { | ||
if (lookup.regex) { | ||
return true; | ||
} | ||
// Look up references in case it's a variable or RegExp declaration. | ||
const reference = findRootReference(lookup); | ||
return reference.regex || reference.name === 'RegExp'; | ||
}; | ||
const booleanHandler = node => { | ||
const firstArg = node.arguments[0]; | ||
const isFunctionCall = firstArg.type === 'CallExpression'; | ||
if (!isFunctionCall || !firstArg.callee.property) { | ||
return; | ||
} | ||
const {name} = firstArg.callee.property; | ||
let lookup = {}; | ||
let variable = {}; | ||
if (name === 'test') { | ||
// Represent: `lookup.test(variable)` | ||
lookup = firstArg.callee.object; | ||
variable = firstArg.arguments[0]; | ||
} else if (['search', 'match'].includes(name)) { | ||
// Represent: `variable.match(lookup)` | ||
lookup = firstArg.arguments[0]; | ||
variable = firstArg.callee.object; | ||
} | ||
if (!isRegExp(lookup)) { | ||
return; | ||
} | ||
const assertion = ['true', 'truthy'].includes(node.callee.property.name) ? 'regex' : 'notRegex'; | ||
const fix = fixer => { | ||
const source = context.getSourceCode(); | ||
return [ | ||
fixer.replaceText(node.callee.property, assertion), | ||
fixer.replaceText(firstArg, `${source.getText(variable)}, ${source.getText(lookup)}`) | ||
]; | ||
}; | ||
context.report({ | ||
node, | ||
message: `Prefer using the \`t.${assertion}()\` assertion.`, | ||
fix | ||
}); | ||
}; | ||
const equalityHandler = node => { | ||
const [firstArg, secondArg] = node.arguments; | ||
const firstArgumentIsRegex = isRegExp(firstArg); | ||
const secondArgumentIsRegex = isRegExp(secondArg); | ||
// If both are regex, or neither are, the expression is ok. | ||
if (firstArgumentIsRegex === secondArgumentIsRegex) { | ||
return; | ||
} | ||
const matchee = secondArgumentIsRegex ? firstArg : secondArg; | ||
const regex = secondArgumentIsRegex ? secondArg : firstArg; | ||
const booleanFixer = assertion => fixer => { | ||
const source = context.getSourceCode(); | ||
return [ | ||
fixer.replaceText(node.callee.property, assertion), | ||
fixer.replaceText(firstArg, `${source.getText(regex.arguments[0])}`), | ||
fixer.replaceText(secondArg, `${source.getText(regex.callee.object)}`) | ||
]; | ||
}; | ||
// Only fix a statically verifiable equality. | ||
if (regex && matchee.type === 'Literal') { | ||
let assertion; | ||
if (matchee.raw === 'true') { | ||
assertion = 'regex'; | ||
} else if (matchee.raw === 'false') { | ||
assertion = 'notRegex'; | ||
} else { | ||
return; | ||
} | ||
const assertion = ['true', 'truthy'].includes(node.callee.property.name) ? 'regex' : 'notRegex'; | ||
const fix = fixer => { | ||
const source = context.getSourceCode(); | ||
return [ | ||
fixer.replaceText(node.callee.property, assertion), | ||
fixer.replaceText(firstArg, `${source.getText(variable)}, ${source.getText(lookup)}`) | ||
]; | ||
}; | ||
context.report({ | ||
node, | ||
message: `Prefer using the \`t.${assertion}()\` assertion.`, | ||
fix | ||
fix: booleanFixer(assertion) | ||
}); | ||
} | ||
}; | ||
return ava.merge({ | ||
CallExpression: visitIf([ | ||
ava.isInTestFile, | ||
ava.isInTestNode | ||
])(node => { | ||
const isAssertion = node.callee.type === 'MemberExpression' && | ||
util.getNameOfRootNodeObject(node.callee) === 't'; | ||
const isBooleanAssertion = isAssertion && | ||
booleanTests.has(node.callee.property.name); | ||
const isEqualityAssertion = isAssertion && | ||
equalityTests.has(node.callee.property.name); | ||
if (isBooleanAssertion) { | ||
booleanHandler(node); | ||
} else if (isEqualityAssertion) { | ||
equalityHandler(node); | ||
} | ||
}) | ||
@@ -87,0 +198,0 @@ }); |
@@ -15,3 +15,3 @@ 'use strict'; | ||
])(node => { | ||
const firstArgumentIsFunction = node.arguments.length < 1 || util.isFunctionExpression(node.arguments[0]); | ||
const firstArgumentIsFunction = node.arguments.length === 0 || util.isFunctionExpression(node.arguments[0]); | ||
@@ -18,0 +18,0 @@ if (firstArgumentIsFunction) { |
@@ -23,3 +23,3 @@ 'use strict'; | ||
if (node.object.type === 'MemberExpression') { | ||
return getMemberNodes(node.object).concat(node.property); | ||
return [...getMemberNodes(node.object), node.property]; | ||
} | ||
@@ -26,0 +26,0 @@ |
@@ -51,3 +51,3 @@ 'use strict'; | ||
if (node.type === 'MemberExpression') { | ||
return getTestModifiers(node.object).concat(node.property); | ||
return [...getTestModifiers(node.object), node.property]; | ||
} | ||
@@ -83,3 +83,3 @@ | ||
if (node.object.type === 'MemberExpression') { | ||
return getMembers(node.object).concat(name); | ||
return [...getMembers(node.object), name]; | ||
} | ||
@@ -130,2 +130,2 @@ | ||
exports.assertionMethods = new Set(assertionMethodNames); | ||
exports.executionMethods = new Set(assertionMethodNames.concat(['end', 'plan', 'log', 'teardown', 'timeout'])); | ||
exports.executionMethods = new Set([...assertionMethodNames, 'end', 'plan', 'log', 'teardown', 'timeout']); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
78457
38
2579
129
+ Addedpkg-dir@5.0.0(transitive)
- Removedfind-up@4.1.0(transitive)
- Removedlocate-path@5.0.0(transitive)
- Removedp-limit@2.3.0(transitive)
- Removedp-locate@4.1.0(transitive)
- Removedp-try@2.2.0(transitive)
- Removedpkg-dir@4.2.0(transitive)
Updatedespree@^7.3.1
Updatedimport-modules@^2.1.0
Updatedpkg-dir@^5.0.0