@e18e/eslint-plugin 0.1.2 → 0.1.4

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
@@ -75,18 +75,70 @@ Copying these rules into your `rules` object will achieve the same effect as usi
75
75
  > Our type-aware rules depend on TypeScript ESLint's parser, which means they
76
76
  > will not work with oxlint at this time.
77
77
 
78
+ ## Linting `package.json`
79
+
80
+ Some rules (e.g. `ban-dependencies`) can be used against your `package.json`.
81
+
82
+ You can achieve this by using `@eslint/json` or `jsonc-eslint-parser`.
83
+
84
+ For example, with `@eslint/json` and `eslint.config.js`:
85
+
86
+ ```ts
87
+ import e18e from '@e18e/eslint-plugin';
88
+ import json from '@eslint/json';
89
+ import {defineConfig} from 'eslint/config';
90
+
91
+ export default defineConfig([
92
+ {
93
+ files: ['package.json'],
94
+ language: 'json/json',
95
+ plugins: {
96
+ e18e,
97
+ json
98
+ },
99
+ extends: ['e18e/recommended'],
100
+ }
101
+ ]);
102
+ ```
103
+
104
+ Or with `jsonc-eslint-parser` and `eslint.config.js`:
105
+
106
+ ```ts
107
+ import e18e from '@e18e/eslint-plugin';
108
+ import jsonParser from 'jsonc-eslint-parser';
109
+ import {defineConfig} from 'eslint/config';
110
+
111
+ export default defineConfig([
112
+ {
113
+ files: ['package.json'],
114
+ languageOptions: {
115
+ parser: jsonParser
116
+ },
117
+ plugins: {
118
+ e18e
119
+ },
120
+ extends: ['e18e/recommended'],
121
+ }
122
+ ]);
123
+ ```
124
+
125
+ Read more at the
126
+ [`@eslint/json` docs](https://github.com/eslint/json) and
127
+ [`jsonc-eslint-parser` docs](https://github.com/ota-meshi/jsonc-eslint-parser).
128
+
78
129
  ## Rules
79
130
 
80
131
  **Legend:**
81
132
  - ✅ = Yes / Enabled
82
133
  - ✖️ = No / Disabled
83
134
  - 💡 = Has suggestions (requires user confirmation for fixes)
135
+ - 🔶 = Optionally uses types (works without TypeScript but more powerful with it)
84
136
 
85
137
  ### Modernization
86
138
 
87
139
  | Rule | Description | Recommended | Fixable | Requires Types |
88
140
  |------|-------------|-------------|---------|----------------|
89
- | [prefer-array-at](./src/rules/prefer-array-at.ts) | Prefer `Array.prototype.at()` over length-based indexing | ✅ | ✅ | ✖️ |
141
+ | [prefer-array-at](./src/rules/prefer-array-at.ts) | Prefer `Array.prototype.at()` over length-based indexing | ✅ | ✅ | 🔶 |
90
142
  | [prefer-array-fill](./src/rules/prefer-array-fill.ts) | Prefer `Array.prototype.fill()` over `Array.from()` or `map()` with constant values | ✅ | ✅ | ✖️ |
91
143
  | [prefer-includes](./src/rules/prefer-includes.ts) | Prefer `.includes()` over `indexOf()` comparisons for arrays and strings | ✅ | ✅ | ✖️ |
92
144
  | [prefer-array-to-reversed](./src/rules/prefer-array-to-reversed.ts) | Prefer `Array.prototype.toReversed()` over copying and reversing arrays | ✅ | ✅ | ✖️ |
@@ -110,7 +162,10 @@ Copying these rules into your `rules` object will achieve the same effect as usi
110
162
  |------|-------------|-------------|---------|----------------|
111
163
  | [no-indexof-equality](./src/rules/no-indexof-equality.ts) | Prefer `startsWith()` for strings and direct array access over `indexOf()` equality checks | ✖️ | ✅ | ✅ |
112
164
  | [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 | ✅ | ✅ | ✖️ |
165
+ | [prefer-array-some](./src/rules/prefer-array-some.ts) | Prefer `Array.some()` over `Array.find()` when checking for element existence | ✅ | ✅ | ✖️ |
113
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
+ | [prefer-date-now](./src/rules/prefer-date-now.ts) | Prefer `Date.now()` over `new Date().getTime()` and `+new Date()` | ✅ | ✅ | ✖️ |
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 | ✅ | ✅ | 🔶 |
114
169
 
115
170
  ## License
116
171
 
@@ -4,6 +4,9 @@ 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',
10
+ 'e18e/prefer-array-some': 'error'
8
11
  }
9
12
  });
