@e18e/eslint-plugin 0.1.4 → 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 | ✅ | ✅ | ✖️ |
145
- | [prefer-array-to-sorted](./src/rules/prefer-array-to-sorted.ts) | Prefer `Array.prototype.toSorted()` over copying and sorting arrays | ✅ | ✅ | ✖️ |
144
+ | [prefer-array-to-reversed](./src/rules/prefer-array-to-reversed.ts) | Prefer `Array.prototype.toReversed()` over copying and reversing arrays | ✅ | ✅ | 🔶 |
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,6 +166,16 @@ 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 | ✅ | ✖️ | 🔶 |
170
+ | [prefer-inline-equality](./src/rules/prefer-inline-equality.ts) | Prefer inline equality checks over temporary object creation for simple comparisons | ✖️ | ✅ | 🔶 |
171
+
172
+ ## Sponsors
173
+
174
+ <p align="center">
175
+ <a href="https://e18e.dev/sponsor">
176
+ <img src="https://e18e.dev/sponsors.svg" alt="e18e community sponsors" />
177
+ </a>
178
+ </p>
169
179
 
170
180
  ## License
171
181
 
@@ -7,6 +7,7 @@ export const performanceImprovements = (plugin) => ({
7
7
  'e18e/prefer-timer-args': 'error',
8
8
  'e18e/prefer-date-now': 'error',
9
9
  'e18e/prefer-regex-test': 'error',
10
- 'e18e/prefer-array-some': 'error'
10
+ 'e18e/prefer-array-some': 'error',
11
+ 'e18e/prefer-static-regex': 'error'
11
12
  }
12
13
  });
package/lib/main.js CHANGED
@@ -19,6 +19,8 @@ import { preferTimerArgs } from './rules/prefer-timer-args.js';
19
19
  import { preferDateNow } from './rules/prefer-date-now.js';
20
20
  import { preferRegexTest } from './rules/prefer-regex-test.js';
21
21
  import { preferArraySome } from './rules/prefer-array-some.js';
22
+ import { preferStaticRegex } from './rules/prefer-static-regex.js';
23
+ import { preferInlineEquality } from './rules/prefer-inline-equality.js';
22
24
  import { rules as dependRules } from 'eslint-plugin-depend';
