@e18e/eslint-plugin 0.0.1 → 0.1.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.
package/README.md CHANGED
@@ -2,9 +2,6 @@
2
2
 
3
3
  > The official e18e ESLint plugin for modernizing JavaScript/TypeScript code and improving performance.
4
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
5
  This plugin focuses on applying the e18e community's best practices and advise to JavaScript/TypeScript codebases.
9
6
 
10
7
  ## Overview
@@ -53,6 +50,19 @@ export default [
53
50
  ];
54
51
  ```
55
52
 
53
+ ## Usage with oxlint
54
+
55
+ If you're using oxlint, you can enable the e18e plugin by adding it to your `.oxlintrc.json` file:
56
+
57
+ ```json
58
+ {
59
+ "jsPlugins": ["@e18e/eslint-plugin"],
60
+ "rules": {
61
+ "e18e/prefer-includes": "error"
62
+ }
63
+ }
64
+ ```
65
+
56
66
  ## Rules
57
67
 
58
68
  **Legend:**
package/lib/main.js CHANGED
@@ -19,7 +19,7 @@ import { preferTimerArgs } from './rules/prefer-timer-args.js';
19
19
  import { rules as dependRules } from 'eslint-plugin-depend';
20
20
  const plugin = {
21
21
  meta: {
22
- name: '@e18e/eslint-plugin',
22
+ name: 'e18e',
23
23
  namespace: 'e18e'
24
24
  },
25
25
  configs: {},
@@ -1,2 +1,4 @@
1
- import type { Rule } from 'eslint';
2
- export declare const noIndexOfEquality: Rule.RuleModule;
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'preferDirectAccess' | 'preferStartsWith';
3
+ export declare const noIndexOfEquality: TSESLint.RuleModule<MessageIds, []>;
4
+ export {};
@@ -3,8 +3,7 @@ export const noIndexOfEquality = {
3
3
  meta: {
4
4
  type: 'suggestion',
5
5
  docs: {
6
- description: 'Prefer optimized alternatives to `indexOf()` equality checks',
7
- recommended: false
6
+ description: 'Prefer optimized alternatives to `indexOf()` equality checks'
8
7
  },
9
8
  fixable: 'code',
10
9
  schema: [],
@@ -13,6 +12,7 @@ export const noIndexOfEquality = {
13
12
  preferStartsWith: 'Use `.startsWith()` instead of `indexOf() === 0` for strings'
14
13
  }
15
14
  },
15
+ defaultOptions: [],
16
16
  create(context) {
17
17
  const sourceCode = context.sourceCode;
18
18
  const services = getTypedParserServices(context);
@@ -42,6 +42,12 @@ export const preferArrayAt = {
42
42
  if (arrayText !== lengthArrayText) {
43
43
  return;
44
44
  }
45
+ const parent = node.parent;
46
+ if (parent &&
47
+ parent.type === 'AssignmentExpression' &&
48
+ parent.left === node) {
49
+ return;
50
+ }
45
51
  context.report({
46
52
  node,
47
53
  messageId: 'preferAt',
@@ -1,5 +1,5 @@
1
- import type { Rule } from 'eslint';
2
- import type { TSNode, TSToken, TSESTree } from '@typescript-eslint/typescript-estree';
1
+ import type { TSNode, TSToken, TSESTree, ParserServicesWithTypeInformation } from '@typescript-eslint/typescript-estree';
2
+ import type { TSESLint } from '@typescript-eslint/utils';
3
3
  import type ts from 'typescript';
4
4
  export interface ParserServices {
5
5
  emitDecoratorMetadata: boolean | undefined;
@@ -11,4 +11,4 @@ export interface ParserServices {
11
11
  getTypeAtLocation: (node: TSESTree.Node) => ts.Type;
12
12
  program: ts.Program;
13
13
  }
14
- export declare function getTypedParserServices(context: Readonly<Rule.RuleContext>): ParserServices;
14
+ export declare function getTypedParserServices(context: Readonly<TSESLint.RuleContext<string, unknown[]>>): ParserServicesWithTypeInformation;
@@ -1,5 +1,5 @@
1
1
  export function getTypedParserServices(context) {
2
- if (context.sourceCode.parserServices.program == null) {
2
+ if (context.sourceCode.parserServices?.program == null) {
3
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.`);
4
4
  }
