diff --git a/README.md b/README.md index e18a0fe0..ceb6ec04 100644 --- a/README.md +++ b/README.md @@ -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/prefer-replace-range.md b/docs/rules/prefer-replace-range.md new file mode 100644 index 00000000..48688d8a --- /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 00000000..a4b9856f --- /dev/null +++ b/lib/rules/prefer-replace-text.js @@ -0,0 +1,83 @@ +/** + * @fileoverview prefer using replaceText instead of replaceTextRange. + * @author 薛定谔的猫 + */ + +'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, + }); + } + } + }, + }; + }, +}; diff --git a/tests/lib/rules/prefer-replace-text.js b/tests/lib/rules/prefer-replace-text.js new file mode 100644 index 00000000..2c339f16 --- /dev/null +++ b/tests/lib/rules/prefer-replace-text.js @@ -0,0 +1,127 @@ +/** + * @fileoverview prefer using replaceText instead of replaceTextRange + * @author 薛定谔的猫 + */ + +'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], + }, + ], +});