diff --git a/README.md b/README.md index e7c17ba2..01f72963 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Name | ✔️ | 🛠 | Description [no-deprecated-report-api](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-report-api.md) | ✔️ | 🛠 | disallow use of the deprecated context.report() API [no-identical-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-identical-tests.md) | ✔️ | 🛠 | disallow identical tests [no-missing-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-missing-placeholders.md) | ✔️ | | disallow missing placeholders in rule report messages +[no-only-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-only-tests.md) | | | disallow the test case property `only` [no-unused-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-unused-placeholders.md) | ✔️ | | disallow unused placeholders in rule report messages [no-useless-token-range](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-useless-token-range.md) | ✔️ | 🛠 | disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken [prefer-object-rule](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-object-rule.md) | | 🛠 | disallow rule exports where the export is a function. diff --git a/docs/rules/no-only-tests.md b/docs/rules/no-only-tests.md new file mode 100644 index 00000000..a42b1f48 --- /dev/null +++ b/docs/rules/no-only-tests.md @@ -0,0 +1,55 @@ +# Disallow the test case property `only` (no-only-tests) + +The [`only` property](https://eslint.org/docs/developer-guide/unit-tests#running-individual-tests) can be used as of [ESLint 7.29](https://eslint.org/blog/2021/06/eslint-v7.29.0-released#highlights) for running individual rule test cases with less-noisy debugging. This feature should be only used in development, as it prevents all the tests from running. Mistakenly checking-in a test case with this property can cause CI tests to incorrectly pass. + +## Rule Details + +This rule flags a violation when a test case is using `only`. Note that this rule is not autofixable since automatically deleting the property would prevent developers from being able to use it during development. + +Examples of **incorrect** code for this rule: + +```js +/* eslint eslint-plugin/no-only-tests: error */ + +const { RuleTester } = require('eslint'); +const ruleTester = new RuleTester(); + +ruleTester.run('my-rule', myRule, { + valid: [ + { + code: 'const valid = 42;', + only: true, + }, + RuleTester.only('const valid = 42;'), + ], + invalid: [ + { + code: 'const invalid = 42;', + only: true, + errors: [/* ... */], + }, + ], +}); +``` + +Examples of **correct** code for this rule: + +```js +/* eslint eslint-plugin/no-only-tests: error */ + +const { RuleTester } = require('eslint'); +const ruleTester = new RuleTester(); + +ruleTester.run('my-rule', myRule, { + valid: [ + 'const valid = 42;', + { code: 'const valid = 42;' }, + ], + invalid: [ + { + code: 'const invalid = 42;', + errors: [/* ... */], + }, + ], +}); +``` diff --git a/lib/rules/no-only-tests.js b/lib/rules/no-only-tests.js new file mode 100644 index 00000000..0cdb12c3 --- /dev/null +++ b/lib/rules/no-only-tests.js @@ -0,0 +1,112 @@ +'use strict'; + +const utils = require('../utils'); + +/** + * Checks if the given token is a comma token or not. + * From: https://github.com/eslint/eslint/blob/master/lib/rules/utils/ast-utils.js + * @param {Token} token The token to check. + * @returns {boolean} `true` if the token is a comma token. + */ +function isCommaToken (token) { + return token.value === ',' && token.type === 'Punctuator'; +} + +/** + * Checks if the given token is an opening brace token or not. + * From: https://github.com/eslint/eslint/blob/master/lib/rules/utils/ast-utils.js + * @param {Token} token The token to check. + * @returns {boolean} `true` if the token is an opening brace token. + */ +function isOpeningBraceToken (token) { + return token.value === '{' && token.type === 'Punctuator'; +} + +/** + * Checks if the given token is a closing brace token or not. + * From: https://github.com/eslint/eslint/blob/master/lib/rules/utils/ast-utils.js + * @param {Token} token The token to check. + * @returns {boolean} `true` if the token is a closing brace token. + */ +function isClosingBraceToken (token) { + return token.value === '}' && token.type === 'Punctuator'; +} + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'disallow the test case property `only`', + category: 'Tests', + recommended: false, + }, + schema: [], + messages: { + foundOnly: + 'The test case property `only` can be used during development, but should not be checked-in, since it prevents all the tests from running.', + removeOnly: 'Remove `only`.', + }, + hasSuggestions: true, + }, + + create (context) { + return { + Program (ast) { + for (const testRun of utils.getTestInfo(context, ast)) { + for (const test of [...testRun.valid, ...testRun.invalid]) { + if (test.type === 'ObjectExpression') { + // Test case object: { code: 'const x = 123;', ... } + + const onlyProperty = test.properties.find( + property => + property.key.type === 'Identifier' && + property.key.name === 'only' && + property.value.type === 'Literal' && + property.value.value + ); + + if (onlyProperty) { + context.report({ + node: onlyProperty, + messageId: 'foundOnly', + suggest: [ + { + messageId: 'removeOnly', + *fix (fixer) { + const sourceCode = context.getSourceCode(); + + const tokenBefore = sourceCode.getTokenBefore(onlyProperty); + const tokenAfter = sourceCode.getTokenAfter(onlyProperty); + if ( + (isCommaToken(tokenBefore) && isCommaToken(tokenAfter)) || // In middle of properties + (isOpeningBraceToken(tokenBefore) && isCommaToken(tokenAfter)) // At beginning of properties + ) { + yield fixer.remove(tokenAfter); // Remove extra comma. + } + if (isCommaToken(tokenBefore) && isClosingBraceToken(tokenAfter)) { // At end of properties + yield fixer.remove(tokenBefore); // Remove extra comma. + } + + yield fixer.remove(onlyProperty); + }, + }, + ], + }); + } + } else if ( + test.type === 'CallExpression' && + test.callee.type === 'MemberExpression' && + test.callee.object.type === 'Identifier' && + test.callee.object.name === 'RuleTester' && + test.callee.property.type === 'Identifier' && + test.callee.property.name === 'only' + ) { + // RuleTester.only('const x = 123;'); + context.report({ node: test.callee, messageId: 'foundOnly' }); + } + } + } + }, + }; + }, +}; diff --git a/tests/lib/rules/no-only-tests.js b/tests/lib/rules/no-only-tests.js new file mode 100644 index 00000000..8fb6e51c --- /dev/null +++ b/tests/lib/rules/no-only-tests.js @@ -0,0 +1,251 @@ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/no-only-tests'); +const RuleTester = require('eslint').RuleTester; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +ruleTester.run('no-only-tests', rule, { + valid: [ + // No test cases with `only` + ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [ + 'foo', + { code: 'foo', foo: true }, + RuleTester.somethingElse(), + notRuleTester.only() + ], + invalid: [ + { code: 'bar', foo: true }, + ] + }); + `, + // `only` set to `false` + ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [ + { code: 'foo', only: false }, + ], + invalid: [ + { code: 'bar', only: false }, + ] + }); + `, + ], + + invalid: [ + { + // Valid test case with `only` + code: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [ + { code: 'foo', only: true }, + ], + invalid: [] + });`, + output: null, + errors: [ + { + messageId: 'foundOnly', + type: 'Property', + line: 6, + endLine: 6, + column: 28, + endColumn: 38, + suggestions: [ + { + messageId: 'removeOnly', + output: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [ + { code: 'foo' }, + ], + invalid: [] + });`, + }, + ], + }, + ], + }, + { + // Invalid test case with `only` (property at end of object, no trailing comma) + code: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo', only: true }, + ] + });`, + output: null, + errors: [ + { + messageId: 'foundOnly', + type: 'Property', + line: 7, + endLine: 7, + column: 28, + endColumn: 38, + suggestions: [ + { + messageId: 'removeOnly', + output: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo' }, + ] + });`, + }, + ], + }, + ], + }, + { + // Invalid test case with `only` (property at end of object, with trailing comma) + code: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo', only: true, }, + ] + });`, + output: null, + errors: [ + { + messageId: 'foundOnly', + type: 'Property', + line: 7, + endLine: 7, + column: 28, + endColumn: 38, + suggestions: [ + { + messageId: 'removeOnly', + output: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo', }, + ] + });`, + }, + ], + }, + ], + }, + { + // Invalid test case with `only` (property in middle of object) + code: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo', only: true, bar: true }, + ] + });`, + output: null, + errors: [ + { + messageId: 'foundOnly', + type: 'Property', + line: 7, + endLine: 7, + column: 28, + endColumn: 38, + suggestions: [ + { + messageId: 'removeOnly', + output: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo', bar: true }, + ] + });`, + }, + ], + }, + ], + }, + { + // Invalid test case with `only` (property at beginning of object) + code: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { only: true, code: 'foo' }, + ] + });`, + output: null, + errors: [ + { + messageId: 'foundOnly', + type: 'Property', + line: 7, + endLine: 7, + column: 15, + endColumn: 25, + suggestions: [ + { + messageId: 'removeOnly', + output: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + ruleTester.run('foo', bar, { + valid: [], + invalid: [ + { code: 'foo' }, + ] + });`, + }, + ], + }, + ], + }, + + { + // Valid test case using `RuleTester.only` + code: ` + const { RuleTester } = require('eslint'); + const ruleTester = new RuleTester(); + new RuleTester().run('foo', bar, { + valid: [ + RuleTester.only('foo'), + ], + invalid: [] + }); + `, + output: null, + errors: [{ messageId: 'foundOnly', type: 'MemberExpression', line: 6, endLine: 6, column: 13, endColumn: 28, suggestions: [] }], + }, + ], +});