@e18e/eslint-plugin 0.1.2 → 0.1.3

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
@@ -81,6 +81,7 @@ Copying these rules into your `rules` object will achieve the same effect as usi
81
81
  - ✅ = Yes / Enabled
82
82
  - ✖️ = No / Disabled
83
83
  - 💡 = Has suggestions (requires user confirmation for fixes)
84
+ - 🔶 = Optionally uses types (works without TypeScript but more powerful with it)
84
85
 
85
86
  ### Modernization
86
87
 
@@ -111,6 +112,8 @@ Copying these rules into your `rules` object will achieve the same effect as usi
111
112
  | [no-indexof-equality](./src/rules/no-indexof-equality.ts) | Prefer `startsWith()` for strings and direct array access over `indexOf()` equality checks | ✖️ | ✅ | ✅ |
112
113
  | [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 | ✅ | ✅ | ✖️ |
113
114
  | [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` | ✅ | ✅ | ✖️ |
115
+ | [prefer-date-now](./src/rules/prefer-date-now.ts) | Prefer `Date.now()` over `new Date().getTime()` and `+new Date()` | ✅ | ✅ | ✖️ |
116
+ | [prefer-regex-test](./src/rules/prefer-regex-test.ts) | Prefer `RegExp.test()` over `String.match()` and `RegExp.exec()` when only checking for match existence | ✅ | ✅ | 🔶 |
114
117
 
115
118
  ## License
116
119
 
@@ -4,6 +4,8 @@ export const performanceImprovements = (plugin) => ({
4
4
  },
5
5
  rules: {
6
6
  'e18e/prefer-array-from-map': 'error',
7
- 'e18e/prefer-timer-args': 'error'
7
+ 'e18e/prefer-timer-args': 'error',
8
+ 'e18e/prefer-date-now': 'error',
9
+ 'e18e/prefer-regex-test': 'error'
8
10
  }
9
11
  });
package/lib/main.js CHANGED
@@ -16,6 +16,8 @@ import { preferSpreadSyntax } from './rules/prefer-spread-syntax.js';
16
16
  import { preferUrlCanParse } from './rules/prefer-url-canparse.js';
17
17
  import { noIndexOfEquality } from './rules/no-indexof-equality.js';
18
18
  import { preferTimerArgs } from './rules/prefer-timer-args.js';
19
+ import { preferDateNow } from './rules/prefer-date-now.js';
20
+ import { preferRegexTest } from './rules/prefer-regex-test.js';
19
21
  import { rules as dependRules } from 'eslint-plugin-depend';
20
22
  const plugin = {
21
23
  meta: {
@@ -38,6 +40,8 @@ const plugin = {
38
40
  'prefer-url-canparse': preferUrlCanParse,
39
41
  'no-indexof-equality': noIndexOfEquality,
40
42
  'prefer-timer-args': preferTimerArgs,
43
+ 'prefer-date-now': preferDateNow,
44
+ 'prefer-regex-test': preferRegexTest,
41
45
  ...dependRules
42
46
  }
43
47
  };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const preferDateNow: Rule.RuleModule;
@@ -0,0 +1,71 @@
1
+ function getDateNowReplacement(node) {
2
+ if (node.type !== 'NewExpression' || node.arguments.length !== 0) {
3
+ return null;
4
+ }
5
+ if (node.callee.type === 'Identifier' && node.callee.name === 'Date') {
6
+ return 'Date.now()';
7
+ }
8
+ if (node.callee.type === 'MemberExpression' &&
9
+ node.callee.object.type === 'Identifier' &&
10
+ (node.callee.object.name === 'window' ||
11
+ node.callee.object.name === 'globalThis') &&
12
+ node.callee.property.type === 'Identifier' &&
13
+ node.callee.property.name === 'Date' &&
14
+ !node.callee.computed) {
15
+ return `${node.callee.object.name}.Date.now()`;
16
+ }
17
+ return null;
18
+ }
19
+ export const preferDateNow = {
20
+ meta: {
21
+ type: 'suggestion',
22
+ docs: {
23
+ description: 'Prefer Date.now() over new Date().getTime() and +new Date()',
24
+ recommended: true
25
+ },
26
+ fixable: 'code',
27
+ schema: [],
28
+ messages: {
29
+ preferDateNow: 'Use Date.now() to avoid allocating a new Date object.'
30
+ }
31
+ },
32
+ create(context) {
33
+ return {
34
+ // new Date().getTime()
35
+ CallExpression(node) {
36
+ if (node.callee.type === 'MemberExpression' &&
37
+ node.callee.object.type === 'NewExpression' &&
38
+ node.callee.property.type === 'Identifier' &&
39
+ node.callee.property.name === 'getTime' &&
40
+ !node.callee.computed &&
41
+ node.arguments.length === 0) {
42
+ const replacement = getDateNowReplacement(node.callee.object);
43
+ if (replacement) {
44
+ context.report({
45
+ node,
46
+ messageId: 'preferDateNow',
47
+ fix(fixer) {
48
+ return fixer.replaceText(node, replacement);
49
+ }
50
+ });
51
+ }
52
+ }
53
+ },
54
+ // +new Date()
55
+ UnaryExpression(node) {
56
+ if (node.operator === '+' && node.argument.type === 'NewExpression') {
57
+ const replacement = getDateNowReplacement(node.argument);
58
+ if (replacement) {
59
+ context.report({
60
+ node,
61
+ messageId: 'preferDateNow',
62
+ fix(fixer) {
63
+ return fixer.replaceText(node, replacement);
64
+ }
65
+ });
66
+ }
67
+ }
68
+ }
69
+ };
70
+ }
71
+ };
@@ -0,0 +1,4 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'preferTest';
3
+ export declare const preferRegexTest: TSESLint.RuleModule<MessageIds, []>;
4
+ export {};
@@ -0,0 +1,173 @@
1
+ import { tryGetTypedParserServices } from '../utils/typescript.js';
2
+ function isRegExpLiteral(node) {
3
+ return (node.type === 'Literal' &&
4
+ 'regex' in node &&
5
+ node.regex !== undefined &&
6
+ node.regex !== null);
7
+ }
8
+ /**
9
+ * Checks if a node is a `new RegExp(...)`
10
+ */
11
+ function isRegExpConstructor(node) {
12
+ if (node.type !== 'NewExpression') {
13
+ return false;
14
+ }
15
+ const { callee } = node;
16
+ // new RegExp()
17
+ if (callee.type === 'Identifier' && callee.name === 'RegExp') {
18
+ return true;
19
+ }
20
+ // new window.RegExp() or new globalThis.RegExp()
21
+ if (callee.type === 'MemberExpression' &&
22
+ callee.object.type === 'Identifier' &&
23
+ (callee.object.name === 'window' || callee.object.name === 'globalThis') &&
24
+ callee.property.type === 'Identifier' &&
25
+ callee.property.name === 'RegExp' &&
26
+ !callee.computed) {
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+ /**
32
+ * Checks if a node is a RegExp (literal or constructor)
33
+ */
34
+ function isRegExp(node) {
35
+ return (node !== null &&
36
+ node !== undefined &&
37
+ (isRegExpLiteral(node) || isRegExpConstructor(node)));
38
+ }
39
+ /**
40
+ * Checks if a node resolves to a RegExp using TypeScript types (when available)
41
+ */
42
+ function isRegExpByType(node, context) {
43
+ const services = tryGetTypedParserServices(context);
44
+ if (!services) {
45
+ return false;
46
+ }
47
+ const type = services.getTypeAtLocation(node);
48
+ if (!type) {
49
+ return false;
50
+ }
51
+ const checker = services.program.getTypeChecker();
52
+ const typeString = checker.typeToString(type);
53
+ return typeString === 'RegExp';
54
+ }
55
+ /**
56
+ * Checks if a node resolves to a RegExp (literal, constructor, or by type)
57
+ */
58
+ function resolvesToRegExp(node, context) {
59
+ if (isRegExpByType(node, context)) {
60
+ return true;
61
+ }
62
+ if (isRegExp(node)) {
63
+ return true;
64
+ }
65
+ if (node.type !== 'Identifier') {
66
+ return false;
67
+ }
68
+ const scope = context.sourceCode.getScope(node);
69
+ const variable = scope.references.find((ref) => ref.identifier === node)?.resolved;
70
+ if (!variable) {
71
+ return false;
72
+ }
73
+ for (const def of variable.defs) {
74
+ if (def.type === 'Variable' && def.node.type === 'VariableDeclarator') {
75
+ const init = def.node.init;
76
+ if (isRegExp(init)) {
77
+ return true;
78
+ }
79
+ }
80
+ }
81
+ return false;
82
+ }
83
+ /**
84
+ * Checks if a node is in a test/condition
85
+ */
86
+ function isInBooleanContext(node) {
87
+ const parent = node.parent;
88
+ if (!parent) {
89
+ return false;
90
+ }
91
+ // if/while/for/do-while test
92
+ if ((parent.type === 'IfStatement' && parent.test === node) ||
93
+ (parent.type === 'WhileStatement' && parent.test === node) ||
94
+ (parent.type === 'ForStatement' && parent.test === node) ||
95
+ (parent.type === 'DoWhileStatement' && parent.test === node)) {
96
+ return true;
97
+ }
98
+ // ternaries
99
+ if (parent.type === 'ConditionalExpression' && parent.test === node) {
100
+ return true;
101
+ }
102
+ // check the parent
103
+ if ((parent.type === 'UnaryExpression' && parent.operator === '!') ||
104
+ (parent.type === 'LogicalExpression' &&
105
+ (parent.operator === '&&' || parent.operator === '||'))) {
106
+ return isInBooleanContext(parent);
107
+ }
108
+ return false;
109
+ }
110
+ export const preferRegexTest = {
111
+ meta: {
112
+ type: 'suggestion',
113
+ docs: {
114
+ description: 'prefer `RegExp.test()` over `String.match()` and `RegExp.exec()` when only checking for match existence'
115
+ },
116
+ fixable: 'code',
117
+ messages: {
118
+ preferTest: 'Prefer `{{regex}}.test({{string}})` over `{{original}}` for boolean checks'
119
+ },
120
+ schema: []
121
+ },
122
+ defaultOptions: [],
123
+ create(context) {
124
+ return {
125
+ CallExpression(node) {
126
+ if (!isInBooleanContext(node)) {
127
+ return;
128
+ }
129
+ const { callee } = node;
130
+ if (callee.type !== 'MemberExpression') {
131
+ return;
132
+ }
133
+ const property = callee.property;
134
+ if (property.type !== 'Identifier' || node.arguments.length !== 1) {
135
+ return;
136
+ }
137
+ let regexNode;
138
+ let stringNode;
139
+ if (property.name === 'match') {
140
+ // str.match(regex)
141
+ stringNode = callee.object;
142
+ regexNode = node.arguments[0];
143
+ }
144
+ else if (property.name === 'exec') {
145
+ // regex.exec(str)
146
+ regexNode = callee.object;
147
+ stringNode = node.arguments[0];
148
+ }
149
+ else {
150
+ return;
151
+ }
152
+ if (!resolvesToRegExp(regexNode, context)) {
153
+ return;
154
+ }
155
+ const sourceCode = context.sourceCode;
156
+ const regexText = sourceCode.getText(regexNode);
157
+ const stringText = sourceCode.getText(stringNode);
158
+ context.report({
159
+ node,
160
+ messageId: 'preferTest',
161
+ data: {
162
+ regex: regexText,
163
+ string: stringText,
164
+ original: sourceCode.getText(node)
165
+ },
166
+ fix(fixer) {
167
+ return fixer.replaceText(node, `${regexText}.test(${stringText})`);
168
+ }
169
+ });
170
+ }
171
+ };
172
+ }
173
+ };
@@ -11,4 +11,5 @@ export interface ParserServices {
11
11
  getTypeAtLocation: (node: TSESTree.Node) => ts.Type;
12
12
  program: ts.Program;
13
13
  }
14
+ export declare function tryGetTypedParserServices(context: Readonly<TSESLint.RuleContext<string, unknown[]>>): ParserServicesWithTypeInformation | null;
14
15
  export declare function getTypedParserServices(context: Readonly<TSESLint.RuleContext<string, unknown[]>>): ParserServicesWithTypeInformation;
@@ -1,6 +1,13 @@
1
- export function getTypedParserServices(context) {
1
+ export function tryGetTypedParserServices(context) {
2
2
  if (context.sourceCode.parserServices?.program == null) {
3
- throw new Error(`You have used a rule which requires type information. Please ensure you have typescript-eslint setup alongside this plugin and configured to enable type-aware linting. See https://typescript-eslint.io/getting-started/typed-linting for more information.`);
3
+ return null;
4
4
  }
5
5
  return context.sourceCode.parserServices;
6
6
  }
7
+ export function getTypedParserServices(context) {
8
+ const services = tryGetTypedParserServices(context);
9
+ if (services === null) {
10
+ throw new Error(`You have used a rule which requires type information. Please ensure you have typescript-eslint setup alongside this plugin and configured to enable type-aware linting. See https://typescript-eslint.io/getting-started/typed-linting for more information.`);
11
+ }
12
+ return services;
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e18e/eslint-plugin",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "The official e18e ESLint plugin for modernizing code and improving performance.",
5
5
  "keywords": [
6
6
  "eslint",