23
25
  const plugin = {
24
26
  meta: {
@@ -44,6 +46,8 @@ const plugin = {
44
46
  'prefer-date-now': preferDateNow,
45
47
  'prefer-regex-test': preferRegexTest,
46
48
  'prefer-array-some': preferArraySome,
49
+ 'prefer-static-regex': preferStaticRegex,
50
+ 'prefer-inline-equality': preferInlineEquality,
47
51
  ...dependRules
48
52
  }
49
53
  };
@@ -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,2 +1,4 @@
1
- import type { Rule } from 'eslint';
2
- export declare const preferArrayToSorted: Rule.RuleModule;
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'preferToSorted';
3
+ export declare const preferArrayToSorted: TSESLint.RuleModule<MessageIds, []>;
4
+ export {};
@@ -1,10 +1,10 @@
1
- import { getArrayFromCopyPattern, formatArguments } from '../utils/ast.js';
1
+ import { getArrayFromCopyPattern, formatArguments, needsParensForPropertyAccess, isCopyPatternOptional } from '../utils/ast.js';
2
+ import { isArrayType } from '../utils/typescript.js';
2
3
  export const preferArrayToSorted = {
3
4
  meta: {
4
5
  type: 'suggestion',
5
6
  docs: {
6
- description: 'Prefer Array.prototype.toSorted() over copying and sorting arrays',
7
- recommended: true
7
+ description: 'Prefer Array.prototype.toSorted() over copying and sorting arrays'
8
8
  },
9
9
  fixable: 'code',
10
10
  schema: [],
@@ -12,6 +12,7 @@ export const preferArrayToSorted = {
12
12
  preferToSorted: 'Use {{array}}.toSorted() instead of copying and sorting'
13
13
  }
14
14
  },
15
+ defaultOptions: [],
15
16
  create(context) {
16
17
  const sourceCode = context.sourceCode;
17
18
  return {
@@ -24,16 +25,23 @@ export const preferArrayToSorted = {
24
25
  const sortCallee = node.callee.object;
25
26
  const arrayNode = getArrayFromCopyPattern(sortCallee);
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;
28
35
  const argsText = formatArguments(node.arguments, sourceCode);
36
+ const optionalChain = isCopyPatternOptional(sortCallee) ? '?.' : '.';
29
37
  context.report({
30
38
  node,
31
39
  messageId: 'preferToSorted',
32
40
  data: {
33
- array: arrayText
41
+ array: rawText
34
42
  },
35
43
  fix(fixer) {
36
- return fixer.replaceText(node, `${arrayText}.toSorted(${argsText})`);
44
+ return fixer.replaceText(node, `${arrayText}${optionalChain}toSorted(${argsText})`);
37
45
  }
38
46
  });
39
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
  }
@@ -0,0 +1,4 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'preferEquality';
3
+ export declare const preferInlineEquality: TSESLint.RuleModule<MessageIds, []>;
4
+ export {};
@@ -0,0 +1,170 @@
1
+ import { isArrayType, isSetType, tryGetTypedParserServices } from '../utils/typescript.js';
2
+ /**
3
+ * Checks if a node is safe to repeat (i.e. no side effects)
4
+ */
5
+ function isSafeToRepeat(node) {
6
+ if (node.type === 'Identifier') {
7
+ return true;
8
+ }
9
+ if (node.type === 'Literal') {
10
+ return true;
11
+ }
12
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
13
+ return true;
14
+ }
15
+ if (node.type === 'MemberExpression' && !node.computed) {
16
+ return isSafeToRepeat(node.object);
17
+ }
18
+ return false;
19
+ }
20
+ /**
21
+ * Checks if a node is NaN
22
+ */
23
+ function isNaN(node) {
24
+ return node.type === 'Identifier' && node.name === 'NaN';
25
+ }
26
+ /**
27
+ * Checks if an array element is an identifier or literal
28
+ */
29
+ function isSimpleElement(node) {
30
+ if (isNaN(node)) {
31
+ return false;
32
+ }
33
+ return node.type === 'Identifier' || node.type === 'Literal';
34
+ }
35
+ /**
36
+ * Checks if the node is negated
37
+ */
38
+ function isNegated(node) {
39
+ return (node.parent !== undefined &&
40
+ node.parent.type === 'UnaryExpression' &&
41
+ node.parent.operator === '!');
42
+ }
43
+ /**
44
+ * Checks if the replacement expression needs to be wrapped in parentheses
45
+ * based on the parent node context
46
+ */
47
+ function needsParentheses(node) {
48
+ if (!node.parent) {
49
+ return false;
50
+ }
51
+ switch (node.parent.type) {
52
+ case 'CallExpression':
53
+ case 'NewExpression':
54
+ case 'MemberExpression':
55
+ case 'ConditionalExpression':
56
+ case 'BinaryExpression':
57
+ case 'LogicalExpression':
58
+ case 'UnaryExpression':
59
+ case 'TaggedTemplateExpression':
60
+ case 'SpreadElement':
61
+ case 'AwaitExpression':
62
+ return true;
63
+ default:
64
+ return false;
65
+ }
66
+ }
67
+ function checkArrayIncludes(node, context) {
68
+ const { callee } = node;
69
+ if (callee.type !== 'MemberExpression') {
70
+ return;
71
+ }
72
+ const property = callee.property;
73
+ if (property.type !== 'Identifier' ||
74
+ property.name !== 'includes' ||
75
+ callee.computed) {
76
+ return;
77
+ }
78
+ if (node.arguments.length !== 1) {
79
+ return;
80
+ }
81
+ const arrayNode = callee.object;
82
+ if (arrayNode.type !== 'ArrayExpression') {
83
+ return;
84
+ }
85
+ const elements = arrayNode.elements;
86
+ if (elements.length === 0 || elements.length > 6) {
87
+ return;
88
+ }
89
+ const val = node.arguments[0];
90
+ if (!isSafeToRepeat(val)) {
91
+ return;
92
+ }
93
+ const hasTypes = tryGetTypedParserServices(context) !== null;
94
+ for (const element of elements) {
95
+ if (element === null) {
96
+ return;
97
+ }
98
+ if (element.type === 'SpreadElement') {
99
+ const arg = element.argument;
100
+ if (!isSafeToRepeat(arg)) {
101
+ return;
102
+ }
103
+ if (!hasTypes ||
104
+ (!isArrayType(arg, context) && !isSetType(arg, context))) {
105
+ return;
106
+ }
107
+ }
108
+ else if (!isSimpleElement(element)) {
109
+ return;
110
+ }
111
+ }
112
+ const sourceCode = context.sourceCode;
113
+ const negated = isNegated(node);
114
+ const operator = negated ? '!==' : '===';
115
+ const joiner = negated ? ' && ' : ' || ';
116
+ const parts = [];
117
+ for (const element of elements) {
118
+ if (element === null) {
119
+ return;
120
+ }
121
+ if (element.type === 'SpreadElement') {
122
+ const argText = sourceCode.getText(element.argument);
123
+ const valText = sourceCode.getText(val);
124
+ const method = isSetType(element.argument, context) ? 'has' : 'includes';
125
+ if (negated) {
126
+ parts.push(`!${argText}.${method}(${valText})`);
127
+ }
128
+ else {
129
+ parts.push(`${argText}.${method}(${valText})`);
130
+ }
131
+ }
132
+ else {
133
+ const elemText = sourceCode.getText(element);
134
+ const valText = sourceCode.getText(val);
135
+ parts.push(`${elemText} ${operator} ${valText}`);
136
+ }
137
+ }
138
+ const replacement = parts.join(joiner);
139
+ const reportNode = negated ? node.parent : node;
140
+ const needsParens = needsParentheses(reportNode);
141
+ const fixText = needsParens ? `(${replacement})` : replacement;
142
+ context.report({
143
+ node: reportNode,
144
+ messageId: 'preferEquality',
145
+ fix(fixer) {
146
+ return fixer.replaceText(reportNode, fixText);
147
+ }
148
+ });
149
+ }
150
+ export const preferInlineEquality = {
151
+ meta: {
152
+ type: 'suggestion',
153
+ docs: {
154
+ description: 'Prefer inline equality checks over temporary object creation for simple comparisons'
155
+ },
156
+ fixable: 'code',
157
+ messages: {
158
+ preferEquality: 'Avoid creating a temporary array just to call `.includes()`. Use equality checks instead.'
159
+ },
160
+ schema: []
161
+ },
162
+ defaultOptions: [],
163
+ create(context) {
164
+ return {
165
+ CallExpression(node) {
166
+ checkArrayIncludes(node, context);
167
+ }
168
+ };
169
+ }
170
+ };
@@ -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
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const preferStaticRegex: Rule.RuleModule;
@@ -0,0 +1,46 @@
1
+ const statefulFlags = /[gy]/;
2
+ function isStaticNewRegExp(node) {
3
+ if (node.callee.type !== 'Identifier' ||
4
+ node.callee.name !== 'RegExp' ||
5
+ node.arguments.length === 0 ||
6
+ node.arguments.length > 2) {
7
+ return false;
8
+ }
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;
17
+ }
18
+ export const preferStaticRegex = {
19
+ meta: {
20
+ type: 'suggestion',
21
+ docs: {
22
+ description: 'Prefer defining regular expressions at module scope to avoid re-compilation on every function call',
23
+ recommended: true
24
+ },
25
+ schema: [],
26
+ messages: {
27
+ preferStatic: 'Move this regular expression to module scope to avoid re-compilation on every call.'
28
+ }
29
+ },
30
+ create(context) {
31
+ return {
32
+ ':function Literal[regex]'(node) {
33
+ const { flags } = node.regex;
34
+ if (statefulFlags.test(flags)) {
35
+ return;
36
+ }
37
+ context.report({ node, messageId: 'preferStatic' });
38
+ },
39
+ ':function NewExpression'(node) {
40
+ if (isStaticNewRegExp(node)) {
41
+ context.report({ node, messageId: 'preferStatic' });
42
+ }
43
+ }
44
+ };
45
+ }
46
+ };
@@ -1,7 +1,7 @@
1
1
  import type { CallExpression, Node, Expression } from 'estree';
2
- import type { TSESTree } from '@typescript-eslint/utils';
3
- import type { Rule, SourceCode } from 'eslint';
4
- type AnyNode = (Node & Rule.NodeParentExtension) | TSESTree.Node;
2
+ import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
3
+ import type { SourceCode } from 'eslint';
4
+ export type AnyNode = Node | TSESTree.Node;
5
5
  /**
6
6
  * Checks if a node is in a boolean context (where the result is only used as truthy/falsy).
7
7
  * e.g. if conditions, while loops, ternary tests, logical operators
@@ -20,9 +20,20 @@ export declare function isCopyCall(node: CallExpression): boolean;
20
20
  /**
21
21
  * Extracts the array node from array copy patterns.
22
22
  */
23
+ export declare function getArrayFromCopyPattern(node: TSESTree.Node): TSESTree.Node | null;
23
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;
24
35
  /**
25
36
  * Formats arguments from a CallExpression as a comma-separated string.
26
37
  */
38
+ export declare function formatArguments(args: TSESTree.CallExpression['arguments'], sourceCode: Readonly<TSESLint.SourceCode>): string;
27
39
  export declare function formatArguments(args: CallExpression['arguments'], sourceCode: SourceCode): string;
28
- export {};
package/lib/utils/ast.js CHANGED
@@ -68,9 +68,6 @@ export function isCopyCall(node) {
68
68
  }
69
69
  return false;
70
70
  }
71
- /**
72
- * Extracts the array node from array copy patterns.
73
- */
74
71
  export function getArrayFromCopyPattern(node) {
75
72
  if (node.type === 'CallExpression' &&
76
73
  isCopyCall(node) &&
@@ -85,11 +82,42 @@ export function getArrayFromCopyPattern(node) {
85
82
  return null;
86
83
  }
87
84
  /**
88
- * Formats arguments from a CallExpression as a comma-separated string.
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()`).
89
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
+ }
90
116
  export function formatArguments(args, sourceCode) {
91
117
  if (args.length === 0) {
92
118
  return '';
93
119
  }
94
- return args.map((arg) => sourceCode.getText(arg)).join(', ');
120
+ return args
121
+ .map((arg) => sourceCode.getText(arg))
122
+ .join(', ');
95
123
  }
@@ -18,6 +18,11 @@ export declare function getTypedParserServices(context: Readonly<TSESLint.RuleCo
18
18
  * Returns true if types are unavailable (to avoid false negatives)
19
19
  */
20
20
  export declare function isArrayType(node: TSESTree.Node, context: Readonly<TSESLint.RuleContext<string, unknown[]>>): boolean;
21
+ /**
22
+ * Checks if a node's type is a Set
23
+ * Returns true if types are unavailable (to avoid false negatives)
24
+ */
25
+ export declare function isSetType(node: TSESTree.Node, context: Readonly<TSESLint.RuleContext<string, unknown[]>>): boolean;
21
26
  /**
22
27
  * Checks if a node's type is a string
23
28
  * Returns false if types are unavailable
@@ -50,6 +50,24 @@ export function isArrayType(node, context) {
50
50
  }
51
51
  return false;
52
52
  }
53
+ const setTypePattern = /^(Readonly)?Set</;
54
+ /**
55
+ * Checks if a node's type is a Set
56
+ * Returns true if types are unavailable (to avoid false negatives)
57
+ */
58
+ export function isSetType(node, context) {
59
+ const services = tryGetTypedParserServices(context);
60
+ if (!services) {
61
+ return true;
62
+ }
63
+ const type = services.getTypeAtLocation(node);
64
+ if (!type) {
65
+ return true;
66
+ }
67
+ const checker = services.program.getTypeChecker();
68
+ const typeString = checker.typeToString(type);
69
+ return setTypePattern.test(typeString);
70
+ }
53
71
  /**
54
72
  * Checks if a node's type is a string
55
73
  * Returns false if types are unavailable
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e18e/eslint-plugin",
3
- "version": "0.1.4",
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,25 +35,34 @@
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
- "eslint": "^9.0.0"
54
+ "eslint": "^9.0.0 || ^10.0.0",
55
+ "oxlint": "^1.55.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "eslint": {
59
+ "optional": true
60
+ },
61
+ "oxlint": {
62
+ "optional": true
63
+ }
55
64
  },
56
65
  "dependencies": {
57
- "eslint-plugin-depend": "^1.4.0"
66
+ "eslint-plugin-depend": "^1.5.0"
58
67
  }
59
68
  }