@e18e/eslint-plugin 0.2.0 → 0.3.0

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.
package/README.md CHANGED
@@ -141,13 +141,13 @@ Read more at the
141
141
  | [prefer-array-at](./src/rules/prefer-array-at.ts) | Prefer `Array.prototype.at()` over length-based indexing | ✅ | ✅ | 🔶 |
142
142
  | [prefer-array-fill](./src/rules/prefer-array-fill.ts) | Prefer `Array.prototype.fill()` over `Array.from()` or `map()` with constant values | ✅ | ✅ | ✖️ |
143
143
  | [prefer-includes](./src/rules/prefer-includes.ts) | Prefer `.includes()` over `indexOf()` comparisons for arrays and strings | ✅ | ✅ | ✖️ |
144
- | [prefer-array-to-reversed](./src/rules/prefer-array-to-reversed.ts) | Prefer `Array.prototype.toReversed()` over copying and reversing arrays | ✅ | ✅ | ✖️ |
144
+ | [prefer-array-to-reversed](./src/rules/prefer-array-to-reversed.ts) | Prefer `Array.prototype.toReversed()` over copying and reversing arrays | ✅ | ✅ | 🔶 |
145
145
  | [prefer-array-to-sorted](./src/rules/prefer-array-to-sorted.ts) | Prefer `Array.prototype.toSorted()` over copying and sorting arrays | ✅ | ✅ | 🔶 |
146
146
  | [prefer-array-to-spliced](./src/rules/prefer-array-to-spliced.ts) | Prefer `Array.prototype.toSpliced()` over copying and splicing arrays | ✅ | ✅ | ✖️ |
147
147
  | [prefer-exponentiation-operator](./src/rules/prefer-exponentiation-operator.ts) | Prefer the exponentiation operator `**` over `Math.pow()` | ✅ | ✅ | ✖️ |
148
148
  | [prefer-nullish-coalescing](./src/rules/prefer-nullish-coalescing.ts) | Prefer nullish coalescing operator (`??` and `??=`) over verbose null checks | ✅ | ✅ | ✖️ |
149
149
  | [prefer-object-has-own](./src/rules/prefer-object-has-own.ts) | Prefer `Object.hasOwn()` over `Object.prototype.hasOwnProperty.call()` and `obj.hasOwnProperty()` | ✅ | ✅ | ✖️ |
150
- | [prefer-spread-syntax](./src/rules/prefer-spread-syntax.ts) | Prefer spread syntax over `Array.concat()`, `Array.from()`, `Object.assign({}, ...)`, and `Function.apply()` | ✅ | ✅ | ✖️ |
150
+ | [prefer-spread-syntax](./src/rules/prefer-spread-syntax.ts) | Prefer spread syntax over `Array.concat()`, `Array.from()`, `Object.assign({}, ...)`, and `Function.apply()` | ✅ | ✅ | 🔶 |
151
151
  | [prefer-url-canparse](./src/rules/prefer-url-canparse.ts) | Prefer `URL.canParse()` over try-catch blocks for URL validation | ✅ | 💡 | ✖️ |
152
152
 
153
153
  ### Module replacements
