@e18e/eslint-plugin 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -0
  3. package/lib/configs/modernization.d.ts +2 -0
  4. package/lib/configs/modernization.js +17 -0
  5. package/lib/configs/module-replacements.d.ts +2 -0
  6. package/lib/configs/module-replacements.js +8 -0
  7. package/lib/configs/performance-improvements.d.ts +2 -0
  8. package/lib/configs/performance-improvements.js +9 -0
  9. package/lib/configs/recommended.d.ts +2 -0
  10. package/lib/configs/recommended.js +18 -0
  11. package/lib/main.d.ts +3 -0
  12. package/lib/main.js +48 -0
  13. package/lib/rules/no-indexof-equality.d.ts +2 -0
  14. package/lib/rules/no-indexof-equality.js +90 -0
  15. package/lib/rules/prefer-array-at.d.ts +2 -0
  16. package/lib/rules/prefer-array-at.js +58 -0
  17. package/lib/rules/prefer-array-fill.d.ts +2 -0
  18. package/lib/rules/prefer-array-fill.js +120 -0
  19. package/lib/rules/prefer-array-from-map.d.ts +2 -0
  20. package/lib/rules/prefer-array-from-map.js +57 -0
  21. package/lib/rules/prefer-array-to-reversed.d.ts +2 -0
  22. package/lib/rules/prefer-array-to-reversed.js +42 -0
  23. package/lib/rules/prefer-array-to-sorted.d.ts +2 -0
  24. package/lib/rules/prefer-array-to-sorted.js +43 -0
  25. package/lib/rules/prefer-array-to-spliced.d.ts +2 -0
  26. package/lib/rules/prefer-array-to-spliced.js +43 -0
  27. package/lib/rules/prefer-exponentiation-operator.d.ts +2 -0
  28. package/lib/rules/prefer-exponentiation-operator.js +42 -0
  29. package/lib/rules/prefer-includes.d.ts +2 -0
  30. package/lib/rules/prefer-includes.js +131 -0
  31. package/lib/rules/prefer-nullish-coalescing.d.ts +2 -0
  32. package/lib/rules/prefer-nullish-coalescing.js +131 -0
  33. package/lib/rules/prefer-object-has-own.d.ts +2 -0
  34. package/lib/rules/prefer-object-has-own.js +71 -0
  35. package/lib/rules/prefer-optimized-indexof.d.ts +2 -0
  36. package/lib/rules/prefer-optimized-indexof.js +90 -0
  37. package/lib/rules/prefer-settimeout-args.d.ts +2 -0
  38. package/lib/rules/prefer-settimeout-args.js +175 -0
  39. package/lib/rules/prefer-spread-syntax.d.ts +2 -0
  40. package/lib/rules/prefer-spread-syntax.js +109 -0
  41. package/lib/rules/prefer-timer-args.d.ts +2 -0
  42. package/lib/rules/prefer-timer-args.js +176 -0
  43. package/lib/rules/prefer-url-canparse.d.ts +2 -0
  44. package/lib/rules/prefer-url-canparse.js +139 -0
  45. package/lib/test/setup.d.ts +1 -0
  46. package/lib/test/setup.js +10 -0
  47. package/lib/utils/ast.d.ts +15 -0
  48. package/lib/utils/ast.js +47 -0
  49. package/lib/utils/typescript.d.ts +14 -0
  50. package/lib/utils/typescript.js +6 -0
  51. package/package.json +56 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 e18e
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # @e18e/eslint-plugin
2
+
3
+ > The official e18e ESLint plugin for modernizing JavaScript/TypeScript code and improving performance.
4
+
5
+ > [!WARNING]
6
+ > This is an experimental, unpublished project for now. Once we have settled on the scope, we will publish it and announce it to start getting community feedback.
7
+
8
+ This plugin focuses on applying the e18e community's best practices and advise to JavaScript/TypeScript codebases.
9
+
10
+ ## Overview
11
+
12
+ There are a few categories of rules in this plugin:
13
+
14
+ - Modernization - New syntax and APIs which improve code readability and performance
15
+ - Module replacements - Community recommended alternatives to popular libraries, focused on performance and size
16
+ - Performance improvements - Patterns that can be optimized for better runtime performance
17
+
18
+ Each of these can be enabled individually, or you can use the recommended configuration to enable all rules.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install --save-dev @e18e/eslint-plugin
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ Add the plugin to your `eslint.config.js`:
29
+
30
+ ```ts
31
+ import e18e from '@e18e/eslint-plugin';
32
+
33
+ export default [
34
+ // Use the recommended configuration (includes all categories)
35
+ e18e.configs.recommended,
36
+
37
+ // Or use specific category configurations
38
+ e18e.configs.modernization,
39
+ e18e.configs.moduleReplacements,
40
+ e18e.configs.performanceImprovements,
41
+
42
+ // Or configure rules manually
43
+ {
44
+ plugins: {
45
+ e18e
46
+ },
47
+ rules: {
48
+ 'e18e/prefer-array-at': 'error',
49
+ 'e18e/prefer-array-fill': 'error',
50
+ 'e18e/prefer-includes': 'error'
51
+ }
52
+ }
53
+ ];
54
+ ```
55
+
56
+ ## Rules
57
+
58
+ **Legend:**
59
+ - ✅ = Yes / Enabled
60
+ - ✖️ = No / Disabled
61
+ - 💡 = Has suggestions (requires user confirmation for fixes)
62
+
63
+ ### Modernization
64
+
65
+ | Rule | Description | Recommended | Fixable | Requires Types |
66
+ |------|-------------|-------------|---------|----------------|
67
+ | [prefer-array-at](./src/rules/prefer-array-at.ts) | Prefer `Array.prototype.at()` over length-based indexing | ✅ | ✅ | ✖️ |
68
+ | [prefer-array-fill](./src/rules/prefer-array-fill.ts) | Prefer `Array.prototype.fill()` over `Array.from()` or `map()` with constant values | ✅ | ✅ | ✖️ |
69
+ | [prefer-includes](./src/rules/prefer-includes.ts) | Prefer `.includes()` over `indexOf()` comparisons for arrays and strings | ✅ | ✅ | ✖️ |
70
+ | [prefer-array-to-reversed](./src/rules/prefer-array-to-reversed.ts) | Prefer `Array.prototype.toReversed()` over copying and reversing arrays | ✅ | ✅ | ✖️ |
71
+ | [prefer-array-to-sorted](./src/rules/prefer-array-to-sorted.ts) | Prefer `Array.prototype.toSorted()` over copying and sorting arrays | ✅ | ✅ | ✖️ |
72
+ | [prefer-array-to-spliced](./src/rules/prefer-array-to-spliced.ts) | Prefer `Array.prototype.toSpliced()` over copying and splicing arrays | ✅ | ✅ | ✖️ |
73
+ | [prefer-exponentiation-operator](./src/rules/prefer-exponentiation-operator.ts) | Prefer the exponentiation operator `**` over `Math.pow()` | ✅ | ✅ | ✖️ |
74
+ | [prefer-nullish-coalescing](./src/rules/prefer-nullish-coalescing.ts) | Prefer nullish coalescing operator (`??` and `??=`) over verbose null checks | ✅ | ✅ | ✖️ |
75
+ | [prefer-object-has-own](./src/rules/prefer-object-has-own.ts) | Prefer `Object.hasOwn()` over `Object.prototype.hasOwnProperty.call()` and `obj.hasOwnProperty()` | ✅ | ✅ | ✖️ |
76
+ | [prefer-spread-syntax](./src/rules/prefer-spread-syntax.ts) | Prefer spread syntax over `Array.concat()`, `Array.from()`, `Object.assign({}, ...)`, and `Function.apply()` | ✅ | ✅ | ✖️ |
77
+ | [prefer-url-canparse](./src/rules/prefer-url-canparse.ts) | Prefer `URL.canParse()` over try-catch blocks for URL validation | ✅ | 💡 | ✖️ |
78
+
79
+ ### Module replacements
80
+
81
+ | Rule | Description | Recommended | Fixable | Requires Types |
82
+ |------|-------------|-------------|---------|----------------|
83
+ | ban-dependencies | Ban dependencies in favor of lighter alternatives | ✅ | ✖️ | ✖️ |
84
+
85
+ ### Performance improvements
86
+
87
+ | Rule | Description | Recommended | Fixable | Requires Types |
88
+ |------|-------------|-------------|---------|----------------|
89
+ | [no-indexof-equality](./src/rules/no-indexof-equality.ts) | Prefer `startsWith()` for strings and direct array access over `indexOf()` equality checks | ✖️ | ✅ | ✅ |
90
+ | [prefer-array-from-map](./src/rules/prefer-array-from-map.ts) | Prefer `Array.from(iterable, mapper)` over `[...iterable].map(mapper)` to avoid intermediate array allocation | ✅ | ✅ | ✖️ |
91
+ | [prefer-timer-args](./src/rules/prefer-timer-args.ts) | Prefer passing function and arguments directly to `setTimeout`/`setInterval` instead of wrapping in an arrow function or using `bind` | ✅ | ✅ | ✖️ |
92
+
93
+ ## License
94
+
95
+ MIT
@@ -0,0 +1,2 @@
1
+ import type { Linter, ESLint } from 'eslint';
2
+ export declare const modernization: (plugin: ESLint.Plugin) => Linter.Config;
@@ -0,0 +1,17 @@
1
+ export const modernization = (plugin) => ({
2
+ plugins: {
3
+ e18e: plugin
4
+ },
5
+ rules: {
6
+ 'e18e/prefer-array-at': 'error',
7
+ 'e18e/prefer-array-fill': 'error',
8
+ 'e18e/prefer-includes': 'error',
9
+ 'e18e/prefer-array-to-reversed': 'error',
10
+ 'e18e/prefer-array-to-sorted': 'error',
11
+ 'e18e/prefer-array-to-spliced': 'error',
12
+ 'e18e/prefer-nullish-coalescing': 'error',
13
+ 'e18e/prefer-object-has-own': 'error',
14
+ 'e18e/prefer-spread-syntax': 'error',
15
+ 'e18e/prefer-url-canparse': 'error'
16
+ }
17
+ });
@@ -0,0 +1,2 @@
1
+ import type { Linter, ESLint } from 'eslint';
2
+ export declare const moduleReplacements: (plugin: ESLint.Plugin) => Linter.Config;
@@ -0,0 +1,8 @@
1
+ export const moduleReplacements = (plugin) => ({
2
+ plugins: {
3
+ e18e: plugin
4
+ },
5
+ rules: {
6
+ 'e18e/ban-dependencies': 'error'
7
+ }
8
+ });
@@ -0,0 +1,2 @@
1
+ import type { Linter, ESLint } from 'eslint';
2
+ export declare const performanceImprovements: (plugin: ESLint.Plugin) => Linter.Config;
@@ -0,0 +1,9 @@
1
+ export const performanceImprovements = (plugin) => ({
2
+ plugins: {
3
+ e18e: plugin
4
+ },
5
+ rules: {
6
+ 'e18e/prefer-array-from-map': 'error',
7
+ 'e18e/prefer-timer-args': 'error'
8
+ }
9
+ });
@@ -0,0 +1,2 @@
1
+ import type { Linter, ESLint } from 'eslint';
2
+ export declare const recommended: (plugin: ESLint.Plugin) => Linter.Config;
@@ -0,0 +1,18 @@
1
+ import { modernization } from './modernization.js';
2
+ import { moduleReplacements } from './module-replacements.js';
3
+ import { performanceImprovements } from './performance-improvements.js';
4
+ export const recommended = (plugin) => {
5
+ const modernizationConfig = modernization(plugin);
6
+ const moduleReplacementsConfig = moduleReplacements(plugin);
7
+ const performanceImprovementsConfig = performanceImprovements(plugin);
8
+ return {
9
+ plugins: {
10
+ e18e: plugin
11
+ },
12
+ rules: {
13
+ ...modernizationConfig.rules,
14
+ ...moduleReplacementsConfig.rules,
15
+ ...performanceImprovementsConfig.rules
16
+ }
17
+ };
18
+ };
package/lib/main.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { ESLint } from 'eslint';
2
+ declare const plugin: ESLint.Plugin;
3
+ export default plugin;
package/lib/main.js ADDED
@@ -0,0 +1,48 @@
1
+ import { recommended } from './configs/recommended.js';
2
+ import { modernization } from './configs/modernization.js';
3
+ import { moduleReplacements } from './configs/module-replacements.js';
4
+ import { performanceImprovements } from './configs/performance-improvements.js';
5
+ import { preferArrayAt } from './rules/prefer-array-at.js';
6
+ import { preferArrayFill } from './rules/prefer-array-fill.js';
7
+ import { preferArrayFromMap } from './rules/prefer-array-from-map.js';
8
+ import { preferIncludes } from './rules/prefer-includes.js';
9
+ import { preferArrayToReversed } from './rules/prefer-array-to-reversed.js';
10
+ import { preferArrayToSorted } from './rules/prefer-array-to-sorted.js';
11
+ import { preferArrayToSpliced } from './rules/prefer-array-to-spliced.js';
12
+ import { preferExponentiationOperator } from './rules/prefer-exponentiation-operator.js';
13
+ import { preferNullishCoalescing } from './rules/prefer-nullish-coalescing.js';
14
+ import { preferObjectHasOwn } from './rules/prefer-object-has-own.js';
15
+ import { preferSpreadSyntax } from './rules/prefer-spread-syntax.js';
16
+ import { preferUrlCanParse } from './rules/prefer-url-canparse.js';
17
+ import { noIndexOfEquality } from './rules/no-indexof-equality.js';
18
+ import { preferTimerArgs } from './rules/prefer-timer-args.js';
19
+ import { rules as dependRules } from 'eslint-plugin-depend';
20
+ const plugin = {
21
+ meta: {
22
+ name: '@e18e/eslint-plugin',
23
+ namespace: 'e18e'
24
+ },
25
+ configs: {},
26
+ rules: {
27
+ 'prefer-array-at': preferArrayAt,
28
+ 'prefer-array-fill': preferArrayFill,
29
+ 'prefer-array-from-map': preferArrayFromMap,
30
+ 'prefer-includes': preferIncludes,
31
+ 'prefer-array-to-reversed': preferArrayToReversed,
32
+ 'prefer-array-to-sorted': preferArrayToSorted,
33
+ 'prefer-array-to-spliced': preferArrayToSpliced,
34
+ 'prefer-exponentiation-operator': preferExponentiationOperator,
35
+ 'prefer-nullish-coalescing': preferNullishCoalescing,
36
+ 'prefer-object-has-own': preferObjectHasOwn,
37
+ 'prefer-spread-syntax': preferSpreadSyntax,
38
+ 'prefer-url-canparse': preferUrlCanParse,
39
+ 'no-indexof-equality': noIndexOfEquality,
40
+ 'prefer-timer-args': preferTimerArgs,
41
+ ...dependRules
42
+ }
43
+ };
44
+ plugin.configs.recommended = recommended(plugin);
45
+ plugin.configs.modernization = modernization(plugin);
46
+ plugin.configs.moduleReplacements = moduleReplacements(plugin);
47
+ plugin.configs.performanceImprovements = performanceImprovements(plugin);
48
+ export default plugin;
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const noIndexOfEquality: Rule.RuleModule;
@@ -0,0 +1,90 @@
1
+ import { getTypedParserServices } from '../utils/typescript.js';
2
+ export const noIndexOfEquality = {
3
+ meta: {
4
+ type: 'suggestion',
5
+ docs: {
6
+ description: 'Prefer optimized alternatives to `indexOf()` equality checks',
7
+ recommended: false
8
+ },
9
+ fixable: 'code',
10
+ schema: [],
11
+ messages: {
12
+ preferDirectAccess: 'Use direct array access `{{array}}[{{index}}] === {{item}}` instead of `indexOf() === {{index}}`',
13
+ preferStartsWith: 'Use `.startsWith()` instead of `indexOf() === 0` for strings'
14
+ }
15
+ },
16
+ create(context) {
17
+ const sourceCode = context.sourceCode;
18
+ const services = getTypedParserServices(context);
19
+ const checker = services.program.getTypeChecker();
20
+ return {
21
+ BinaryExpression(node) {
22
+ if (node.operator !== '===' && node.operator !== '==') {
23
+ return;
24
+ }
25
+ let indexOfCall;
26
+ let compareIndex;
27
+ if (node.left.type === 'CallExpression' &&
28
+ node.right.type === 'Literal' &&
29
+ typeof node.right.value === 'number' &&
30
+ node.right.value >= 0) {
31
+ indexOfCall = node.left;
32
+ compareIndex = node.right.value;
33
+ }
34
+ else if (node.right.type === 'CallExpression' &&
35
+ node.left.type === 'Literal' &&
36
+ typeof node.left.value === 'number' &&
37
+ node.left.value >= 0) {
38
+ indexOfCall = node.right;
39
+ compareIndex = node.left.value;
40
+ }
41
+ if (!indexOfCall || compareIndex === undefined) {
42
+ return;
43
+ }
44
+ if (indexOfCall.callee.type !== 'MemberExpression' ||
45
+ indexOfCall.callee.property.type !== 'Identifier' ||
46
+ indexOfCall.callee.property.name !== 'indexOf') {
47
+ return;
48
+ }
49
+ if (indexOfCall.arguments.length !== 1) {
50
+ return;
51
+ }
52
+ const objectNode = indexOfCall.callee.object;
53
+ const searchArg = indexOfCall.arguments[0];
54
+ const type = services.getTypeAtLocation(objectNode);
55
+ if (!type) {
56
+ return;
57
+ }
58
+ const objectText = sourceCode.getText(objectNode);
59
+ const searchText = sourceCode.getText(searchArg);
60
+ const stringType = checker.getStringType();
61
+ if (checker.isTypeAssignableTo(type, stringType)) {
62
+ if (compareIndex === 0) {
63
+ context.report({
64
+ node,
65
+ messageId: 'preferStartsWith',
66
+ fix(fixer) {
67
+ return fixer.replaceText(node, `${objectText}.startsWith(${searchText})`);
68
+ }
69
+ });
70
+ }
71
+ return;
72
+ }
73
+ if (checker.isArrayType(type)) {
74
+ context.report({
75
+ node,
76
+ messageId: 'preferDirectAccess',
77
+ data: {
78
+ array: objectText,
79
+ item: searchText,
80
+ index: String(compareIndex)
81
+ },
82
+ fix(fixer) {
83
+ return fixer.replaceText(node, `${objectText}[${compareIndex}] === ${searchText}`);
84
+ }
85
+ });
86
+ }
87
+ }
88
+ };
89
+ }
90
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const preferArrayAt: Rule.RuleModule;
@@ -0,0 +1,58 @@
1
+ export const preferArrayAt = {
2
+ meta: {
3
+ type: 'suggestion',
4
+ docs: {
5
+ description: 'Prefer Array.prototype.at() over length-based indexing',
6
+ recommended: true
7
+ },
8
+ fixable: 'code',
9
+ schema: [],
10
+ messages: {
11
+ preferAt: 'Use .at(-1) instead of [{{array}}.length - 1]'
12
+ }
13
+ },
14
+ create(context) {
15
+ const sourceCode = context.sourceCode;
16
+ return {
17
+ MemberExpression(node) {
18
+ if (!node.computed || !node.property) {
19
+ return;
20
+ }
21
+ if (node.property.type !== 'BinaryExpression') {
22
+ return;
23
+ }
24
+ const propertyExpr = node.property;
25
+ if (propertyExpr.operator !== '-') {
26
+ return;
27
+ }
28
+ if (propertyExpr.right.type !== 'Literal' ||
29
+ propertyExpr.right.value !== 1) {
30
+ return;
31
+ }
32
+ if (propertyExpr.left.type !== 'MemberExpression') {
33
+ return;
34
+ }
35
+ const leftMember = propertyExpr.left;
36
+ if (leftMember.property.type !== 'Identifier' ||
37
+ leftMember.property.name !== 'length') {
38
+ return;
39
+ }
40
+ const arrayText = sourceCode.getText(node.object);
41
+ const lengthArrayText = sourceCode.getText(leftMember.object);
42
+ if (arrayText !== lengthArrayText) {
43
+ return;
44
+ }
45
+ context.report({
46
+ node,
47
+ messageId: 'preferAt',
48
+ data: {
49
+ array: arrayText
50
+ },
51
+ fix(fixer) {
52
+ return fixer.replaceText(node, `${arrayText}.at(-1)`);
53
+ }
54
+ });
55
+ }
56
+ };
57
+ }
58
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const preferArrayFill: Rule.RuleModule;
@@ -0,0 +1,120 @@
1
+ function isConstantCallback(func) {
2
+ return (func.params.length === 0 &&
3
+ (func.body.type !== 'BlockStatement' ||
4
+ (func.body.body.length === 1 &&
5
+ func.body.body[0]?.type === 'ReturnStatement')));
6
+ }
7
+ function getCallbackValueText(func, sourceCode) {
8
+ if (func.body.type === 'BlockStatement') {
9
+ const returnStmt = func.body.body[0];
10
+ if (returnStmt?.type === 'ReturnStatement' && returnStmt.argument) {
11
+ return sourceCode.getText(returnStmt.argument);
12
+ }
13
+ return undefined;
14
+ }
15
+ return sourceCode.getText(func.body);
16
+ }
17
+ export const preferArrayFill = {
18
+ meta: {
19
+ type: 'suggestion',
20
+ docs: {
21
+ description: 'Prefer Array.prototype.fill() over Array.from or map with constant values',
22
+ recommended: true
23
+ },
24
+ fixable: 'code',
25
+ schema: [],
26
+ messages: {
27
+ preferFillArrayFrom: 'Use Array.from({length: {{length}}}).fill({{value}}) instead of Array.from with a constant callback',
28
+ preferFillSpreadMap: 'Use Array({{length}}).fill({{value}}) instead of spread Array with map'
29
+ }
30
+ },
31
+ create(context) {
32
+ const sourceCode = context.sourceCode;
33
+ return {
34
+ CallExpression(node) {
35
+ // Check for Array.from({length: n}, () => value)
36
+ if (node.callee.type === 'MemberExpression' &&
37
+ node.callee.object.type === 'Identifier' &&
38
+ node.callee.object.name === 'Array' &&
39
+ node.callee.property.type === 'Identifier' &&
40
+ node.callee.property.name === 'from' &&
41
+ node.arguments.length === 2) {
42
+ const firstArg = node.arguments[0];
43
+ const secondArg = node.arguments[1];
44
+ // Check if first arg is {length: n}
45
+ if (firstArg?.type === 'ObjectExpression' &&
46
+ firstArg.properties.length === 1 &&
47
+ firstArg.properties[0]?.type === 'Property' &&
48
+ firstArg.properties[0].key.type === 'Identifier' &&
49
+ firstArg.properties[0].key.name === 'length') {
50
+ // Check if second arg is a constant callback
51
+ if (secondArg &&
52
+ (secondArg.type === 'ArrowFunctionExpression' ||
53
+ secondArg.type === 'FunctionExpression') &&
54
+ isConstantCallback(secondArg)) {
55
+ const lengthValue = firstArg.properties[0].value;
56
+ const lengthText = sourceCode.getText(lengthValue);
57
+ const valueText = getCallbackValueText(secondArg, sourceCode);
58
+ if (!valueText) {
59
+ return;
60
+ }
61
+ context.report({
62
+ node,
63
+ messageId: 'preferFillArrayFrom',
64
+ data: {
65
+ length: lengthText,
66
+ value: valueText
67
+ },
68
+ fix(fixer) {
69
+ return fixer.replaceText(node, `Array.from({length: ${lengthText}}).fill(${valueText})`);
70
+ }
71
+ });
72
+ }
73
+ }
74
+ }
75
+ // Check for [...Array(n)].map(() => value)
76
+ if (node.callee.type === 'MemberExpression' &&
77
+ node.callee.property.type === 'Identifier' &&
78
+ node.callee.property.name === 'map' &&
79
+ node.callee.object.type === 'ArrayExpression' &&
80
+ node.callee.object.elements.length === 1 &&
81
+ node.arguments.length === 1) {
82
+ const spreadElement = node.callee.object.elements[0];
83
+ const callback = node.arguments[0];
84
+ if (spreadElement?.type === 'SpreadElement' &&
85
+ spreadElement.argument.type === 'CallExpression' &&
86
+ spreadElement.argument.callee.type === 'Identifier' &&
87
+ spreadElement.argument.callee.name === 'Array' &&
88
+ spreadElement.argument.arguments.length === 1) {
89
+ const arrayArg = spreadElement.argument.arguments[0];
90
+ // Check if callback is a constant function
91
+ if (callback &&
92
+ (callback.type === 'ArrowFunctionExpression' ||
93
+ callback.type === 'FunctionExpression') &&
94
+ isConstantCallback(callback)) {
95
+ if (!arrayArg) {
96
+ return;
97
+ }
98
+ const lengthText = sourceCode.getText(arrayArg);
99
+ const valueText = getCallbackValueText(callback, sourceCode);
100
+ if (!valueText) {
101
+ return;
102
+ }
103
+ context.report({
104
+ node,
105
+ messageId: 'preferFillSpreadMap',
106
+ data: {
107
+ length: lengthText,
108
+ value: valueText
109
+ },
110
+ fix(fixer) {
111
+ return fixer.replaceText(node, `Array(${lengthText}).fill(${valueText})`);
112
+ }
113
+ });
114
+ }
115
+ }
116
+ }
117
+ }
118
+ };
119
+ }
120
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const preferArrayFromMap: Rule.RuleModule;
@@ -0,0 +1,57 @@
1
+ export const preferArrayFromMap = {
2
+ meta: {
3
+ type: 'suggestion',
4
+ docs: {
5
+ description: 'Prefer Array.from(iterable, mapper) over [...iterable].map(mapper) to avoid intermediate array allocation',
6
+ recommended: true
7
+ },
8
+ fixable: 'code',
9
+ schema: [],
10
+ messages: {
11
+ preferArrayFrom: 'Use Array.from({{iterable}}, {{mapper}}) instead of [...{{iterable}}].map({{mapper}}) to avoid creating an intermediate array'
12
+ }
13
+ },
14
+ create(context) {
15
+ const sourceCode = context.sourceCode;
16
+ return {
17
+ CallExpression(node) {
18
+ // Check if this is a .map() call
19
+ if (node.callee.type !== 'MemberExpression') {
20
+ return;
21
+ }
22
+ if (node.callee.property.type !== 'Identifier' ||
23
+ node.callee.property.name !== 'map') {
24
+ return;
25
+ }
26
+ // Check if .map() is being called on an array literal with spread
27
+ if (node.callee.object.type !== 'ArrayExpression') {
28
+ return;
29
+ }
30
+ const arrayExpr = node.callee.object;
31
+ // Check if the array has exactly one element and it's a spread element
32
+ if (arrayExpr.elements.length !== 1 ||
33
+ arrayExpr.elements[0]?.type !== 'SpreadElement') {
34
+ return;
35
+ }
36
+ // Check if map has exactly one argument (the mapper function)
37
+ if (node.arguments.length !== 1) {
38
+ return;
39
+ }
40
+ const spreadElement = arrayExpr.elements[0];
41
+ const iterableText = sourceCode.getText(spreadElement.argument);
42
+ const mapperText = sourceCode.getText(node.arguments[0]);
43
+ context.report({
44
+ node,
45
+ messageId: 'preferArrayFrom',
46
+ data: {
47
+ iterable: iterableText,
48
+ mapper: mapperText
49
+ },
50
+ fix(fixer) {
51
+ return fixer.replaceText(node, `Array.from(${iterableText}, ${mapperText})`);
52
+ }
53
+ });
54
+ }
55
+ };
56
+ }
57
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const preferArrayToReversed: Rule.RuleModule;
@@ -0,0 +1,42 @@
1
+ import { getArrayFromCopyPattern } from '../utils/ast.js';
2
+ export const preferArrayToReversed = {
3
+ meta: {
4
+ type: 'suggestion',
5
+ docs: {
6
+ description: 'Prefer Array.prototype.toReversed() over copying and reversing arrays',
7
+ recommended: true
8
+ },
9
+ fixable: 'code',
10
+ schema: [],
11
+ messages: {
12
+ preferToReversed: 'Use {{array}}.toReversed() instead of copying and reversing'
13
+ }
14
+ },
15
+ create(context) {
16
+ const sourceCode = context.sourceCode;
17
+ return {
18
+ CallExpression(node) {
19
+ if (node.callee.type !== 'MemberExpression' ||
20
+ node.callee.property.type !== 'Identifier' ||
21
+ node.callee.property.name !== 'reverse') {
22
+ return;
23
+ }
24
+ const reverseCallee = node.callee.object;
25
+ const arrayNode = getArrayFromCopyPattern(reverseCallee);
26
+ if (arrayNode) {
27
+ const arrayText = sourceCode.getText(arrayNode);
28
+ context.report({
29
+ node,
30
+ messageId: 'preferToReversed',
31
+ data: {
32
+ array: arrayText
33
+ },
34
+ fix(fixer) {
35
+ return fixer.replaceText(node, `${arrayText}.toReversed()`);
36
+ }
37
+ });
38
+ }
39
+ }
40
+ };
41
+ }
42
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const preferArrayToSorted: Rule.RuleModule;