Skip to content

Commit

Permalink
feat(eslint-plugin) [prefer-at] Create rule typescript-eslint#6401
Browse files Browse the repository at this point in the history
  • Loading branch information
Святослав Зайцев committed Feb 2, 2023
1 parent b14d3be commit 85e3083
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 0 deletions.
40 changes: 40 additions & 0 deletions packages/eslint-plugin/docs/rules/prefer-at.md
@@ -0,0 +1,40 @@
---
description: Enforce the use of `array.at(-1)` instead of `array[array.length - 1]`
---

> 🛑 This file is source code, not the primary documentation location! 🛑
>
> See **https://typescript-eslint.io/rules/prefer-at** for documentation.
There are two ways to get the last item of the array:

- `arr[arr.length - 1]`: getting an item by index calculated relative to the length of the array.
- `arr.at(-1)`: getting an item by the `at` method. [Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at)

`arr.at(-1)` is a cleaner equivalent to `arr[arr.length - 1]`.

## Examples

<!--tabs-->

### ❌ Incorrect

```ts
let arr = [1, 2, 3];
let a = arr[arr.length - 1];
```

### ✅ Correct

```ts
let arr = [1, 2, 3];
let a = arr.at(-1);
```

<!--/tabs-->

## When Not To Use It

If you support a browser that does not match
[this table](https://caniuse.com/mdn-javascript_builtins_array_at)
or use `node.js < 16.6.0` and don't include the polyfill
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -94,6 +94,7 @@ import objectCurlySpacing from './object-curly-spacing';
import paddingLineBetweenStatements from './padding-line-between-statements';
import parameterProperties from './parameter-properties';
import preferAsConst from './prefer-as-const';
import preferAt from './prefer-at';
import preferEnumInitializers from './prefer-enum-initializers';
import preferForOf from './prefer-for-of';
import preferFunctionType from './prefer-function-type';
Expand Down Expand Up @@ -227,6 +228,7 @@ export default {
'padding-line-between-statements': paddingLineBetweenStatements,
'parameter-properties': parameterProperties,
'prefer-as-const': preferAsConst,
'prefer-at': preferAt,
'prefer-enum-initializers': preferEnumInitializers,
'prefer-for-of': preferForOf,
'prefer-function-type': preferFunctionType,
Expand Down
77 changes: 77 additions & 0 deletions packages/eslint-plugin/src/rules/prefer-at.ts
@@ -0,0 +1,77 @@
import type { TSESTree } from '@typescript-eslint/utils';
import { AST_NODE_TYPES } from '@typescript-eslint/utils';

import * as util from '../util';

export default util.createRule({
name: 'prefer-at',
meta: {
type: 'suggestion',
fixable: 'code',
docs: {
description:
'Enforce the use of `array.at(-1)` instead of `array[array.length - 1]`',
recommended: false,
},
messages: {
preferAt:
'Expected a `{{name}}.at(-1)` instead of `{{name}}[{{name}}.length - 1]`.',
},
schema: [],
},
defaultOptions: [],
create(context) {
class UnknownNodeError extends Error {
public constructor(node: TSESTree.Node) {
super(`UnknownNode ${node.type}`);
}
}

function getName(node: TSESTree.Node): string {
switch (node.type) {
case AST_NODE_TYPES.PrivateIdentifier:
return `#${node.name}`;
case AST_NODE_TYPES.Identifier:
return node.name;
case AST_NODE_TYPES.ThisExpression:
return 'this';
case AST_NODE_TYPES.MemberExpression:
return `${getName(node.object)}.${getName(node.property)}`;
default:
throw new UnknownNodeError(node);
}
}

return {
MemberExpression(node: TSESTree.MemberExpression): void {
try {
if (
node.property.type !== AST_NODE_TYPES.BinaryExpression ||
node.property.operator !== '-' ||
node.property.right.type !== AST_NODE_TYPES.Literal ||
node.property.right.value !== 1
) {
return;
}
const objectName = getName(node.object);
const propertyLeftName = getName(node.property.left);
if (`${objectName}.length` === propertyLeftName) {
context.report({
messageId: 'preferAt',
data: {
name: objectName,
},
node,
fix: fixer => fixer.replaceText(node, `${objectName}.at(-1)`),
});
}
} catch (error) {
if (error instanceof UnknownNodeError) {
return;
}
throw error;
}
},
};
},
});
67 changes: 67 additions & 0 deletions packages/eslint-plugin/tests/rules/prefer-at.test.ts
@@ -0,0 +1,67 @@
import rule from '../../src/rules/prefer-at';
import { RuleTester } from '../RuleTester';

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});

ruleTester.run('prefer-at', rule, {
valid: [
'const a = arr.at(-1);',
'const a = this.arr.at(-1);',
'const a = this.#arr.at(-1);',
'const a = this.#prop.arr.at(-1);',
'const a = arr[arr.length + 1];',
'const a = (arr ? b : c)[arr.length - 1];',
],
invalid: [
{
code: 'const a = arr[arr.length - 1];',
errors: [
{
messageId: 'preferAt',
data: {
name: 'arr',
},
},
],
output: 'const a = arr.at(-1);',
},
{
code: 'const a = this.arr[this.arr.length - 1];',
errors: [
{
messageId: 'preferAt',
data: {
name: 'this.arr',
},
},
],
output: 'const a = this.arr.at(-1);',
},
{
code: 'const a = this.#arr[this.#arr.length - 1];',
errors: [
{
messageId: 'preferAt',
data: {
name: 'this.#arr',
},
},
],
output: 'const a = this.#arr.at(-1);',
},
{
code: 'const a = this.#prop.arr[this.#prop.arr.length - 1];',
errors: [
{
messageId: 'preferAt',
data: {
name: 'this.#prop.arr',
},
},
],
output: 'const a = this.#prop.arr.at(-1);',
},
],
});

0 comments on commit 85e3083

Please sign in to comment.