@e18e/eslint-plugin 0.0.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -0
  3. package/lib/configs/modernization.d.ts +2 -0
  4. package/lib/configs/modernization.js +17 -0
  5. package/lib/configs/module-replacements.d.ts +2 -0
  6. package/lib/configs/module-replacements.js +8 -0
  7. package/lib/configs/performance-improvements.d.ts +2 -0
  8. package/lib/configs/performance-improvements.js +9 -0
  9. package/lib/configs/recommended.d.ts +2 -0
  10. package/lib/configs/recommended.js +18 -0
  11. package/lib/main.d.ts +3 -0
  12. package/lib/main.js +48 -0
  13. package/lib/rules/no-indexof-equality.d.ts +2 -0
  14. package/lib/rules/no-indexof-equality.js +90 -0
  15. package/lib/rules/prefer-array-at.d.ts +2 -0
  16. package/lib/rules/prefer-array-at.js +58 -0
  17. package/lib/rules/prefer-array-fill.d.ts +2 -0
  18. package/lib/rules/prefer-array-fill.js +120 -0
  19. package/lib/rules/prefer-array-from-map.d.ts +2 -0
  20. package/lib/rules/prefer-array-from-map.js +57 -0
  21. package/lib/rules/prefer-array-to-reversed.d.ts +2 -0
  22. package/lib/rules/prefer-array-to-reversed.js +42 -0
  23. package/lib/rules/prefer-array-to-sorted.d.ts +2 -0
  24. package/lib/rules/prefer-array-to-sorted.js +43 -0
  25. package/lib/rules/prefer-array-to-spliced.d.ts +2 -0
  26. package/lib/rules/prefer-array-to-spliced.js +43 -0
  27. package/lib/rules/prefer-exponentiation-operator.d.ts +2 -0
  28. package/lib/rules/prefer-exponentiation-operator.js +42 -0
  29. package/lib/rules/prefer-includes.d.ts +2 -0
  30. package/lib/rules/prefer-includes.js +131 -0
  31. package/lib/rules/prefer-nullish-coalescing.d.ts +2 -0
  32. package/lib/rules/prefer-nullish-coalescing.js +131 -0
  33. package/lib/rules/prefer-object-has-own.d.ts +2 -0
  34. package/lib/rules/prefer-object-has-own.js +71 -0
  35. package/lib/rules/prefer-optimized-indexof.d.ts +2 -0
  36. package/lib/rules/prefer-optimized-indexof.js +90 -0
  37. package/lib/rules/prefer-settimeout-args.d.ts +2 -0
  38. package/lib/rules/prefer-settimeout-args.js +175 -0
  39. package/lib/rules/prefer-spread-syntax.d.ts +2 -0
  40. package/lib/rules/prefer-spread-syntax.js +109 -0
  41. package/lib/rules/prefer-timer-args.d.ts +2 -0
  42. package/lib/rules/prefer-timer-args.js +176 -0
  43. package/lib/rules/prefer-url-canparse.d.ts +2 -0
  44. package/lib/rules/prefer-url-canparse.js +139 -0
  45. package/lib/test/setup.d.ts +1 -0
  46. package/lib/test/setup.js +10 -0
  47. package/lib/utils/ast.d.ts +15 -0
  48. package/lib/utils/ast.js +47 -0
  49. package/lib/utils/typescript.d.ts +14 -0
  50. package/lib/utils/typescript.js +6 -0
  51. package/package.json +56 -0
