@e18e/eslint-plugin 0.1.4 → 0.2.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
@@ -142,7 +142,7 @@ Read more at the
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
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 | ✅ | ✅ | ✖️ |
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 | ✅ | ✅ | ✖️ |
@@ -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://github.com/sponsors/e18e">
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,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
1
  import { getArrayFromCopyPattern, formatArguments } 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,6 +25,9 @@ export const preferArrayToSorted = {
24
25
  const sortCallee = node.callee.object;
25
26
  const arrayNode = getArrayFromCopyPattern(sortCallee);
26
27
  if (arrayNode) {
28
+ if (!isArrayType(arrayNode, context)) {
29
+ return;
30
+ }
27
31
  const arrayText = sourceCode.getText(arrayNode);
28
32
  const argsText = formatArguments(node.arguments, sourceCode);
29
33
  context.report({
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const preferStaticRegex: Rule.RuleModule;
@@ -0,0 +1,34 @@
1
+ function isStaticNewRegExp(node) {
2
+ if (node.callee.type !== 'Identifier' ||
3
+ node.callee.name !== 'RegExp' ||
4
+ node.arguments.length === 0 ||
5
+ node.arguments.length > 2) {
6
+ return false;
7
+ }
8
+ return node.arguments.every((arg) => arg.type === 'Literal' && typeof arg.value === 'string');
9
+ }
10
+ export const preferStaticRegex = {
11
+ meta: {
12
+ type: 'suggestion',
13
+ docs: {
14
+ description: 'Prefer defining regular expressions at module scope to avoid re-compilation on every function call',
15
+ recommended: true
16
+ },
17
+ schema: [],
18
+ messages: {
19
+ preferStatic: 'Move this regular expression to module scope to avoid re-compilation on every call.'
20
+ }
21
+ },
22
+ create(context) {
23
+ return {
24
+ ':function Literal[regex]'(node) {
25
+ context.report({ node, messageId: 'preferStatic' });
26
+ },
27
+ ':function NewExpression'(node) {
28
+ if (isStaticNewRegExp(node)) {
29
+ context.report({ node, messageId: 'preferStatic' });
30
+ }
31
+ }
32
+ };
33
+ }
34
+ };
@@ -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,10 @@ 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;
24
25
  /**
25
26
  * Formats arguments from a CallExpression as a comma-separated string.
26
27
  */
28
+ export declare function formatArguments(args: TSESTree.CallExpression['arguments'], sourceCode: Readonly<TSESLint.SourceCode>): string;
27
29
  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) &&
@@ -84,12 +81,11 @@ export function getArrayFromCopyPattern(node) {
84
81
  }
85
82
  return null;
86
83
  }
87
- /**
88
- * Formats arguments from a CallExpression as a comma-separated string.
89
- */
90
84
  export function formatArguments(args, sourceCode) {
91
85
  if (args.length === 0) {
92
86
  return '';
93
87
  }
94
- return args.map((arg) => sourceCode.getText(arg)).join(', ');
88
+ return args
89
+ .map((arg) => sourceCode.getText(arg))
90
+ .join(', ');
95
91
  }
@@ -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.2.0",
4
4
  "description": "The official e18e ESLint plugin for modernizing code and improving performance.",
5
5
  "keywords": [
6
6
  "eslint",
@@ -51,7 +51,16 @@
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.41.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "eslint": {
59
+ "optional": true
60
+ },
61
+ "oxlint": {
62
+ "optional": true
63
+ }
55
64
  },
56
65
  "dependencies": {
57
66
  "eslint-plugin-depend": "^1.4.0"