Repository: not-an-aardvark/eslint-plugin-eslint-plugin (js)

Issue:
New rule: no-test-only
refs: https://github.com/eslint/rfcs/tree/main/designs/2020-rule-tester-only
RuleTester test cases support a new only property to run an individual case or subset of cases for less noisy debugging. It should be only used in development.

```js
valid: [
{ code: "", only: true}, // should report an error
]
```

```js
valid: [
ruleTester.only("var i = 0;"), // should report an error
]
```



Pull request:
Add new rule `no-only-tests`
Fixes #118.

I named it `no-only-tests` for consistency with the [no-identical-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-identical-tests.md) rule name. I would be equally happy to name it `no-test-only` or something else. Let me know if you want me to change the name.

We should enable this as a `recommended` rule in v4 (#120).

Most other test frameworks have a similar rule to this:
* [ava/no-only-test](https://github.com/avajs/eslint-plugin-ava/blob/main/docs/rules/no-only-test.md)
* [jest/no-focused-tests](https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/no-focused-tests.md)
* [mocha/no-exclusive-tests](https://github.com/lo1tuma/eslint-plugin-mocha/blob/master/docs/rules/no-exclusive-tests.md)
* [qunit/no-only](https://github.com/platinumazure/eslint-plugin-qunit/blob/master/docs/rules/no-only.md)

Patch:
diff --git a/README.md b/README.md
index e7c17ba..01f7296 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.


Test patch:
diff --git a/docs/rules/no-only-tests.md b/docs/rules/no-only-tests.md
new file mode 100644
index 0000000..a42b1f4
--- /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 0000000..0cdb12c
--- /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 0000000..8fb6e51
--- /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: [] }],
+    },
+  ],
+});