5
5
  return context.sourceCode.parserServices;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e18e/eslint-plugin",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
4
  "description": "The official e18e ESLint plugin for modernizing code and improving performance.",
5
5
  "keywords": [
6
6
  "eslint",
@@ -42,6 +42,7 @@
42
42
  "@vitest/coverage-v8": "^4.0.16",
43
43
  "eslint": "^9.39.2",
44
44
  "eslint-plugin-eslint-plugin": "^7.2.0",
45
+ "oxlint": "^1.34.0",
45
46
  "prettier": "^3.7.4",
46
47
  "typescript": "^5.9.3",
47
48
  "typescript-eslint": "^8.50.0",
@@ -1,2 +0,0 @@
1
- import type { Rule } from 'eslint';
2
- export declare const preferOptimizedIndexof: Rule.RuleModule;
@@ -1,90 +0,0 @@
1
- import { getTypedParserServices } from '../utils/typescript.js';
2
- export const preferOptimizedIndexof = {
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
- };
@@ -1,2 +0,0 @@
1
- import type { Rule } from 'eslint';
2
- export declare const preferSetTimeoutArgs: Rule.RuleModule;
@@ -1,175 +0,0 @@
1
- function isNullOrUndefined(node) {
2
- if (node.type === 'Literal' && node.value === null) {
3
- return true;
4
- }
5
- return node.type === 'Identifier' && node.name === 'undefined';
6
- }
7
- function isSafeArgument(arg) {
8
- if (arg.type === 'SpreadElement') {
9
- return arg.argument.type === 'Identifier';
10
- }
11
- switch (arg.type) {
12
- case 'Identifier':
13
- case 'Literal':
14
- case 'TemplateLiteral':
15
- return true;
16
- case 'MemberExpression':
17
- if (arg.object.type === 'Super' || arg.property.type === 'PrivateIdentifier') {
18
- return false;
19
- }
20
- if (!isSafeArgument(arg.object)) {
21
- return false;
22
- }
23
- if (arg.computed) {
24
- return isSafeArgument(arg.property);
25
- }
26
- return true;
27
- case 'ArrayExpression':
28
- return arg.elements.every((el) => el === null || isSafeArgument(el));
29
- case 'ObjectExpression':
30
- return arg.properties.every((prop) => {
31
- if (prop.type === 'SpreadElement') {
32
- return isSafeArgument(prop.argument);
33
- }
34
- const valueType = prop.value.type;
35
- if (valueType === 'ObjectPattern' ||
36
- valueType === 'ArrayPattern' ||
37
- valueType === 'RestElement' ||
38
- valueType === 'AssignmentPattern') {
39
- return false;
40
- }
41
- return isSafeArgument(prop.value);
42
- });
43
- case 'UnaryExpression':
44
- case 'UpdateExpression':
45
- return isSafeArgument(arg.argument);
46
- case 'BinaryExpression':
47
- case 'LogicalExpression':
48
- return arg.left.type !== 'PrivateIdentifier' && isSafeArgument(arg.left) && isSafeArgument(arg.right);
49
- case 'ConditionalExpression':
50
- return (isSafeArgument(arg.test) &&
51
- isSafeArgument(arg.consequent) &&
52
- isSafeArgument(arg.alternate));
53
- // CallExpression, NewExpression, etc. are NOT safe
54
- default:
55
- return false;
56
- }
57
- }
58
- export const preferSetTimeoutArgs = {
59
- meta: {
60
- type: 'suggestion',
61
- docs: {
62
- description: 'Prefer passing function and arguments directly to setTimeout instead of wrapping in an arrow function or using bind',
63
- recommended: true
64
- },
65
- fixable: 'code',
66
- schema: [],
67
- messages: {
68
- preferArgs: 'Pass function and arguments directly to setTimeout to avoid allocating an extra function'
69
- }
70
- },
71
- create(context) {
72
- const sourceCode = context.sourceCode;
73
- return {
74
- CallExpression(node) {
75
- // Check if this is setTimeout, window.setTimeout, or globalThis.setTimeout
76
- const isSetTimeout = (node.callee.type === 'Identifier' &&
77
- node.callee.name === 'setTimeout') ||
78
- (node.callee.type === 'MemberExpression' &&
79
- node.callee.object.type === 'Identifier' &&
80
- (node.callee.object.name === 'window' ||
81
- node.callee.object.name === 'globalThis') &&
82
- node.callee.property.type === 'Identifier' &&
83
- node.callee.property.name === 'setTimeout');
84
- if (!isSetTimeout) {
85
- return;
86
- }
87
- if (node.arguments.length < 2) {
88
- return;
89
- }
90
- const firstArg = node.arguments[0];
91
- if (!firstArg || firstArg.type === 'SpreadElement') {
92
- return;
93
- }
94
- const delayText = sourceCode.getText(node.arguments[1]);
95
- // Preserve the original setTimeout call style
96
- const setTimeoutCall = sourceCode.getText(node.callee);
97
- let replacement = null;
98
- // simple arrow functions, e.g. () => fn(args)
99
- if (firstArg.type === 'ArrowFunctionExpression') {
100
- const arrowFn = firstArg;
101
- // skip if it is a block body
102
- if (arrowFn.body.type === 'BlockStatement') {
103
- return;
104
- }
105
- // skip if it has parameters
106
- if (arrowFn.params.length > 0) {
107
- return;
108
- }
109
- if (arrowFn.body.type !== 'CallExpression') {
110
- return;
111
- }
112
- const callExpression = arrowFn.body;
113
- const callee = callExpression.callee;
114
- const callArgs = callExpression.arguments;
115
- // Check if any argument contains a call expression or other unsafe construct
116
- // If so, transforming would change when those expressions are evaluated
117
- if (!callArgs.every(isSafeArgument)) {
118
- return;
119
- }
120
- // Build the replacement
121
- const calleeText = sourceCode.getText(callee);
122
- if (callArgs.length === 0) {
123
- replacement = `${setTimeoutCall}(${calleeText}, ${delayText})`;
124
- }
125
- else {
126
- const argsTexts = callArgs.map((arg) => sourceCode.getText(arg));
127
- replacement = `${setTimeoutCall}(${calleeText}, ${delayText}, ${argsTexts.join(', ')})`;
128
- }
129
- }
130
- // fn.bind(null/undefined, args)
131
- else if (firstArg.type === 'CallExpression') {
132
- const bindCall = firstArg;
133
- if (bindCall.callee.type !== 'MemberExpression' ||
134
- bindCall.callee.property.type !== 'Identifier' ||
135
- bindCall.callee.property.name !== 'bind' ||
136
- bindCall.arguments.length === 0) {
137
- return;
138
- }
139
- const bindContext = bindCall.arguments[0];
140
- if (!bindContext || bindContext.type === 'SpreadElement') {
141
- return;
142
- }
143
- if (!isNullOrUndefined(bindContext)) {
144
- return;
145
- }
146
- const fnText = sourceCode.getText(bindCall.callee.object);
147
- const bindArgs = bindCall.arguments.slice(1);
148
- // Check if any bind argument contains a call expression or other unsafe construct
149
- if (!bindArgs.every(isSafeArgument)) {
150
- return;
151
- }
152
- if (bindArgs.length === 0) {
153
- replacement = `${setTimeoutCall}(${fnText}, ${delayText})`;
154
- }
155
- else {
156
- const argsTexts = bindArgs.map((arg) => sourceCode.getText(arg));
157
- replacement = `${setTimeoutCall}(${fnText}, ${delayText}, ${argsTexts.join(', ')})`;
158
- }
159
- }
160
- else {
161
- return;
162
- }
163
- if (replacement) {
164
- context.report({
165
- node,
166
- messageId: 'preferArgs',
167
- fix(fixer) {
168
- return fixer.replaceText(node, replacement);
169
- }
170
- });
171
- }
172
- }
173
- };
174
- }
175
- };