@@ -0,0 +1,175 @@
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
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const preferSpreadSyntax: Rule.RuleModule;
@@ -0,0 +1,109 @@
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
+ export const preferSpreadSyntax = {
8
+ meta: {
9
+ type: 'suggestion',
10
+ docs: {
11
+ description: 'Prefer spread syntax over Array.concat(), Array.from(), Object.assign({}, ...), and Function.apply()',
12
+ recommended: true
13
+ },
14
+ fixable: 'code',
15
+ schema: [],
16
+ messages: {
17
+ preferSpreadArray: 'Use spread syntax [...arr, ...other] instead of arr.concat(other)',
18
+ preferSpreadArrayFrom: 'Use spread syntax [...iterable] instead of Array.from(iterable) when no mapper function is provided',
19
+ preferSpreadObject: 'Use spread syntax {...a, ...b} instead of Object.assign({}, a, b)',
20
+ preferSpreadFunction: 'Use spread syntax fn(...args) instead of fn.apply(null/undefined, args)'
21
+ }
22
+ },
23
+ create(context) {
24
+ const sourceCode = context.sourceCode;
25
+ return {
26
+ CallExpression(node) {
27
+ if (node.callee.type !== 'MemberExpression') {
28
+ return;
29
+ }
30
+ let messageId;
31
+ let replacement;
32
+ // array.concat()
33
+ if (node.callee.property.type === 'Identifier' &&
34
+ node.callee.property.name === 'concat' &&
35
+ node.arguments.length > 0) {
36
+ const arrayText = sourceCode.getText(node.callee.object);
37
+ const argTexts = node.arguments.map((arg) => sourceCode.getText(arg));
38
+ const spreadParts = [arrayText, ...argTexts]
39
+ .map((part) => `...${part}`)
40
+ .join(', ');
41
+ replacement = `[${spreadParts}]`;
42
+ messageId = 'preferSpreadArray';
43
+ }
44
+ // Array.from(iterable) with no mapper
45
+ else if (node.callee.object.type === 'Identifier' &&
46
+ node.callee.object.name === 'Array' &&
47
+ node.callee.property.type === 'Identifier' &&
48
+ node.callee.property.name === 'from' &&
49
+ node.arguments.length === 1) {
50
+ const iterableText = sourceCode.getText(node.arguments[0]);
51
+ replacement = `[...${iterableText}]`;
52
+ messageId = 'preferSpreadArrayFrom';
53
+ }
54
+ // Object.assign({...}, ...)
55
+ else if (node.callee.object.type === 'Identifier' &&
56
+ node.callee.object.name === 'Object' &&
57
+ node.callee.property.type === 'Identifier' &&
58
+ node.callee.property.name === 'assign' &&
59
+ node.arguments.length >= 2) {
60
+ const firstArg = node.arguments[0];
61
+ if (firstArg.type !== 'SpreadElement' &&
62
+ firstArg.type === 'ObjectExpression') {
63
+ const hasUnquotedProto = firstArg.properties.some((prop) => prop.type === 'Property' &&
64
+ !prop.computed &&
65
+ prop.key.type === 'Identifier' &&
66
+ prop.key.name === '__proto__');
67
+ if (!hasUnquotedProto) {
68
+ const spreadArgs = node.arguments
69
+ .slice(1)
70
+ .map((arg) => `...${sourceCode.getText(arg)}`)
71
+ .join(', ');
72
+ if (firstArg.properties.length === 0) {
73
+ replacement = `{${spreadArgs}}`;
74
+ }
75
+ else {
76
+ const literalText = sourceCode.getText(firstArg);
77
+ const innerContent = literalText.slice(1, -1); // Remove { and }
78
+ replacement = `{${innerContent}, ${spreadArgs}}`;
79
+ }
80
+ messageId = 'preferSpreadObject';
81
+ }
82
+ }
83
+ }
84
+ // function.apply(null/undefined, args)
85
+ else if (node.callee.property.type === 'Identifier' &&
86
+ node.callee.property.name === 'apply' &&
87
+ node.arguments.length === 2) {
88
+ const firstArg = node.arguments[0];
89
+ if (firstArg.type !== 'SpreadElement' &&
90
+ isNullOrUndefined(firstArg)) {
91
+ const fnText = sourceCode.getText(node.callee.object);
92
+ const argsText = sourceCode.getText(node.arguments[1]);
93
+ replacement = `${fnText}(...${argsText})`;
94
+ messageId = 'preferSpreadFunction';
95
+ }
96
+ }
97
+ if (messageId && replacement) {
98
+ context.report({
99
+ node,
100
+ messageId,
101
+ fix(fixer) {
102
+ return fixer.replaceText(node, replacement);
103
+ }
104
+ });
105
+ }
106
+ }
107
+ };
108
+ }
109
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const preferTimerArgs: Rule.RuleModule;
@@ -0,0 +1,176 @@
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' ||
18
+ arg.property.type === 'PrivateIdentifier') {
19
+ return false;
20
+ }
21
+ if (!isSafeArgument(arg.object)) {
22
+ return false;
23
+ }
24
+ if (arg.computed) {
25
+ return isSafeArgument(arg.property);
26
+ }
27
+ return true;
28
+ case 'ArrayExpression':
29
+ return arg.elements.every((el) => el === null || isSafeArgument(el));
30
+ case 'ObjectExpression':
31
+ return arg.properties.every((prop) => {
32
+ if (prop.type === 'SpreadElement') {
33
+ return isSafeArgument(prop.argument);
34
+ }
35
+ const valueType = prop.value.type;
36
+ if (valueType === 'ObjectPattern' ||
37
+ valueType === 'ArrayPattern' ||
38
+ valueType === 'RestElement' ||
39
+ valueType === 'AssignmentPattern') {
40
+ return false;
41
+ }
42
+ return isSafeArgument(prop.value);
43
+ });
44
+ case 'UnaryExpression':
45
+ case 'UpdateExpression':
46
+ return isSafeArgument(arg.argument);
47
+ case 'BinaryExpression':
48
+ case 'LogicalExpression':
49
+ return (arg.left.type !== 'PrivateIdentifier' &&
50
+ isSafeArgument(arg.left) &&
51
+ isSafeArgument(arg.right));
52
+ case 'ConditionalExpression':
53
+ return (isSafeArgument(arg.test) &&
54
+ isSafeArgument(arg.consequent) &&
55
+ isSafeArgument(arg.alternate));
56
+ // CallExpression, NewExpression, etc.
57
+ default:
58
+ return false;
59
+ }
60
+ }
61
+ export const preferTimerArgs = {
62
+ meta: {
63
+ type: 'suggestion',
64
+ docs: {
65
+ description: 'Prefer passing function and arguments directly to setTimeout/setInterval instead of wrapping in an arrow function or using bind',
66
+ recommended: true
67
+ },
68
+ fixable: 'code',
69
+ schema: [],
70
+ messages: {
71
+ preferArgs: 'Pass function and arguments directly to timer function to avoid allocating an extra function'
72
+ }
73
+ },
74
+ create(context) {
75
+ const sourceCode = context.sourceCode;
76
+ return {
77
+ 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) {
90
+ return;
91
+ }
92
+ if (node.arguments.length < 2) {
93
+ return;
94
+ }
95
+ const firstArg = node.arguments[0];
96
+ if (!firstArg || firstArg.type === 'SpreadElement') {
97
+ return;
98
+ }
99
+ const delayText = sourceCode.getText(node.arguments[1]);
100
+ const timerCall = sourceCode.getText(node.callee);
101
+ let replacement = null;
102
+ // simple arrow functions, e.g. () => fn(args)
103
+ if (firstArg.type === 'ArrowFunctionExpression') {
104
+ const arrowFn = firstArg;
105
+ // skip if it is a block body
106
+ if (arrowFn.body.type === 'BlockStatement') {
107
+ return;
108
+ }
109
+ // skip if it has parameters
110
+ if (arrowFn.params.length > 0) {
111
+ return;
112
+ }
113
+ if (arrowFn.body.type !== 'CallExpression') {
114
+ return;
115
+ }
116
+ const callExpression = arrowFn.body;
117
+ const callee = callExpression.callee;
118
+ const callArgs = callExpression.arguments;
119
+ if (!callArgs.every(isSafeArgument)) {
120
+ return;
121
+ }
122
+ const calleeText = sourceCode.getText(callee);
123
+ if (callArgs.length === 0) {
124
+ replacement = `${timerCall}(${calleeText}, ${delayText})`;
125
+ }
126
+ else {
127
+ const argsTexts = callArgs.map((arg) => sourceCode.getText(arg));
128
+ replacement = `${timerCall}(${calleeText}, ${delayText}, ${argsTexts.join(', ')})`;
129
+ }
130
+ }
131
+ // fn.bind(null/undefined, args)
132
+ else if (firstArg.type === 'CallExpression') {
133
+ const bindCall = firstArg;
134
+ if (bindCall.callee.type !== 'MemberExpression' ||
135
+ bindCall.callee.property.type !== 'Identifier' ||
136
+ bindCall.callee.property.name !== 'bind' ||
137
+ bindCall.arguments.length === 0) {
138
+ return;
139
+ }
140
+ const bindContext = bindCall.arguments[0];
141
+ if (!bindContext || bindContext.type === 'SpreadElement') {
142
+ return;
143
+ }
144
+ if (!isNullOrUndefined(bindContext)) {
145
+ return;
146
+ }
147
+ const fnText = sourceCode.getText(bindCall.callee.object);
148
+ const bindArgs = bindCall.arguments.slice(1);
149
+ // Check if any bind argument contains a call expression or other unsafe construct
150
+ if (!bindArgs.every(isSafeArgument)) {
151
+ return;
152
+ }
153
+ if (bindArgs.length === 0) {
154
+ replacement = `${timerCall}(${fnText}, ${delayText})`;
155
+ }
156
+ else {
157
+ const argsTexts = bindArgs.map((arg) => sourceCode.getText(arg));
158
+ replacement = `${timerCall}(${fnText}, ${delayText}, ${argsTexts.join(', ')})`;
159
+ }
160
+ }
161
+ else {
162
+ return;
163
+ }
164
+ if (replacement) {
165
+ context.report({
166
+ node,
167
+ messageId: 'preferArgs',
168
+ fix(fixer) {
169
+ return fixer.replaceText(node, replacement);
170
+ }
171
+ });
172
+ }
173
+ }
174
+ };
175
+ }
176
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const preferUrlCanParse: Rule.RuleModule;
@@ -0,0 +1,139 @@
1
+ import { formatArguments } from '../utils/ast.js';
2
+ /**
3
+ * Check if a statement is `new URL(...)`
4
+ */
5
+ function isNewURLStatement(stmt) {
6
+ return (stmt.type === 'ExpressionStatement' &&
7
+ stmt.expression.type === 'NewExpression' &&
8
+ stmt.expression.callee.type === 'Identifier' &&
9
+ stmt.expression.callee.name === 'URL' &&
10
+ stmt.expression.arguments.length >= 1);
11
+ }
12
+ /**
13
+ * Check if a statement is `return (true|false)`
14
+ */
15
+ function isReturnBoolean(stmt, value) {
16
+ return (stmt.type === 'ReturnStatement' &&
17
+ stmt.argument?.type === 'Literal' &&
18
+ stmt.argument.value === value);
19
+ }
20
+ /**
21
+ * Check if block has only a return statement with a boolean literal
22
+ */
23
+ function hasOnlyReturnBoolean(block, value) {
24
+ if (block.body.length !== 1) {
25
+ return false;
26
+ }
27
+ const firstStmt = block.body[0];
28
+ if (!firstStmt) {
29
+ return false;
30
+ }
31
+ return isReturnBoolean(firstStmt, value);
32
+ }
33
+ /**
34
+ * Check if block is empty or contains only empty statements
35
+ */
36
+ function isEmptyBlock(block, sourceCode) {
37
+ return (block.body.length === 0 ||
38
+ block.body.every((stmt) => stmt.type === 'EmptyStatement' || !sourceCode.getText(stmt).trim()));
39
+ }
40
+ export const preferUrlCanParse = {
41
+ meta: {
42
+ type: 'suggestion',
43
+ docs: {
44
+ description: 'Prefer URL.canParse() over try-catch blocks for URL validation',
45
+ recommended: true
46
+ },
47
+ hasSuggestions: true,
48
+ schema: [],
49
+ messages: {
50
+ preferCanParse: 'Use URL.canParse() instead of try-catch for URL validation',
51
+ replaceWithCanParse: 'Replace with URL.canParse()'
52
+ }
53
+ },
54
+ create(context) {
55
+ const sourceCode = context.sourceCode;
56
+ return {
57
+ TryStatement(node) {
58
+ const tryBlock = node.block;
59
+ const catchClause = node.handler;
60
+ if (!catchClause) {
61
+ return;
62
+ }
63
+ const tryStatements = tryBlock.body;
64
+ if (tryStatements.length === 0) {
65
+ return;
66
+ }
67
+ const firstStmt = tryStatements[0];
68
+ if (!firstStmt || !isNewURLStatement(firstStmt)) {
69
+ return;
70
+ }
71
+ const urlArgText = formatArguments(firstStmt.expression.arguments, sourceCode);
72
+ // try { new URL(u); return true; } catch { return false; }
73
+ const secondStmt = tryStatements[1];
74
+ if (tryStatements.length === 2 &&
75
+ secondStmt &&
76
+ isReturnBoolean(secondStmt, true) &&
77
+ hasOnlyReturnBoolean(catchClause.body, false)) {
78
+ context.report({
79
+ node,
80
+ messageId: 'preferCanParse',
81
+ suggest: [
82
+ {
83
+ messageId: 'replaceWithCanParse',
84
+ fix(fixer) {
85
+ return fixer.replaceText(node, `return URL.canParse(${urlArgText})`);
86
+ }
87
+ }
88
+ ]
89
+ });
90
+ return;
91
+ }
92
+ // try { new URL(u); ...body } catch { ...catchBody }
93
+ // Basically if there's a body after the URL construction
94
+ if (tryStatements.length >= 2) {
95
+ const bodyAfterURL = tryStatements.slice(1);
96
+ const firstBodyStmt = bodyAfterURL[0];
97
+ const lastBodyStmt = bodyAfterURL.at(-1);
98
+ const bodyText = firstBodyStmt &&
99
+ lastBodyStmt &&
100
+ firstBodyStmt.range &&
101
+ lastBodyStmt.range
102
+ ? sourceCode.text.slice(firstBodyStmt.range[0], lastBodyStmt.range[1])
103
+ : '';
104
+ const catchBody = catchClause.body;
105
+ const catchBodyEmpty = isEmptyBlock(catchBody, sourceCode);
106
+ let replacement;
107
+ if (catchBodyEmpty) {
108
+ // No catch body, just if without else
109
+ replacement = `if (URL.canParse(${urlArgText})) {\n${bodyText}\n}`;
110
+ }
111
+ else {
112
+ const catchStatements = catchBody.body;
113
+ const firstCatchStmt = catchStatements[0];
114
+ const lastCatchStmt = catchStatements.at(-1);
115
+ const catchBodyText = firstCatchStmt &&
116
+ lastCatchStmt &&
117
+ firstCatchStmt.range &&
118
+ lastCatchStmt.range
119
+ ? sourceCode.text.slice(firstCatchStmt.range[0], lastCatchStmt.range[1])
120
+ : '';
121
+ replacement = `if (URL.canParse(${urlArgText})) {\n${bodyText}\n} else {\n${catchBodyText}\n}`;
122
+ }
123
+ context.report({
124
+ node,
125
+ messageId: 'preferCanParse',
126
+ suggest: [
127
+ {
128
+ messageId: 'replaceWithCanParse',
129
+ fix(fixer) {
130
+ return fixer.replaceText(node, replacement);
131
+ }
132
+ }
133
+ ]
134
+ });
135
+ }
136
+ }
137
+ };
138
+ }
139
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { RuleTester } from 'eslint';
2
+ import { describe, it, afterAll } from 'vitest';
3
+ import { RuleTester as TSRuleTester } from '@typescript-eslint/rule-tester';
4
+ RuleTester.describe = describe;
5
+ RuleTester.it = it;
6
+ RuleTester.itOnly = it.only;
7
+ TSRuleTester.afterAll = afterAll;
8
+ TSRuleTester.describe = describe;
9
+ TSRuleTester.it = it;
10
+ TSRuleTester.itOnly = it.only;
@@ -0,0 +1,15 @@
1
+ import type { CallExpression, Node } from 'estree';
2
+ import type { SourceCode } from 'eslint';
3
+ /**
4
+ * Checks if a CallExpression is a copy operation that creates a shallow copy of an array.
5
+ * e.g. concat(), slice(), slice(0)
6
+ */
7
+ export declare function isCopyCall(node: CallExpression): boolean;
8
+ /**
9
+ * Extracts the array node from array copy patterns.
10
+ */
11
+ export declare function getArrayFromCopyPattern(node: Node): Node | null;
12
+ /**
13
+ * Formats arguments from a CallExpression as a comma-separated string.
14
+ */
15
+ export declare function formatArguments(args: CallExpression['arguments'], sourceCode: SourceCode): string;