Repository: not-an-aardvark/eslint-plugin-eslint-plugin
Pull request #50 (2017-10-22 20:26:27)

New: rule prefer-replace-text


Patch:
diff --git a/README.md b/README.md
index e18a0fe..17993af 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ An ESLint plugin for linting ESLint plugins
 
 ## Installation
 
-You'll first need to install [ESLint](http://eslint.org):
+You'll first need to install [ESLint](https://eslint.org):
 
 ```
 $ npm i eslint --save-dev
@@ -59,6 +59,7 @@ Name | ✔️ | 🛠 | Description
 [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-output-null](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-output-null.md) |  | 🛠 | disallows invalid RuleTester test cases with the output the same as the code.
 [prefer-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-placeholders.md) |  |  | disallow template literals as report messages
+[prefer-replace-text](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-replace-text.md) |  |  | prefer using replaceText instead of replaceTextRange.
 [report-message-format](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/report-message-format.md) |  |  | enforce a consistent format for rule report messages
 [require-meta-fixable](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-fixable.md) | ✔️ |  | require rules to implement a meta.fixable property
 [test-case-property-ordering](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-property-ordering.md) |  | 🛠 | Requires the properties of a test case to be placed in a consistent order
diff --git a/docs/rules/no-useless-token-range.md b/docs/rules/no-useless-token-range.md
index 15ef018..93d6133 100644
--- a/docs/rules/no-useless-token-range.md
+++ b/docs/rules/no-useless-token-range.md
@@ -42,4 +42,4 @@ module.exports = {
 
 ## Further Reading
 
-* [`SourceCode` API](http://eslint.org/docs/developer-guide/working-with-rules#contextgetsourcecode)
+* [`SourceCode` API](https://eslint.org/docs/developer-guide/working-with-rules#contextgetsourcecode)
diff --git a/docs/rules/prefer-replace-range.md b/docs/rules/prefer-replace-range.md
new file mode 100644
index 0000000..48688d8
--- /dev/null
+++ b/docs/rules/prefer-replace-range.md
@@ -0,0 +1,52 @@
+# prefer using replaceText instead of replaceTextRange. (prefer-replace-text)
+
+## Rule Details
+
+The rule reports an error if `replaceTextRange`'s first argument is an array of identical array elements. It can be easily replaced by `replaceText` to improve readability.
+
+Examples of **incorrect** code for this rule:
+
+```js
+/* eslint eslint-plugin/prefer-text-range: error */
+module.exports = {
+  create(context) {
+    context.report({
+      fix(fixer) {
+        // error, can be written: return fixer.replaceText([node, '']);
+        return fixer.replaceTextRange([node.range[0], node.range[1]], '');
+      }
+    });
+  }
+};
+```
+
+Examples of **correct** code for this rule:
+
+```js
+/* eslint eslint-plugin/prefer-text-range: error */
+module.exports = {
+  create(context) {
+    context.report({
+      fix(fixer) {
+        return fixer.replaceText(node, '');
+      }
+    });
+  }
+};
+
+module.exports = {
+  create(context) {
+    context.report({
+      fix(fixer) {
+        // start = ...
+        // end = ...
+        return fixer.replaceTextRange([start, end], '');
+      }
+    });
+  }
+};
+```
+
+## Further Reading
+
+* [Applying Fixes](https://eslint.org/docs/developer-guide/working-with-rules#applying-fixes)
diff --git a/lib/rules/prefer-replace-text.js b/lib/rules/prefer-replace-text.js
new file mode 100644
index 0000000..a4b9856
--- /dev/null
+++ b/lib/rules/prefer-replace-text.js
@@ -0,0 +1,83 @@
+/**
+ * @fileoverview prefer using replaceText instead of replaceTextRange.
+ * @author 薛定谔的猫<hh_2013@foxmail.com>
+ */
+
+'use strict';
+
+const utils = require('../utils');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'prefer using replaceText instead of replaceTextRange.',
+      category: 'Rules',
+      recommended: false,
+    },
+    fixable: null,
+    schema: [],
+  },
+
+  create (context) {
+    const sourceCode = context.getSourceCode();
+    const message = 'Use replaceText instead of replaceTextRange.';
+    let funcInfo = {
+      upper: null,
+      codePath: null,
+      shouldCheck: false,
+      node: null,
+    };
+    let contextIdentifiers;
+
+    return {
+      Program (node) {
+        contextIdentifiers = utils.getContextIdentifiers(context, node);
+      },
+
+      // Stacks this function's information.
+      onCodePathStart (codePath, node) {
+        const parent = node.parent;
+        const shouldCheck = (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') &&
+          parent.parent.type === 'ObjectExpression' &&
+          parent.parent.parent.type === 'CallExpression' &&
+          contextIdentifiers.has(parent.parent.parent.callee.object) &&
+          parent.parent.parent.callee.property.name === 'report' &&
+          utils.getReportInfo(parent.parent.parent.arguments).fix === node;
+
+        funcInfo = {
+          upper: funcInfo,
+          codePath,
+          shouldCheck,
+          node,
+        };
+      },
+
+      // Pops this function's information.
+      onCodePathEnd () {
+        funcInfo = funcInfo.upper;
+      },
+
+      // Checks the replaceTextRange arguments.
+      'CallExpression[arguments.length=2]' (node) {
+        if (funcInfo.shouldCheck &&
+          node.callee.type === 'MemberExpression' &&
+          node.callee.property.name === 'replaceTextRange') {
+          const arg = node.arguments[0];
+          const isIdenticalNodeRange = arg.type === 'ArrayExpression' &&
+            arg.elements[0].type === 'MemberExpression' && arg.elements[1].type === 'MemberExpression' &&
+            sourceCode.getText(arg.elements[0].object) === sourceCode.getText(arg.elements[1].object);
+          if (isIdenticalNodeRange) {
+            context.report({
+              node,
+              message,
+            });
+          }
+        }
+      },
+    };
+  },
+};


Test patch:
diff --git a/tests/lib/rules/prefer-replace-text.js b/tests/lib/rules/prefer-replace-text.js
new file mode 100644
index 0000000..2c339f1
--- /dev/null
+++ b/tests/lib/rules/prefer-replace-text.js
@@ -0,0 +1,127 @@
+/**
+ * @fileoverview prefer using replaceText instead of replaceTextRange
+ * @author 薛定谔的猫<hh_2013@foxmail.com>
+ */
+
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/prefer-replace-text');
+const RuleTester = require('eslint').RuleTester;
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
+const ERROR = { message: 'Use replaceText instead of replaceTextRange.' };
+
+
+ruleTester.run('prefer-placeholders', rule, {
+  valid: [
+    `
+      module.exports = {
+        create(context) {
+          context.report({
+            fix(fixer) {
+              return fixer.replaceTextRange([start, end], '');
+            }
+          });
+        }
+      };
+    `,
+    `
+      module.exports = {
+        create(context) {
+          context.report({
+            fix(fixer) {
+              return fixer.replaceTextRange([node1[0], node2[1]], '');
+            }
+          });
+        }
+      };
+    `,
+    `
+      module.exports = {
+        create(context) {}
+      };
+    `,
+    `
+      fixer.replaceTextRange([node.range[0], node.range[1]], '');
+    `,
+  ],
+
+  invalid: [
+    {
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              fix(fixer) {
+                return fixer.replaceTextRange([node.range[0], node.range[1]], '');
+              }
+            });
+          }
+        };
+    `,
+      errors: [ERROR],
+    },
+    {
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              fix: function(fixer) {
+                return fixer.replaceTextRange([node.range[0], node.range[1]], '');
+              }
+            });
+          }
+        };
+    `,
+      errors: [ERROR],
+    },
+    {
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              fix: function(fixer) {
+                if (foo) {return fixer.replaceTextRange([node.range[0], node.range[1]], '')}
+              }
+            });
+          }
+        };
+    `,
+      errors: [ERROR],
+    },
+    {
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              fix: fixer => fixer.replaceTextRange([node.range[0], node.range[1]], '')
+            });
+          }
+        };
+    `,
+      errors: [ERROR],
+    },
+    {
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              fix(fixer) {
+                return fixer.replaceTextRange([node.start, node.end], '');
+              }
+            });
+          }
+        };
+    `,
+      errors: [ERROR],
+    },
+  ],
+});