diff --git a/docs/rules/prefer-at.md b/docs/rules/prefer-at.md new file mode 100644 index 0000000000..31430745dc --- /dev/null +++ b/docs/rules/prefer-at.md @@ -0,0 +1,17 @@ +# Prefer `.at()` method for negative index access. + + + +This rule is fixable. + +## Fail + +```js +const foo = 'unicorn'; +``` + +## Pass + +```js +const foo = '🦄'; +``` diff --git a/index.js b/index.js index 56f5cf44f3..51c7f45b12 100644 --- a/index.js +++ b/index.js @@ -86,6 +86,7 @@ module.exports = { 'unicorn/prefer-array-flat-map': 'error', 'unicorn/prefer-array-index-of': 'error', 'unicorn/prefer-array-some': 'error', + 'unicorn/prefer-at': 'error', 'unicorn/prefer-date-now': 'error', 'unicorn/prefer-default-parameters': 'error', 'unicorn/prefer-dom-node-append': 'error', diff --git a/readme.md b/readme.md index 5a6353756b..7eafd36d61 100644 --- a/readme.md +++ b/readme.md @@ -82,6 +82,7 @@ Configure it in `package.json`. "unicorn/prefer-array-flat-map": "error", "unicorn/prefer-array-index-of": "error", "unicorn/prefer-array-some": "error", + "unicorn/prefer-at": "error", "unicorn/prefer-date-now": "error", "unicorn/prefer-default-parameters": "error", "unicorn/prefer-dom-node-append": "error", @@ -176,6 +177,7 @@ Each rule has emojis denoting: | [prefer-array-flat-map](docs/rules/prefer-array-flat-map.md) | Prefer `.flatMap(…)` over `.map(…).flat()`. | ✅ | 🔧 | | [prefer-array-index-of](docs/rules/prefer-array-index-of.md) | Prefer `Array#indexOf()` over `Array#findIndex()` when looking for the index of an item. | ✅ | 🔧 | | [prefer-array-some](docs/rules/prefer-array-some.md) | Prefer `.some(…)` over `.find(…)`. | ✅ | | +| [prefer-at](docs/rules/prefer-at.md) | Prefer `.at()` method for negative index access. | ✅ | 🔧 | | [prefer-date-now](docs/rules/prefer-date-now.md) | Prefer `Date.now()` to get the number of milliseconds since the Unix Epoch. | ✅ | 🔧 | | [prefer-default-parameters](docs/rules/prefer-default-parameters.md) | Prefer default parameters over reassignment. | ✅ | 🔧 | | [prefer-dom-node-append](docs/rules/prefer-dom-node-append.md) | Prefer `Node#append()` over `Node#appendChild()`. | ✅ | 🔧 | diff --git a/rules/prefer-at.js b/rules/prefer-at.js new file mode 100644 index 0000000000..f83b850f53 --- /dev/null +++ b/rules/prefer-at.js @@ -0,0 +1,71 @@ +'use strict'; +const {getStaticValue} = require('eslint-utils'); +const getDocumentationUrl = require('./utils/get-documentation-url'); +const isSameReference = require('./utils/is-same-reference'); +const replaceNodeOrTokenAndSpacesBefore = require('./utils/replace-node-or-token-and-spaces-before'); + +const MESSAGE_ID = 'prefer-at'; +const messages = { + [MESSAGE_ID]: 'Prefer `.at()` method for negative index access.' +}; + +const selector = [ + 'MemberExpression', + '[optional!=true]', + '[computed=true]', + '[property.type="BinaryExpression"]', + '[property.operator="-"]', + '[property.left.type="MemberExpression"]', + '[property.left.optional!=true]', + '[property.left.computed=false]', + '[property.left.property.type="Identifier"]', + '[property.left.property.name="length"]' +].join('') + +/** @param {import('eslint').Rule.RuleContext} context */ +const create = context => { + return { + [selector](node) { + const array = node.object; + const lengthObject = node.property.left.object; + if (!isSameReference(array, lengthObject)) { + return; + } + + const offset = node.property.right; + const staticResult = getStaticValue(offset, context.getScope()); + if ( + !staticResult || + !Number.isInteger(staticResult.value) || + staticResult.value < 1 + ) { + return; + } + + context.report({ + node: node.property, + messageId: MESSAGE_ID, + /** @param {import('eslint').Rule.RuleFixer} fixer */ + * fix(fixer) { + yield * replaceNodeOrTokenAndSpacesBefore(node.property.left, '', fixer, context.getSourceCode()); + } + }); + } + } +}; + +const schema = []; + +module.exports = { + create, + meta: { + type: 'suggestion', + docs: { + description: 'Prefer `.at()` method for negative index access.', + url: getDocumentationUrl(__filename) + }, + fixable: 'code', + schema, + messages + } +}; diff --git a/test/prefer-at.mjs b/test/prefer-at.mjs new file mode 100644 index 0000000000..c8b0b1ad5c --- /dev/null +++ b/test/prefer-at.mjs @@ -0,0 +1,31 @@ +import outdent from 'outdent'; +import {getTester} from './utils/test.mjs'; + +const {test} = getTester(import.meta); + + +test.snapshot({ + valid: [ + 'array.at(-1)', + 'array.at(1)', + 'array[0]', + 'array.foo', + 'array[-1]', + 'array[array.length + 1]', + 'array[array.length - 0]', + 'array[-10 + array.length]', + 'array[array.length - 1n]', + 'array[+array.length - 1]', + 'array[array.length - -1]', + 'array[array.length - foo]', + 'array[array.length + -10]', + 'const OFFSET = -1; array[array.length + OFFSET];' + ], + invalid: [ + 'array[array.length - 1]', + 'array[array.length - 10]', + 'array[ /**/ (( /**/ (( /**/ array.length /**/ )) /**/ - /**/ (( /**/ 10 /**/ )) /**/ )) /**/ ]', + 'const OFFSET = 1; array[array.length - OFFSET];', + // 'array.at(array.length - 1)' + ] +}); diff --git a/test/snapshots/prefer-at.mjs.md b/test/snapshots/prefer-at.mjs.md new file mode 100644 index 0000000000..8112ae6d1c --- /dev/null +++ b/test/snapshots/prefer-at.mjs.md @@ -0,0 +1,69 @@ +# Snapshot report for `test/prefer-at.mjs` + +The actual snapshot is saved in `prefer-at.mjs.snap`. + +Generated by [AVA](https://avajs.dev). + +## Invalid #1 + 1 | array[array.length - 1] + +> Output + + `␊ + 1 | array[ - 1]␊ + ` + +> Error 1/1 + + `␊ + > 1 | array[array.length - 1]␊ + | ^^^^^^^^^^^^^^^^ Prefer \`.at()\` method for negative index access.␊ + ` + +## Invalid #2 + 1 | array[array.length - 10] + +> Output + + `␊ + 1 | array[ - 10]␊ + ` + +> Error 1/1 + + `␊ + > 1 | array[array.length - 10]␊ + | ^^^^^^^^^^^^^^^^^ Prefer \`.at()\` method for negative index access.␊ + ` + +## Invalid #3 + 1 | array[ /**/ (( /**/ (( /**/ array.length /**/ )) /**/ - /**/ (( /**/ 10 /**/ )) /**/ )) /**/ ] + +> Output + + `␊ + 1 | array[ /**/ (( /**/ /**/ /**/ /**/ - /**/ (( /**/ 10 /**/ )) /**/ )) /**/ ]␊ + ` + +> Error 1/1 + + `␊ + > 1 | array[ /**/ (( /**/ (( /**/ array.length /**/ )) /**/ - /**/ (( /**/ 10 /**/ )) /**/ )) /**/ ]␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer \`.at()\` method for negative index access.␊ + ` + +## Invalid #4 + 1 | const OFFSET = 1; array[array.length - OFFSET]; + +> Output + + `␊ + 1 | const OFFSET = 1; array[ - OFFSET];␊ + ` + +> Error 1/1 + + `␊ + > 1 | const OFFSET = 1; array[array.length - OFFSET];␊ + | ^^^^^^^^^^^^^^^^^^^^^ Prefer \`.at()\` method for negative index access.␊ + ` diff --git a/test/snapshots/prefer-at.mjs.snap b/test/snapshots/prefer-at.mjs.snap new file mode 100644 index 0000000000..6c9661c00e Binary files /dev/null and b/test/snapshots/prefer-at.mjs.snap differ