@@ -166,13 +166,13 @@ Read more at the
166
166
  | [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` | ✅ | ✅ | ✖️ |
167
167
  | [prefer-date-now](./src/rules/prefer-date-now.ts) | Prefer `Date.now()` over `new Date().getTime()` and `+new Date()` | ✅ | ✅ | ✖️ |
168
168
  | [prefer-regex-test](./src/rules/prefer-regex-test.ts) | Prefer `RegExp.test()` over `String.match()` and `RegExp.exec()` when only checking for match existence | ✅ | ✅ | 🔶 |
169
- | [prefer-static-regex](./src/rules/prefer-static-regex.ts) | Prefer defining regular expressions at module scope to avoid re-compilation on every function call | ✅ | ✖️ | ✖️ |
169
+ | [prefer-static-regex](./src/rules/prefer-static-regex.ts) | Prefer defining regular expressions at module scope to avoid re-compilation on every function call | ✅ | ✖️ | 🔶 |
170
170
  | [prefer-inline-equality](./src/rules/prefer-inline-equality.ts) | Prefer inline equality checks over temporary object creation for simple comparisons | ✖️ | ✅ | 🔶 |
171
171
 
172
172
  ## Sponsors
173
173
 
174
174
  <p align="center">
175
- <a href="https://github.com/sponsors/e18e">
175
+ <a href="https://e18e.dev/sponsor">
176
176
  <img src="https://e18e.dev/sponsors.svg" alt="e18e community sponsors" />
177
177
  </a>
178
178
  </p>
@@ -1,18 +1,59 @@
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')));
1
+ function isConstantExpression(node) {
2
+ switch (node.type) {
3
+ case 'Literal':
4
+ case 'Identifier':
5
+ return true;
6
+ case 'CallExpression':
7
+ case 'NewExpression':
8
+ case 'ObjectExpression':
9
+ case 'ArrayExpression':
10
+ return false;
11
+ case 'MemberExpression':
12
+ return (node.object.type !== 'Super' &&
13
+ isConstantExpression(node.object) &&
14
+ (!node.computed || isConstantExpression(node.property)));
15
+ case 'UnaryExpression':
16
+ return isConstantExpression(node.argument);
17
+ case 'BinaryExpression':
18
+ case 'LogicalExpression':
19
+ return (node.left.type !== 'PrivateIdentifier' &&
20
+ isConstantExpression(node.left) &&
21
+ isConstantExpression(node.right));
22
+ case 'ConditionalExpression':
23
+ return (isConstantExpression(node.test) &&
24
+ isConstantExpression(node.consequent) &&
25
+ isConstantExpression(node.alternate));
26
+ case 'TemplateLiteral':
27
+ return node.expressions.every((expr) => isConstantExpression(expr));
28
+ default:
29
+ return false;
30
+ }
6
31
  }
7
- function getCallbackValueText(func, sourceCode) {
32
+ function getCallbackValueNode(func) {
8
33
  if (func.body.type === 'BlockStatement') {
34
+ if (func.body.body.length !== 1)
35
+ return undefined;
9
36
  const returnStmt = func.body.body[0];
10
37
  if (returnStmt?.type === 'ReturnStatement' && returnStmt.argument) {
11
- return sourceCode.getText(returnStmt.argument);
38
+ return returnStmt.argument;
12
39
  }
13
40
  return undefined;
14
41
  }
15
- return sourceCode.getText(func.body);
42
+ return func.body;
43
+ }
44
+ function isConstantCallback(func) {
45
+ if (func.params.length !== 0) {
46
+ return false;
47
+ }
48
+ const valueNode = getCallbackValueNode(func);
49
+ if (!valueNode) {
50
+ return false;
51
+ }
52
+ return isConstantExpression(valueNode);
53
+ }
54
+ function getCallbackValueText(func, sourceCode) {
55
+ const valueNode = getCallbackValueNode(func);
56
+ return valueNode ? sourceCode.getText(valueNode) : undefined;
16
57
  }
17
58
  export const preferArrayFill = {
18
59
  meta: {
@@ -1,2 +1,4 @@
1
- import type { Rule } from 'eslint';
2
- export declare const preferArrayToReversed: Rule.RuleModule;
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'preferToReversed';
3
+ export declare const preferArrayToReversed: TSESLint.RuleModule<MessageIds, []>;
4
+ export {};
@@ -1,10 +1,10 @@
1
- import { getArrayFromCopyPattern } from '../utils/ast.js';
1
+ import { getArrayFromCopyPattern, needsParensForPropertyAccess, isCopyPatternOptional } from '../utils/ast.js';
2
+ import { isArrayType } from '../utils/typescript.js';
2
3
  export const preferArrayToReversed = {
3
4
  meta: {
4
5
  type: 'suggestion',
5
6
  docs: {
6
- description: 'Prefer Array.prototype.toReversed() over copying and reversing arrays',
7
- recommended: true
7
+ description: 'Prefer Array.prototype.toReversed() over copying and reversing arrays'
8
8
  },
9
9
  fixable: 'code',
10
10
  schema: [],
@@ -12,6 +12,7 @@ export const preferArrayToReversed = {
12
12
  preferToReversed: 'Use {{array}}.toReversed() instead of copying and reversing'
13
13
  }
14
14
  },
15
+ defaultOptions: [],
15
16
  create(context) {
16
17
  const sourceCode = context.sourceCode;
17
18
  return {
@@ -24,15 +25,24 @@ export const preferArrayToReversed = {
24
25
  const reverseCallee = node.callee.object;
25
26
  const arrayNode = getArrayFromCopyPattern(reverseCallee);
26
27
  if (arrayNode) {
27
- const arrayText = sourceCode.getText(arrayNode);
28
+ if (!isArrayType(arrayNode, context)) {
29
+ return;
30
+ }
31
+ const rawText = sourceCode.getText(arrayNode);
32
+ const arrayText = needsParensForPropertyAccess(arrayNode)
33
+ ? `(${rawText})`
34
+ : rawText;
35
+ const optionalChain = isCopyPatternOptional(reverseCallee)
36
+ ? '?.'
37
+ : '.';
28
38
  context.report({
29
39
  node,
30
40
  messageId: 'preferToReversed',
31
41
  data: {
32
- array: arrayText
42
+ array: rawText
33
43
  },
34
44
  fix(fixer) {
35
- return fixer.replaceText(node, `${arrayText}.toReversed()`);
45
+ return fixer.replaceText(node, `${arrayText}${optionalChain}toReversed()`);
36
46
  }
37
47
  });
38
48
  }
@@ -1,4 +1,4 @@
1
- import { getArrayFromCopyPattern, formatArguments } from '../utils/ast.js';
1
+ import { getArrayFromCopyPattern, formatArguments, needsParensForPropertyAccess, isCopyPatternOptional } from '../utils/ast.js';
2
2
  import { isArrayType } from '../utils/typescript.js';
3
3
  export const preferArrayToSorted = {
4
4
  meta: {
@@ -28,16 +28,20 @@ export const preferArrayToSorted = {
28
28
  if (!isArrayType(arrayNode, context)) {
29
29
  return;
30
30
  }
31
- const arrayText = sourceCode.getText(arrayNode);
31
+ const rawText = sourceCode.getText(arrayNode);
32
+ const arrayText = needsParensForPropertyAccess(arrayNode)
33
+ ? `(${rawText})`
34
+ : rawText;
32
35
  const argsText = formatArguments(node.arguments, sourceCode);
36
+ const optionalChain = isCopyPatternOptional(sortCallee) ? '?.' : '.';
33
37
  context.report({
34
38
  node,
35
39
  messageId: 'preferToSorted',
36
40
  data: {
37
- array: arrayText
41
+ array: rawText
38
42
  },
39
43
  fix(fixer) {
40
- return fixer.replaceText(node, `${arrayText}.toSorted(${argsText})`);
44
+ return fixer.replaceText(node, `${arrayText}${optionalChain}toSorted(${argsText})`);
41
45
  }
42
46
  });
43
47
  }
@@ -1,4 +1,4 @@
1
- import { getArrayFromCopyPattern, formatArguments } from '../utils/ast.js';
1
+ import { getArrayFromCopyPattern, formatArguments, needsParensForPropertyAccess, isCopyPatternOptional } from '../utils/ast.js';
2
2
  export const preferArrayToSpliced = {
3
3
  meta: {
4
4
  type: 'suggestion',
@@ -24,16 +24,22 @@ export const preferArrayToSpliced = {
24
24
  const spliceCallee = node.callee.object;
25
25
  const arrayNode = getArrayFromCopyPattern(spliceCallee);
26
26
  if (arrayNode) {
27
- const arrayText = sourceCode.getText(arrayNode);
27
+ const rawText = sourceCode.getText(arrayNode);
28
+ const arrayText = needsParensForPropertyAccess(arrayNode)
29
+ ? `(${rawText})`
30
+ : rawText;
28
31
  const argsText = formatArguments(node.arguments, sourceCode);
32
+ const optionalChain = isCopyPatternOptional(spliceCallee)
33
+ ? '?.'
34
+ : '.';
29
35
  context.report({
30
36
  node,
31
37
  messageId: 'preferToSpliced',
32
38
  data: {
33
- array: arrayText
39
+ array: rawText
34
40
  },
35
41
  fix(fixer) {
36
- return fixer.replaceText(node, `${arrayText}.toSpliced(${argsText})`);
42
+ return fixer.replaceText(node, `${arrayText}${optionalChain}toSpliced(${argsText})`);
37
43
  }
38
44
  });
39
45
  }
@@ -1,2 +1,4 @@
1
- import type { Rule } from 'eslint';
2
- export declare const preferSpreadSyntax: Rule.RuleModule;
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'preferSpreadArray' | 'preferSpreadArrayFrom' | 'preferSpreadObject' | 'preferSpreadFunction';
3
+ export declare const preferSpreadSyntax: TSESLint.RuleModule<MessageIds, []>;
4
+ export {};
@@ -1,3 +1,4 @@
1
+ import { isArrayType } from '../utils/typescript.js';
1
2
  function isNullOrUndefined(node) {
2
3
  if (node.type === 'Literal' && node.value === null) {
3
4
  return true;
@@ -8,8 +9,7 @@ export const preferSpreadSyntax = {
8
9
  meta: {
9
10
  type: 'suggestion',
10
11
  docs: {
11
- description: 'Prefer spread syntax over Array.concat(), Array.from(), Object.assign({}, ...), and Function.apply()',
12
- recommended: true
12
+ description: 'Prefer spread syntax over Array.concat(), Array.from(), Object.assign({}, ...), and Function.apply()'
13
13
  },
14
14
  fixable: 'code',
15
15
  schema: [],
@@ -20,6 +20,7 @@ export const preferSpreadSyntax = {
20
20
  preferSpreadFunction: 'Use spread syntax fn(...args) instead of fn.apply(null/undefined, args)'
21
21
  }
22
22
  },
23
+ defaultOptions: [],
23
24
  create(context) {
24
25
  const sourceCode = context.sourceCode;
25
26
  return {
@@ -36,12 +37,25 @@ export const preferSpreadSyntax = {
36
37
  node.arguments.length > 0 &&
37
38
  !(node.callee.object.type === 'Identifier' &&
38
39
  node.callee.object.name === 'Buffer')) {
40
+ // If type info is available, only flag when the receiver is an array
41
+ if (!isArrayType(node.callee.object, context)) {
42
+ return;
43
+ }
39
44
  const arrayText = sourceCode.getText(node.callee.object);
40
- const argTexts = node.arguments.map((arg) => sourceCode.getText(arg));
41
- const spreadParts = [arrayText, ...argTexts]
42
- .map((part) => `...${part}`)
43
- .join(', ');
44
- replacement = `[${spreadParts}]`;
45
+ const parts = [`...${arrayText}`];
46
+ for (const arg of node.arguments) {
47
+ const argText = sourceCode.getText(arg);
48
+ if (arg.type === 'SpreadElement') {
49
+ parts.push(argText);
50
+ }
51
+ else if (isArrayType(arg, context)) {
52
+ parts.push(`...${argText}`);
53
+ }
54
+ else {
55
+ parts.push(argText);
56
+ }
57
+ }
58
+ replacement = `[${parts.join(', ')}]`;
45
59
  messageId = 'preferSpreadArray';
46
60
  }
47
61
  // Array.from(iterable) with no mapper
@@ -1,3 +1,4 @@
1
+ const statefulFlags = /[gy]/;
1
2
  function isStaticNewRegExp(node) {
2
3
  if (node.callee.type !== 'Identifier' ||
3
4
  node.callee.name !== 'RegExp' ||
@@ -5,7 +6,14 @@ function isStaticNewRegExp(node) {
5
6
  node.arguments.length > 2) {
6
7
  return false;
7
8
  }
8
- return node.arguments.every((arg) => arg.type === 'Literal' && typeof arg.value === 'string');
9
+ if (!node.arguments.every((arg) => arg.type === 'Literal' && typeof arg.value === 'string')) {
10
+ return false;
11
+ }
12
+ const flagsArg = node.arguments[1];
13
+ if (flagsArg && statefulFlags.test(flagsArg.value)) {
14
+ return false;
15
+ }
16
+ return true;
9
17
  }
10
18
  export const preferStaticRegex = {
11
19
  meta: {
@@ -22,6 +30,10 @@ export const preferStaticRegex = {
22
30
  create(context) {
23
31
  return {
24
32
  ':function Literal[regex]'(node) {
33
+ const { flags } = node.regex;
34
+ if (statefulFlags.test(flags)) {
35
+ return;
36
+ }
25
37
  context.report({ node, messageId: 'preferStatic' });
26
38
  },
27
39
  ':function NewExpression'(node) {
@@ -22,6 +22,16 @@ export declare function isCopyCall(node: CallExpression): boolean;
22
22
  */
23
23
  export declare function getArrayFromCopyPattern(node: TSESTree.Node): TSESTree.Node | null;
24
24
  export declare function getArrayFromCopyPattern(node: Node): Node | null;
25
+ /**
26
+ * Checks if a node needs to be wrapped in parentheses when used as the
27
+ * object of a property access (e.g. `expr.foo()`).
28
+ */
29
+ export declare function needsParensForPropertyAccess(node: AnyNode): boolean;
30
+ /**
31
+ * Checks if a copy pattern (node passed to getArrayFromCopyPattern) uses
32
+ * optional chaining on the copy method call, e.g. `arr?.slice()`.
33
+ */
34
+ export declare function isCopyPatternOptional(node: AnyNode): boolean;
25
35
  /**
26
36
  * Formats arguments from a CallExpression as a comma-separated string.
27
37
  */
package/lib/utils/ast.js CHANGED
@@ -81,6 +81,38 @@ export function getArrayFromCopyPattern(node) {
81
81
  }
82
82
  return null;
83
83
  }
84
+ /**
85
+ * Checks if a node needs to be wrapped in parentheses when used as the
86
+ * object of a property access (e.g. `expr.foo()`).
87
+ */
88
+ export function needsParensForPropertyAccess(node) {
89
+ switch (node.type) {
90
+ case 'Identifier':
91
+ case 'MemberExpression':
92
+ case 'CallExpression':
93
+ case 'Literal':
94
+ case 'ArrayExpression':
95
+ case 'ObjectExpression':
96
+ case 'TemplateLiteral':
97
+ case 'TaggedTemplateExpression':
98
+ case 'ThisExpression':
99
+ case 'NewExpression':
100
+ case 'ChainExpression':
101
+ case 'MetaProperty':
102
+ return false;
103
+ default:
104
+ return true;
105
+ }
106
+ }
107
+ /**
108
+ * Checks if a copy pattern (node passed to getArrayFromCopyPattern) uses
109
+ * optional chaining on the copy method call, e.g. `arr?.slice()`.
110
+ */
111
+ export function isCopyPatternOptional(node) {
112
+ return (node.type === 'CallExpression' &&
113
+ node.callee.type === 'MemberExpression' &&
114
+ node.callee.optional === true);
115
+ }
84
116
  export function formatArguments(args, sourceCode) {
85
117
  if (args.length === 0) {
86
118
  return '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e18e/eslint-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The official e18e ESLint plugin for modernizing code and improving performance.",
5
5
  "keywords": [
6
6
  "eslint",
@@ -35,24 +35,24 @@
35
35
  "lint:format": "prettier --check src"
36
36
  },
37
37
  "devDependencies": {
38
- "@eslint/js": "^9.39.2",
39
- "@eslint/json": "^0.14.0",
40
- "@types/node": "^25.0.10",
41
- "@typescript-eslint/rule-tester": "^8.53.1",
42
- "@typescript-eslint/typescript-estree": "^8.53.1",
43
- "@vitest/coverage-v8": "^4.0.18",
44
- "eslint": "^9.39.2",
45
- "eslint-plugin-eslint-plugin": "^7.3.0",
46
- "jsonc-eslint-parser": "^2.4.2",
47
- "oxlint": "^1.41.0",
38
+ "@eslint/js": "^10.0.1",
39
+ "@eslint/json": "^1.1.0",
40
+ "@types/node": "^25.5.0",
41
+ "@typescript-eslint/rule-tester": "^8.57.0",
42
+ "@typescript-eslint/typescript-estree": "^8.57.0",
43
+ "@vitest/coverage-v8": "^4.1.0",
44
+ "eslint": "^10.0.3",
45
+ "eslint-plugin-eslint-plugin": "^7.3.2",
46
+ "jsonc-eslint-parser": "^3.1.0",
47
+ "oxlint": "^1.55.0",
48
48
  "prettier": "^3.8.1",
49
49
  "typescript": "^5.9.3",
50
- "typescript-eslint": "^8.53.1",
50
+ "typescript-eslint": "^8.57.0",
51
51
  "vitest": "^4.0.14"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "eslint": "^9.0.0 || ^10.0.0",
55
- "oxlint": "^1.41.0"
55
+ "oxlint": "^1.55.0"
56
56
  },
57
57
  "peerDependenciesMeta": {
58
58
  "eslint": {
@@ -63,6 +63,6 @@
63
63
  }
64
64
  },
65
65
  "dependencies": {
66
- "eslint-plugin-depend": "^1.4.0"
66
+ "eslint-plugin-depend": "^1.5.0"
67
67
  }
68
68
  }