package/lib/main.js CHANGED
@@ -16,6 +16,9 @@ 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';
21
+ import { preferArraySome } from './rules/prefer-array-some.js';
19
22
  import { rules as dependRules } from 'eslint-plugin-depend';
20
23
  const plugin = {
21
24
  meta: {
@@ -38,6 +41,9 @@ const plugin = {
38
41
  'prefer-url-canparse': preferUrlCanParse,
39
42
  'no-indexof-equality': noIndexOfEquality,
40
43
  'prefer-timer-args': preferTimerArgs,
44
+ 'prefer-date-now': preferDateNow,
45
+ 'prefer-regex-test': preferRegexTest,
46
+ 'prefer-array-some': preferArraySome,
41
47
  ...dependRules
42
48
  }
43
49
  };
@@ -1,2 +1,4 @@
1
- import type { Rule } from 'eslint';
2
- export declare const preferArrayAt: Rule.RuleModule;
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'preferAt';
3
+ export declare const preferArrayAt: TSESLint.RuleModule<MessageIds, []>;
4
+ export {};
@@ -1,9 +1,9 @@
1
+ import { isArrayType, isStringType } from '../utils/typescript.js';
1
2
  export const preferArrayAt = {
2
3
  meta: {
3
4
  type: 'suggestion',
4
5
  docs: {
5
- description: 'Prefer Array.prototype.at() over length-based indexing',
6
- recommended: true
6
+ description: 'Prefer Array.prototype.at() over length-based indexing'
7
7
  },
8
8
  fixable: 'code',
9
9
  schema: [],
@@ -11,6 +11,7 @@ export const preferArrayAt = {
11
11
  preferAt: 'Use .at(-1) instead of [{{array}}.length - 1]'
12
12
  }
13
13
  },
14
+ defaultOptions: [],
14
15
  create(context) {
15
16
  const sourceCode = context.sourceCode;
16
17
  return {
@@ -48,6 +49,11 @@ export const preferArrayAt = {
48
49
  parent.left === node) {
49
50
  return;
50
51
  }
52
+ // Check if the object supports .at() (array or string, when types are available)
53
+ if (!isArrayType(node.object, context) &&
54
+ !isStringType(node.object, context)) {
55
+ return;
56
+ }
51
57
  context.report({
52
58
  node,
53
59
  messageId: 'preferAt',
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const preferArraySome: Rule.RuleModule;
@@ -0,0 +1,118 @@
1
+ import { isInBooleanContext, isNullish } from '../utils/ast.js';
2
+ function isFindCall(node) {
3
+ return (node.type === 'CallExpression' &&
4
+ node.callee.type === 'MemberExpression' &&
5
+ node.callee.property.type === 'Identifier' &&
6
+ node.callee.property.name === 'find' &&
7
+ node.arguments.length >= 1);
8
+ }
9
+ function reportFind(context, node, findCall, shouldNegate) {
10
+ if (findCall.callee.type !== 'MemberExpression') {
11
+ return;
12
+ }
13
+ const sourceCode = context.sourceCode;
14
+ const arrayText = sourceCode.getText(findCall.callee.object);
15
+ const argsText = findCall.arguments
16
+ .map((arg) => sourceCode.getText(arg))
17
+ .join(', ');
18
+ const replacement = shouldNegate
19
+ ? `!${arrayText}.some(${argsText})`
20
+ : `${arrayText}.some(${argsText})`;
21
+ context.report({
22
+ node,
23
+ messageId: 'preferArraySome',
24
+ fix(fixer) {
25
+ return fixer.replaceText(node, replacement);
26
+ }
27
+ });
28
+ }
29
+ function checkBinaryExpression(node, context) {
30
+ const { left, right, operator } = node;
31
+ if (left.type === 'PrivateIdentifier') {
32
+ return;
33
+ }
34
+ let findCall;
35
+ let constantSide;
36
+ if (isFindCall(left)) {
37
+ findCall = left;
38
+ constantSide = right;
39
+ }
40
+ else if (isFindCall(right)) {
41
+ findCall = right;
42
+ constantSide = left;
43
+ }
44
+ else {
45
+ return;
46
+ }
47
+ const nullishType = isNullish(constantSide);
48
+ if (!nullishType) {
49
+ return;
50
+ }
51
+ if (operator === '===' || operator === '!==') {
52
+ if (nullishType !== 'undefined') {
53
+ return;
54
+ }
55
+ const shouldNegate = operator === '===';
56
+ reportFind(context, node, findCall, shouldNegate);
57
+ return;
58
+ }
59
+ }
60
+ function checkUnaryExpression(node, context) {
61
+ // !arr.find(fn) -> !arr.some(fn)
62
+ if (node.operator === '!' && isFindCall(node.argument)) {
63
+ reportFind(context, node, node.argument, true);
64
+ return;
65
+ }
66
+ // !!arr.find(fn) -> arr.some(fn)
67
+ if (node.operator === '!' &&
68
+ node.argument.type === 'UnaryExpression' &&
69
+ node.argument.operator === '!' &&
70
+ isFindCall(node.argument.argument)) {
71
+ reportFind(context, node, node.argument.argument, false);
72
+ return;
73
+ }
74
+ }
75
+ export const preferArraySome = {
76
+ meta: {
77
+ type: 'suggestion',
78
+ docs: {
79
+ description: 'Prefer Array.some() over Array.find() when checking for element existence',
80
+ recommended: true
81
+ },
82
+ fixable: 'code',
83
+ schema: [],
84
+ messages: {
85
+ preferArraySome: 'Use Array.some() instead of Array.find() when checking for element existence'
86
+ }
87
+ },
88
+ create(context) {
89
+ return {
90
+ BinaryExpression(node) {
91
+ checkBinaryExpression(node, context);
92
+ },
93
+ UnaryExpression(node) {
94
+ // Skip inner ! if it's inside !! (the outer will handle it)
95
+ if (node.operator === '!' && node.parent) {
96
+ if (node.parent.type === 'UnaryExpression' &&
97
+ node.parent.operator === '!') {
98
+ return;
99
+ }
100
+ }
101
+ checkUnaryExpression(node, context);
102
+ },
103
+ CallExpression(node) {
104
+ // Skip if handled by UnaryExpression or BinaryExpression
105
+ if (node.parent?.type === 'UnaryExpression' ||
106
+ node.parent?.type === 'BinaryExpression') {
107
+ return;
108
+ }
109
+ if (!isFindCall(node)) {
110
+ return;
111
+ }
112
+ if (isInBooleanContext(node)) {
113
+ reportFind(context, node, node, false);
114
+ }
115
+ }
116
+ };
117
+ }
118
+ };
@@ -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,147 @@
1
+ import { tryGetTypedParserServices } from '../utils/typescript.js';
2
+ import { isInBooleanContext } from '../utils/ast.js';
3
+ function isRegExpLiteral(node) {
4
+ return (node.type === 'Literal' &&
5
+ 'regex' in node &&
6
+ node.regex !== undefined &&
7
+ node.regex !== null);
8
+ }
9
+ /**
10
+ * Checks if a node is a `new RegExp(...)`
11
+ */
12
+ function isRegExpConstructor(node) {
13
+ if (node.type !== 'NewExpression') {
14
+ return false;
15
+ }
16
+ const { callee } = node;
17
+ // new RegExp()
18
+ if (callee.type === 'Identifier' && callee.name === 'RegExp') {
19
+ return true;
20
+ }
21
+ // new window.RegExp() or new globalThis.RegExp()
22
+ if (callee.type === 'MemberExpression' &&
23
+ callee.object.type === 'Identifier' &&
24
+ (callee.object.name === 'window' || callee.object.name === 'globalThis') &&
25
+ callee.property.type === 'Identifier' &&
26
+ callee.property.name === 'RegExp' &&
27
+ !callee.computed) {
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ /**
33
+ * Checks if a node is a RegExp (literal or constructor)
34
+ */
35
+ function isRegExp(node) {
36
+ return (node !== null &&
37
+ node !== undefined &&
38
+ (isRegExpLiteral(node) || isRegExpConstructor(node)));
39
+ }
40
+ /**
41
+ * Checks if a node resolves to a RegExp using TypeScript types (when available)
42
+ */
43
+ function isRegExpByType(node, context) {
44
+ const services = tryGetTypedParserServices(context);
45
+ if (!services) {
46
+ return false;
47
+ }
48
+ const type = services.getTypeAtLocation(node);
49
+ if (!type) {
50
+ return false;
51
+ }
52
+ const checker = services.program.getTypeChecker();
53
+ const typeString = checker.typeToString(type);
54
+ return typeString === 'RegExp';
55
+ }
56
+ /**
57
+ * Checks if a node resolves to a RegExp (literal, constructor, or by type)
58
+ */
59
+ function resolvesToRegExp(node, context) {
60
+ if (isRegExpByType(node, context)) {
61
+ return true;
62
+ }
63
+ if (isRegExp(node)) {
64
+ return true;
65
+ }
66
+ if (node.type !== 'Identifier') {
67
+ return false;
68
+ }
69
+ const scope = context.sourceCode.getScope(node);
70
+ const variable = scope.references.find((ref) => ref.identifier === node)?.resolved;
71
+ if (!variable) {
72
+ return false;
73
+ }
74
+ for (const def of variable.defs) {
75
+ if (def.type === 'Variable' && def.node.type === 'VariableDeclarator') {
76
+ const init = def.node.init;
77
+ if (isRegExp(init)) {
78
+ return true;
79
+ }
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+ export const preferRegexTest = {
85
+ meta: {
86
+ type: 'suggestion',
87
+ docs: {
88
+ description: 'prefer `RegExp.test()` over `String.match()` and `RegExp.exec()` when only checking for match existence'
89
+ },
90
+ fixable: 'code',
91
+ messages: {
92
+ preferTest: 'Prefer `{{regex}}.test({{string}})` over `{{original}}` for boolean checks'
93
+ },
94
+ schema: []
95
+ },
96
+ defaultOptions: [],
97
+ create(context) {
98
+ return {
99
+ CallExpression(node) {
100
+ if (!isInBooleanContext(node)) {
101
+ return;
102
+ }
103
+ const { callee } = node;
104
+ if (callee.type !== 'MemberExpression') {
105
+ return;
106
+ }
107
+ const property = callee.property;
108
+ if (property.type !== 'Identifier' || node.arguments.length !== 1) {
109
+ return;
110
+ }
111
+ let regexNode;
112
+ let stringNode;
113
+ if (property.name === 'match') {
114
+ // str.match(regex)
115
+ stringNode = callee.object;
116
+ regexNode = node.arguments[0];
117
+ }
118
+ else if (property.name === 'exec') {
119
+ // regex.exec(str)
120
+ regexNode = callee.object;
121
+ stringNode = node.arguments[0];
122
+ }
123
+ else {
124
+ return;
125
+ }
126
+ if (!resolvesToRegExp(regexNode, context)) {
127
+ return;
128
+ }
129
+ const sourceCode = context.sourceCode;
130
+ const regexText = sourceCode.getText(regexNode);
131
+ const stringText = sourceCode.getText(stringNode);
132
+ context.report({
133
+ node,
134
+ messageId: 'preferTest',
135
+ data: {
136
+ regex: regexText,
137
+ string: stringText,
138
+ original: sourceCode.getText(node)
139
+ },
140
+ fix(fixer) {
141
+ return fixer.replaceText(node, `${regexText}.test(${stringText})`);
142
+ }
143
+ });
144
+ }
145
+ };
146
+ }
147
+ };
@@ -30,9 +30,12 @@ export const preferSpreadSyntax = {
30
30
  let messageId;
31
31
  let replacement;
32
32
  // array.concat()
33
+ // excluding Buffer.concat()
33
34
  if (node.callee.property.type === 'Identifier' &&
34
35
  node.callee.property.name === 'concat' &&
35
- node.arguments.length > 0) {
36
+ node.arguments.length > 0 &&
37
+ !(node.callee.object.type === 'Identifier' &&
38
+ node.callee.object.name === 'Buffer')) {
36
39
  const arrayText = sourceCode.getText(node.callee.object);
37
40
  const argTexts = node.arguments.map((arg) => sourceCode.getText(arg));
38
41
  const spreadParts = [arrayText, ...argTexts]
@@ -4,6 +4,22 @@ function isNullOrUndefined(node) {
4
4
  }
5
5
  return node.type === 'Identifier' && node.name === 'undefined';
6
6
  }
7
+ function isTimerCall(node) {
8
+ if (node.callee.type === 'Identifier' &&
9
+ (node.callee.name === 'setTimeout' || node.callee.name === 'setInterval')) {
10
+ return true;
11
+ }
12
+ if (node.callee.type === 'MemberExpression' &&
13
+ node.callee.object.type === 'Identifier' &&
14
+ (node.callee.object.name === 'window' ||
15
+ node.callee.object.name === 'globalThis') &&
16
+ node.callee.property.type === 'Identifier' &&
17
+ (node.callee.property.name === 'setTimeout' ||
18
+ node.callee.property.name === 'setInterval')) {
19
+ return true;
20
+ }
21
+ return false;
22
+ }
7
23
  function isSafeArgument(arg) {
8
24
  if (arg.type === 'SpreadElement') {
9
25
  return arg.argument.type === 'Identifier';
@@ -75,18 +91,7 @@ export const preferTimerArgs = {
75
91
  const sourceCode = context.sourceCode;
76
92
  return {
77
93
  CallExpression(node) {
78
- // Check if this is setTimeout/setInterval (with optional window/globalThis prefix)
79
- const isTimerFunction = (node.callee.type === 'Identifier' &&
80
- (node.callee.name === 'setTimeout' ||
81
- node.callee.name === 'setInterval')) ||
82
- (node.callee.type === 'MemberExpression' &&
83
- node.callee.object.type === 'Identifier' &&
84
- (node.callee.object.name === 'window' ||
85
- node.callee.object.name === 'globalThis') &&
86
- node.callee.property.type === 'Identifier' &&
87
- (node.callee.property.name === 'setTimeout' ||
88
- node.callee.property.name === 'setInterval'));
89
- if (!isTimerFunction) {
94
+ if (!isTimerCall(node)) {
90
95
  return;
91
96
  }
92
97
  if (node.arguments.length < 2) {
@@ -101,21 +106,23 @@ export const preferTimerArgs = {
101
106
  let replacement = null;
102
107
  // simple arrow functions, e.g. () => fn(args)
103
108
  if (firstArg.type === 'ArrowFunctionExpression') {
104
- const arrowFn = firstArg;
105
109
  // skip if it is a block body
106
- if (arrowFn.body.type === 'BlockStatement') {
110
+ if (firstArg.body.type === 'BlockStatement') {
107
111
  return;
108
112
  }
109
113
  // skip if it has parameters
110
- if (arrowFn.params.length > 0) {
114
+ if (firstArg.params.length > 0) {
111
115
  return;
112
116
  }
113
- if (arrowFn.body.type !== 'CallExpression') {
117
+ if (firstArg.body.type !== 'CallExpression') {
114
118
  return;
115
119
  }
116
- const callExpression = arrowFn.body;
120
+ const callExpression = firstArg.body;
117
121
  const callee = callExpression.callee;
118
122
  const callArgs = callExpression.arguments;
123
+ if (callee.type === 'MemberExpression') {
124
+ return;
125
+ }
119
126
  if (!callArgs.every(isSafeArgument)) {
120
127
  return;
121
128
  }
@@ -1,5 +1,17 @@
1
- import type { CallExpression, Node } from 'estree';
2
- import type { SourceCode } from 'eslint';
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;
5
+ /**
6
+ * Checks if a node is in a boolean context (where the result is only used as truthy/falsy).
7
+ * e.g. if conditions, while loops, ternary tests, logical operators
8
+ */
9
+ export declare function isInBooleanContext(node: AnyNode): boolean;
10
+ /**
11
+ * Checks if a node is undefined, null, or void 0.
12
+ * Returns the type of nullish value or false if not nullish.
13
+ */
14
+ export declare function isNullish(node: Expression): 'undefined' | 'null' | false;
3
15
  /**
4
16
  * Checks if a CallExpression is a copy operation that creates a shallow copy of an array.
5
17
  * e.g. concat(), slice(), slice(0)
@@ -13,3 +25,4 @@ export declare function getArrayFromCopyPattern(node: Node): Node | null;
13
25
  * Formats arguments from a CallExpression as a comma-separated string.
14
26
  */
15
27
  export declare function formatArguments(args: CallExpression['arguments'], sourceCode: SourceCode): string;
28
+ export {};
package/lib/utils/ast.js CHANGED
@@ -1,3 +1,51 @@
1
+ /**
2
+ * Checks if a node is in a boolean context (where the result is only used as truthy/falsy).
3
+ * e.g. if conditions, while loops, ternary tests, logical operators
4
+ */
5
+ export function isInBooleanContext(node) {
6
+ const parent = node.parent;
7
+ if (!parent) {
8
+ return false;
9
+ }
10
+ // if/while/for/do-while test
11
+ if ((parent.type === 'IfStatement' && parent.test === node) ||
12
+ (parent.type === 'WhileStatement' && parent.test === node) ||
13
+ (parent.type === 'ForStatement' && parent.test === node) ||
14
+ (parent.type === 'DoWhileStatement' && parent.test === node)) {
15
+ return true;
16
+ }
17
+ // ternaries
18
+ if (parent.type === 'ConditionalExpression' && parent.test === node) {
19
+ return true;
20
+ }
21
+ // check the parent recursively for unary ! and logical operators
22
+ if ((parent.type === 'UnaryExpression' && parent.operator === '!') ||
23
+ (parent.type === 'LogicalExpression' &&
24
+ (parent.operator === '&&' || parent.operator === '||'))) {
25
+ return isInBooleanContext(parent);
26
+ }
27
+ return false;
28
+ }
29
+ /**
30
+ * Checks if a node is undefined, null, or void 0.
31
+ * Returns the type of nullish value or false if not nullish.
32
+ */
33
+ export function isNullish(node) {
34
+ if (node.type === 'Identifier' && node.name === 'undefined') {
35
+ return 'undefined';
36
+ }
37
+ if (node.type === 'Literal' && node.value === null) {
38
+ return 'null';
39
+ }
40
+ // void 0
41
+ if (node.type === 'UnaryExpression' &&
42
+ node.operator === 'void' &&
43
+ node.argument.type === 'Literal' &&
44
+ node.argument.value === 0) {
45
+ return 'undefined';
46
+ }
47
+ return false;
48
+ }
1
49
  /**
2
50
  * Checks if a CallExpression is a copy operation that creates a shallow copy of an array.
3
51
  * e.g. concat(), slice(), slice(0)
@@ -11,4 +11,15 @@ 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;
16
+ /**
17
+ * Checks if a node's type is an Array type (Array, tuple, or typed array)
18
+ * Returns true if types are unavailable (to avoid false negatives)
19
+ */
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 string
23
+ * Returns false if types are unavailable
24
+ */
25
+ export declare function isStringType(node: TSESTree.Node, context: Readonly<TSESLint.RuleContext<string, unknown[]>>): boolean;
@@ -1,6 +1,69 @@
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
+ }
14
+ const typedArrayTypes = [
15
+ 'Int8Array',
16
+ 'Uint8Array',
17
+ 'Uint8ClampedArray',
18
+ 'Int16Array',
19
+ 'Uint16Array',
20
+ 'Int32Array',
21
+ 'Uint32Array',
22
+ 'Float32Array',
23
+ 'Float64Array',
24
+ 'BigInt64Array',
25
+ 'BigUint64Array'
26
+ ];
27
+ /**
28
+ * Checks if a node's type is an Array type (Array, tuple, or typed array)
29
+ * Returns true if types are unavailable (to avoid false negatives)
30
+ */
31
+ export function isArrayType(node, context) {
32
+ const services = tryGetTypedParserServices(context);
33
+ if (!services) {
34
+ return true;
35
+ }
36
+ const type = services.getTypeAtLocation(node);
37
+ if (!type) {
38
+ return true;
39
+ }
40
+ const checker = services.program.getTypeChecker();
41
+ if (checker.isArrayType(type)) {
42
+ return true;
43
+ }
44
+ if (checker.isTupleType(type)) {
45
+ return true;
46
+ }
47
+ const typeString = checker.typeToString(type);
48
+ if (typedArrayTypes.some((t) => typeString.startsWith(t))) {
49
+ return true;
50
+ }
51
+ return false;
52
+ }
53
+ /**
54
+ * Checks if a node's type is a string
55
+ * Returns false if types are unavailable
56
+ */
57
+ export function isStringType(node, context) {
58
+ const services = tryGetTypedParserServices(context);
59
+ if (!services) {
60
+ return false;
61
+ }
62
+ const type = services.getTypeAtLocation(node);
63
+ if (!type) {
64
+ return false;
65
+ }
66
+ const checker = services.program.getTypeChecker();
67
+ const stringType = checker.getStringType();
68
+ return checker.isTypeAssignableTo(type, stringType);
69
+ }
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.4",
4
4
  "description": "The official e18e ESLint plugin for modernizing code and improving performance.",
5
5
  "keywords": [
6
6
  "eslint",
@@ -36,16 +36,18 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@eslint/js": "^9.39.2",
39
- "@types/node": "^25.0.3",
40
- "@typescript-eslint/rule-tester": "^8.50.0",
41
- "@typescript-eslint/typescript-estree": "^8.50.0",
42
- "@vitest/coverage-v8": "^4.0.16",
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",
43
44
  "eslint": "^9.39.2",
44
- "eslint-plugin-eslint-plugin": "^7.2.0",
45
- "oxlint": "^1.34.0",
46
- "prettier": "^3.7.4",
45
+ "eslint-plugin-eslint-plugin": "^7.3.0",
46
+ "jsonc-eslint-parser": "^2.4.2",
47
+ "oxlint": "^1.41.0",
48
+ "prettier": "^3.8.1",
47
49
  "typescript": "^5.9.3",
48
- "typescript-eslint": "^8.50.0",
50
+ "typescript-eslint": "^8.53.1",
49
51
  "vitest": "^4.0.14"
50
52
  },
51
53
  "peerDependencies": {