From 15d020b023365ef5caa112b5b867076f6547c81a Mon Sep 17 00:00:00 2001 From: Martin Giger Date: Mon, 15 Jan 2018 19:53:14 +0100 Subject: [PATCH] feat: add avoid-reverse rule (fixes #12) --- README.md | 39 ++++++++++++++++++++++++ index.js | 6 ++-- rules/avoid-reverse.js | 59 +++++++++++++++++++++++++++++++++++++ test/rules/avoid-reverse.js | 58 ++++++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 rules/avoid-reverse.js create mode 100644 test/rules/avoid-reverse.js diff --git a/README.md b/README.md index 63543fe..c805015 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Rules for Array functions and methods. - [Examples](#examples-1) - [`prefer-array-from`](#prefer-array-from) - [Examples](#examples-2) + - [`avoid-reverse`](#avoid-reverse) + - [Examples](#examples-3) - [`array-func/recommended` Configuration](#array-funcrecommended-configuration) - [Using the Configuration](#using-the-configuration) - [License](#license) @@ -151,6 +153,42 @@ const arrayCopy = Array.from(array); const characterArray = Array.from("string"); ``` +### `avoid-reverse` +Avoid reversing the array and running a method on it if there is an equivalent +of the method operating on the array from the other end. + +There are two operations with such equivalents: `indexOf` with `lastIndexOf` and +`reduce` with `reduceRight`. + +This rule is auto fixable. + +#### Examples +Code that triggers this rule: +```js +const lastIndex = array.reverse().indexOf(1); + +const firstIndex = array.reverse().lastIndexOf(1); + +const sum = array.reverse().reduce((p, c) => p + c, 0); + +const reverseSum = array.reverse().reduceRight((p, c) => p + c, 0); +``` + +Code that doesn't trigger this rule: +```js +const lastIndex = array.lastIndexOf(1); + +const firstIndex = array.indexOf(1); + +const sum = array.reduce((p, c) => p + c, 0); + +const reverseSum = array.reduceRight((p, c) => p + c, 0); + +const reverseArray = array.reverse(); + +const reverseMap = array.reverse().map((r) => r + 1); +``` + ## `array-func/recommended` Configuration The recommended configuration will set your parser ECMA Version to 2015, since that's when the Array functions and methods were added. @@ -159,6 +197,7 @@ Rule | Error level | Fixable `from-map` | Error | Yes `no-unnecessary-this-arg` | Error | Sometimes `prefer-array-from` | Error | Yes +`avoid-reverse` | Error | Yes ### Using the Configuration To enable this configuration use the `extends` property in your `.eslintrc.json` config file (may look different for other config file styles): diff --git a/index.js b/index.js index 0457c3b..4cb6204 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,8 @@ module.exports = { rules: { "from-map": require("./rules/from-map"), "no-unnecessary-this-arg": require("./rules/no-unnecessary-this-arg"), - "prefer-array-from": require("./rules/prefer-array-from") + "prefer-array-from": require("./rules/prefer-array-from"), + "avoid-reverse": require("./rules/avoid-reverse") }, configs: { recommended: { @@ -19,7 +20,8 @@ module.exports = { rules: { "array-func/from-map": "error", "array-func/no-unnecessary-this-arg": "error", - "array-func/prefer-array-from": "error" + "array-func/prefer-array-from": "error", + "array-func/avoid-reverse": "error" } } } diff --git a/rules/avoid-reverse.js b/rules/avoid-reverse.js new file mode 100644 index 0000000..1d20f7b --- /dev/null +++ b/rules/avoid-reverse.js @@ -0,0 +1,59 @@ +/** + * @author Martin Giger + * @license MIT + */ +"use strict"; + +const { + isMethod, + getParent +} = require("../lib/helpers/call-expression"); + +const REPLACEMENTS = { + indexOf: "lastIndexOf", + reduce: "reduceRight", + lastIndexOf: "indexOf", + reduceRight: "reduce" +}; + +module.exports = { + meta: { + docs: { + description: "Prefer methods operating from the right over reversing the array", + recommended: true + }, + schema: [], + fixable: "code" + }, + create(context) { + return { + "CallExpression:exit"(node) { + if(Object.keys(REPLACEMENTS).every((m) => !isMethod(node, m))) { + return; + } + + const parent = getParent(node); + if(!isMethod(parent, 'reverse')) { + return; + } + + const reversed = REPLACEMENTS[node.callee.property.name]; + + context.report({ + node: node.callee.property, + loc: { + start: parent.callee.property.loc.start, + end: node.callee.property.loc.end + }, + message: `Prefer using ${reversed} over reversing the array and ${node.callee.property.name}`, + fix(fixer) { + return fixer.replaceTextRange([ + parent.callee.property.start, + node.callee.property.end + ], reversed); + } + }); + } + }; + } +}; diff --git a/test/rules/avoid-reverse.js b/test/rules/avoid-reverse.js new file mode 100644 index 0000000..ae31e90 --- /dev/null +++ b/test/rules/avoid-reverse.js @@ -0,0 +1,58 @@ +import test from 'ava'; +import AvaRuleTester from 'eslint-ava-rule-tester'; +import rule from '../../rules/avoid-reverse'; + +const ruleTester = new AvaRuleTester(test, { + parserOptions: { + ecmaVersion: 2015 + } +}); + +ruleTester.run('from-map', rule, { + valid: [ + 'array.lastIndexOf(1)', + 'array.indexOf(1)', + 'array.reduce((p, c) => p + c, 0)', + 'array.reduceRight((p, c) => p + c, 0)', + 'array.reverse()', + 'array.reverse().map((r) => r + 1)' + ], + invalid: [ + { + code: 'array.reverse().indexOf(1)', + errors: [ { + message: 'Prefer using lastIndexOf over reversing the array and indexOf', + column: 7, + line: 1 + } ], + output: 'array.lastIndexOf(1)' + }, + { + code: 'array.reverse().lastIndexOf(1)', + errors: [ { + message: 'Prefer using indexOf over reversing the array and lastIndexOf', + column: 7, + line: 1 + } ], + output: 'array.indexOf(1)' + }, + { + code: 'array.reverse().reduce((p, c) => p + c, 0)', + errors: [ { + message: 'Prefer using reduceRight over reversing the array and reduce', + column: 7, + line: 1 + } ], + output: 'array.reduceRight((p, c) => p + c, 0)' + }, + { + code: 'array.reverse().reduceRight((p, c) => p + c, 0)', + errors: [ { + message: 'Prefer using reduce over reversing the array and reduceRight', + column: 7, + line: 1 + } ], + output: 'array.reduce((p, c) => p + c, 0)' + } + ] +});