@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.
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/lib/configs/modernization.d.ts +2 -0
- package/lib/configs/modernization.js +17 -0
- package/lib/configs/module-replacements.d.ts +2 -0
- package/lib/configs/module-replacements.js +8 -0
- package/lib/configs/performance-improvements.d.ts +2 -0
- package/lib/configs/performance-improvements.js +9 -0
- package/lib/configs/recommended.d.ts +2 -0
- package/lib/configs/recommended.js +18 -0
- package/lib/main.d.ts +3 -0
- package/lib/main.js +48 -0
- package/lib/rules/no-indexof-equality.d.ts +2 -0
- package/lib/rules/no-indexof-equality.js +90 -0
- package/lib/rules/prefer-array-at.d.ts +2 -0
- package/lib/rules/prefer-array-at.js +58 -0
- package/lib/rules/prefer-array-fill.d.ts +2 -0
- package/lib/rules/prefer-array-fill.js +120 -0
- package/lib/rules/prefer-array-from-map.d.ts +2 -0
- package/lib/rules/prefer-array-from-map.js +57 -0
- package/lib/rules/prefer-array-to-reversed.d.ts +2 -0
- package/lib/rules/prefer-array-to-reversed.js +42 -0
- package/lib/rules/prefer-array-to-sorted.d.ts +2 -0
- package/lib/rules/prefer-array-to-sorted.js +43 -0
- package/lib/rules/prefer-array-to-spliced.d.ts +2 -0
- package/lib/rules/prefer-array-to-spliced.js +43 -0
- package/lib/rules/prefer-exponentiation-operator.d.ts +2 -0
- package/lib/rules/prefer-exponentiation-operator.js +42 -0
- package/lib/rules/prefer-includes.d.ts +2 -0
- package/lib/rules/prefer-includes.js +131 -0
- package/lib/rules/prefer-nullish-coalescing.d.ts +2 -0
- package/lib/rules/prefer-nullish-coalescing.js +131 -0
- package/lib/rules/prefer-object-has-own.d.ts +2 -0
- package/lib/rules/prefer-object-has-own.js +71 -0
- package/lib/rules/prefer-optimized-indexof.d.ts +2 -0
- package/lib/rules/prefer-optimized-indexof.js +90 -0
- package/lib/rules/prefer-settimeout-args.d.ts +2 -0
- package/lib/rules/prefer-settimeout-args.js +175 -0
- package/lib/rules/prefer-spread-syntax.d.ts +2 -0
- package/lib/rules/prefer-spread-syntax.js +109 -0
- package/lib/rules/prefer-timer-args.d.ts +2 -0
- package/lib/rules/prefer-timer-args.js +176 -0
- package/lib/rules/prefer-url-canparse.d.ts +2 -0
- package/lib/rules/prefer-url-canparse.js +139 -0
- package/lib/test/setup.d.ts +1 -0
- package/lib/test/setup.js +10 -0
- package/lib/utils/ast.d.ts +15 -0
- package/lib/utils/ast.js +47 -0
- package/lib/utils/typescript.d.ts +14 -0
- package/lib/utils/typescript.js +6 -0
- package/package.json +56 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getArrayFromCopyPattern, formatArguments } from '../utils/ast.js';
|
|
2
|
+
export const preferArrayToSorted = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: 'suggestion',
|
|
5
|
+
docs: {
|
|
6
|
+
description: 'Prefer Array.prototype.toSorted() over copying and sorting arrays',
|
|
7
|
+
recommended: true
|
|
8
|
+
},
|
|
9
|
+
fixable: 'code',
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
preferToSorted: 'Use {{array}}.toSorted() instead of copying and sorting'
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
const sourceCode = context.sourceCode;
|
|
17
|
+
return {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
if (node.callee.type !== 'MemberExpression' ||
|
|
20
|
+
node.callee.property.type !== 'Identifier' ||
|
|
21
|
+
node.callee.property.name !== 'sort') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const sortCallee = node.callee.object;
|
|
25
|
+
const arrayNode = getArrayFromCopyPattern(sortCallee);
|
|
26
|
+
if (arrayNode) {
|
|
27
|
+
const arrayText = sourceCode.getText(arrayNode);
|
|
28
|
+
const argsText = formatArguments(node.arguments, sourceCode);
|
|
29
|
+
context.report({
|
|
30
|
+
node,
|
|
31
|
+
messageId: 'preferToSorted',
|
|
32
|
+
data: {
|
|
33
|
+
array: arrayText
|
|
34
|
+
},
|
|
35
|
+
fix(fixer) {
|
|
36
|
+
return fixer.replaceText(node, `${arrayText}.toSorted(${argsText})`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getArrayFromCopyPattern, formatArguments } from '../utils/ast.js';
|
|
2
|
+
export const preferArrayToSpliced = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: 'suggestion',
|
|
5
|
+
docs: {
|
|
6
|
+
description: 'Prefer Array.prototype.toSpliced() over copying and splicing arrays',
|
|
7
|
+
recommended: true
|
|
8
|
+
},
|
|
9
|
+
fixable: 'code',
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
preferToSpliced: 'Use {{array}}.toSpliced() instead of copying and splicing'
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
const sourceCode = context.sourceCode;
|
|
17
|
+
return {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
if (node.callee.type !== 'MemberExpression' ||
|
|
20
|
+
node.callee.property.type !== 'Identifier' ||
|
|
21
|
+
node.callee.property.name !== 'splice') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const spliceCallee = node.callee.object;
|
|
25
|
+
const arrayNode = getArrayFromCopyPattern(spliceCallee);
|
|
26
|
+
if (arrayNode) {
|
|
27
|
+
const arrayText = sourceCode.getText(arrayNode);
|
|
28
|
+
const argsText = formatArguments(node.arguments, sourceCode);
|
|
29
|
+
context.report({
|
|
30
|
+
node,
|
|
31
|
+
messageId: 'preferToSpliced',
|
|
32
|
+
data: {
|
|
33
|
+
array: arrayText
|
|
34
|
+
},
|
|
35
|
+
fix(fixer) {
|
|
36
|
+
return fixer.replaceText(node, `${arrayText}.toSpliced(${argsText})`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const preferExponentiationOperator = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
docs: {
|
|
5
|
+
description: 'Prefer the exponentiation operator ** over Math.pow()',
|
|
6
|
+
recommended: true
|
|
7
|
+
},
|
|
8
|
+
fixable: 'code',
|
|
9
|
+
schema: [],
|
|
10
|
+
messages: {
|
|
11
|
+
preferExponentiation: 'Use the ** operator instead of Math.pow()'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
const sourceCode = context.sourceCode;
|
|
16
|
+
return {
|
|
17
|
+
CallExpression(node) {
|
|
18
|
+
if (node.callee.type !== 'MemberExpression' ||
|
|
19
|
+
node.callee.object.type !== 'Identifier' ||
|
|
20
|
+
node.callee.object.name !== 'Math' ||
|
|
21
|
+
node.callee.property.type !== 'Identifier' ||
|
|
22
|
+
node.callee.property.name !== 'pow') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const base = node.arguments[0];
|
|
26
|
+
const exponent = node.arguments[1];
|
|
27
|
+
if (!base || !exponent || node.arguments.length !== 2) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
context.report({
|
|
31
|
+
node,
|
|
32
|
+
messageId: 'preferExponentiation',
|
|
33
|
+
fix(fixer) {
|
|
34
|
+
const baseText = sourceCode.getText(base);
|
|
35
|
+
const exponentText = sourceCode.getText(exponent);
|
|
36
|
+
return fixer.replaceText(node, `(${baseText}) ** (${exponentText})`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
function isIndexOfCall(node) {
|
|
2
|
+
return (node.type === 'CallExpression' &&
|
|
3
|
+
node.callee.type === 'MemberExpression' &&
|
|
4
|
+
node.callee.property.type === 'Identifier' &&
|
|
5
|
+
node.callee.property.name === 'indexOf' &&
|
|
6
|
+
node.arguments.length >= 1);
|
|
7
|
+
}
|
|
8
|
+
function isNegativeOne(node) {
|
|
9
|
+
return (node.type === 'UnaryExpression' &&
|
|
10
|
+
node.operator === '-' &&
|
|
11
|
+
node.argument.type === 'Literal' &&
|
|
12
|
+
node.argument.value === 1);
|
|
13
|
+
}
|
|
14
|
+
function isZero(node) {
|
|
15
|
+
return node.type === 'Literal' && node.value === 0;
|
|
16
|
+
}
|
|
17
|
+
function reportIndexOf(context, node, indexOfCall, shouldNegate) {
|
|
18
|
+
const sourceCode = context.sourceCode;
|
|
19
|
+
const arrayText = sourceCode.getText(indexOfCall.callee.type === 'MemberExpression'
|
|
20
|
+
? indexOfCall.callee.object
|
|
21
|
+
: indexOfCall.callee);
|
|
22
|
+
const argsText = indexOfCall.arguments
|
|
23
|
+
.map((arg) => sourceCode.getText(arg))
|
|
24
|
+
.join(', ');
|
|
25
|
+
const replacement = shouldNegate
|
|
26
|
+
? `!${arrayText}.includes(${argsText})`
|
|
27
|
+
: `${arrayText}.includes(${argsText})`;
|
|
28
|
+
context.report({
|
|
29
|
+
node,
|
|
30
|
+
messageId: 'preferIncludes',
|
|
31
|
+
fix(fixer) {
|
|
32
|
+
return fixer.replaceText(node, replacement);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function checkBinaryExpression(node, context) {
|
|
37
|
+
const { left, right, operator } = node;
|
|
38
|
+
if (left.type === 'PrivateIdentifier') {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let indexOfCall;
|
|
42
|
+
let constantSide;
|
|
43
|
+
let op = operator;
|
|
44
|
+
if (isIndexOfCall(left)) {
|
|
45
|
+
indexOfCall = left;
|
|
46
|
+
constantSide = right;
|
|
47
|
+
}
|
|
48
|
+
else if (isIndexOfCall(right)) {
|
|
49
|
+
indexOfCall = right;
|
|
50
|
+
constantSide = left;
|
|
51
|
+
if (operator === '<') {
|
|
52
|
+
op = '>';
|
|
53
|
+
}
|
|
54
|
+
else if (operator === '>') {
|
|
55
|
+
op = '<';
|
|
56
|
+
}
|
|
57
|
+
else if (operator === '<=') {
|
|
58
|
+
op = '>=';
|
|
59
|
+
}
|
|
60
|
+
else if (operator === '>=') {
|
|
61
|
+
op = '<=';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (isNegativeOne(constantSide)) {
|
|
68
|
+
if (op === '!==' || op === '!=' || op === '>') {
|
|
69
|
+
reportIndexOf(context, node, indexOfCall, false);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (op === '===' || op === '==') {
|
|
73
|
+
reportIndexOf(context, node, indexOfCall, true);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (isZero(constantSide)) {
|
|
78
|
+
if (op === '>=') {
|
|
79
|
+
reportIndexOf(context, node, indexOfCall, false);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (op === '<') {
|
|
83
|
+
reportIndexOf(context, node, indexOfCall, true);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function checkUnaryExpression(node, context) {
|
|
89
|
+
if (node.operator === '~' && isIndexOfCall(node.argument)) {
|
|
90
|
+
reportIndexOf(context, node, node.argument, false);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (node.operator === '!' &&
|
|
94
|
+
node.argument.type === 'UnaryExpression' &&
|
|
95
|
+
node.argument.operator === '~' &&
|
|
96
|
+
isIndexOfCall(node.argument.argument)) {
|
|
97
|
+
reportIndexOf(context, node, node.argument.argument, true);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export const preferIncludes = {
|
|
102
|
+
meta: {
|
|
103
|
+
type: 'suggestion',
|
|
104
|
+
docs: {
|
|
105
|
+
description: 'Prefer .includes() over indexOf() comparisons for arrays and strings',
|
|
106
|
+
recommended: true
|
|
107
|
+
},
|
|
108
|
+
fixable: 'code',
|
|
109
|
+
schema: [],
|
|
110
|
+
messages: {
|
|
111
|
+
preferIncludes: 'Use .includes() instead of indexOf() comparison'
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
create(context) {
|
|
115
|
+
return {
|
|
116
|
+
BinaryExpression(node) {
|
|
117
|
+
checkBinaryExpression(node, context);
|
|
118
|
+
},
|
|
119
|
+
UnaryExpression(node) {
|
|
120
|
+
// Skip ~ if it's inside !~ (the parent will handle it)
|
|
121
|
+
if (node.operator === '~' && node.parent) {
|
|
122
|
+
if (node.parent.type === 'UnaryExpression' &&
|
|
123
|
+
node.parent.operator === '!') {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
checkUnaryExpression(node, context);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
function areExpressionsEquivalent(sourceCode, expr1, expr2) {
|
|
2
|
+
return sourceCode.getText(expr1) === sourceCode.getText(expr2);
|
|
3
|
+
}
|
|
4
|
+
function isNullLiteral(node) {
|
|
5
|
+
return node.type === 'Literal' && node.value === null;
|
|
6
|
+
}
|
|
7
|
+
function isUndefinedIdentifier(node) {
|
|
8
|
+
return node.type === 'Identifier' && node.name === 'undefined';
|
|
9
|
+
}
|
|
10
|
+
function isNullishCheck(sourceCode, expr) {
|
|
11
|
+
if (expr.type === 'BinaryExpression' &&
|
|
12
|
+
expr.left.type !== 'PrivateIdentifier' &&
|
|
13
|
+
isNullLiteral(expr.right) &&
|
|
14
|
+
(expr.operator === '==' || expr.operator === '!=')) {
|
|
15
|
+
return { value: expr.left, checksForNullish: expr.operator === '==' };
|
|
16
|
+
}
|
|
17
|
+
if (expr.type === 'LogicalExpression' &&
|
|
18
|
+
expr.left.type === 'BinaryExpression' &&
|
|
19
|
+
expr.right.type === 'BinaryExpression' &&
|
|
20
|
+
expr.left.left.type !== 'PrivateIdentifier' &&
|
|
21
|
+
expr.right.left.type !== 'PrivateIdentifier') {
|
|
22
|
+
const leftOp = expr.left.operator;
|
|
23
|
+
const rightOp = expr.right.operator;
|
|
24
|
+
const leftRight = expr.left.right;
|
|
25
|
+
const rightRight = expr.right.right;
|
|
26
|
+
const leftLeft = expr.left.left;
|
|
27
|
+
const rightLeft = expr.right.left;
|
|
28
|
+
const leftIsNull = isNullLiteral(leftRight);
|
|
29
|
+
const leftIsUndefined = isUndefinedIdentifier(leftRight);
|
|
30
|
+
const rightIsNull = isNullLiteral(rightRight);
|
|
31
|
+
const rightIsUndefined = isUndefinedIdentifier(rightRight);
|
|
32
|
+
if (!areExpressionsEquivalent(sourceCode, leftLeft, rightLeft)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if ((leftIsNull && rightIsUndefined) || (leftIsUndefined && rightIsNull)) {
|
|
36
|
+
if (expr.operator === '||' && leftOp === '===' && rightOp === '===') {
|
|
37
|
+
return { value: leftLeft, checksForNullish: true };
|
|
38
|
+
}
|
|
39
|
+
if (expr.operator === '&&' && leftOp === '!==' && rightOp === '!==') {
|
|
40
|
+
return { value: leftLeft, checksForNullish: false };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
export const preferNullishCoalescing = {
|
|
47
|
+
meta: {
|
|
48
|
+
type: 'suggestion',
|
|
49
|
+
docs: {
|
|
50
|
+
description: 'Prefer nullish coalescing operator (?? and ??=) over verbose null checks',
|
|
51
|
+
recommended: true
|
|
52
|
+
},
|
|
53
|
+
fixable: 'code',
|
|
54
|
+
schema: [],
|
|
55
|
+
messages: {
|
|
56
|
+
preferNullishCoalescing: 'Use nullish coalescing operator (??) instead of verbose null check',
|
|
57
|
+
preferNullishCoalescingAssignment: 'Use nullish coalescing assignment (??=) instead of verbose null check'
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
create(context) {
|
|
61
|
+
const sourceCode = context.sourceCode;
|
|
62
|
+
return {
|
|
63
|
+
ConditionalExpression(node) {
|
|
64
|
+
const checkResult = isNullishCheck(sourceCode, node.test);
|
|
65
|
+
if (checkResult) {
|
|
66
|
+
const { value, checksForNullish } = checkResult;
|
|
67
|
+
const compareNode = checksForNullish
|
|
68
|
+
? node.alternate
|
|
69
|
+
: node.consequent;
|
|
70
|
+
const defaultNode = checksForNullish
|
|
71
|
+
? node.consequent
|
|
72
|
+
: node.alternate;
|
|
73
|
+
if (areExpressionsEquivalent(sourceCode, value, compareNode)) {
|
|
74
|
+
context.report({
|
|
75
|
+
node,
|
|
76
|
+
messageId: 'preferNullishCoalescing',
|
|
77
|
+
fix(fixer) {
|
|
78
|
+
const valueText = sourceCode.getText(value);
|
|
79
|
+
const defaultText = sourceCode.getText(defaultNode);
|
|
80
|
+
return fixer.replaceText(node, `${valueText} ?? ${defaultText}`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
IfStatement(node) {
|
|
87
|
+
if (node.alternate) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
let body = null;
|
|
91
|
+
if (node.consequent.type === 'BlockStatement') {
|
|
92
|
+
const blockStmt = node.consequent;
|
|
93
|
+
if (blockStmt.body.length !== 1) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (blockStmt.body[0]?.type === 'ExpressionStatement') {
|
|
97
|
+
body = blockStmt.body[0];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (node.consequent.type === 'ExpressionStatement') {
|
|
101
|
+
body = node.consequent;
|
|
102
|
+
}
|
|
103
|
+
if (!body || body.expression.type !== 'AssignmentExpression') {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const assignment = body.expression;
|
|
107
|
+
if (assignment.operator !== '=') {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (assignment.left.type !== 'Identifier' &&
|
|
111
|
+
assignment.left.type !== 'MemberExpression') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const checkResult = isNullishCheck(sourceCode, node.test);
|
|
115
|
+
if (checkResult &&
|
|
116
|
+
checkResult.checksForNullish &&
|
|
117
|
+
areExpressionsEquivalent(sourceCode, checkResult.value, assignment.left)) {
|
|
118
|
+
context.report({
|
|
119
|
+
node,
|
|
120
|
+
messageId: 'preferNullishCoalescingAssignment',
|
|
121
|
+
fix(fixer) {
|
|
122
|
+
const leftText = sourceCode.getText(assignment.left);
|
|
123
|
+
const rightText = sourceCode.getText(assignment.right);
|
|
124
|
+
return fixer.replaceText(node, `${leftText} ??= ${rightText}`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
function getObjectPrototypeHasOwnPropertyArgs(node) {
|
|
2
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
3
|
+
node.callee.property.type === 'Identifier' &&
|
|
4
|
+
node.callee.property.name === 'call') {
|
|
5
|
+
const hasOwnPropertyExpr = node.callee.object;
|
|
6
|
+
if (hasOwnPropertyExpr.type === 'MemberExpression' &&
|
|
7
|
+
hasOwnPropertyExpr.property.type === 'Identifier' &&
|
|
8
|
+
hasOwnPropertyExpr.property.name === 'hasOwnProperty' &&
|
|
9
|
+
hasOwnPropertyExpr.object.type === 'MemberExpression' &&
|
|
10
|
+
hasOwnPropertyExpr.object.property.type === 'Identifier' &&
|
|
11
|
+
hasOwnPropertyExpr.object.property.name === 'prototype' &&
|
|
12
|
+
hasOwnPropertyExpr.object.object.type === 'Identifier' &&
|
|
13
|
+
hasOwnPropertyExpr.object.object.name === 'Object') {
|
|
14
|
+
return node.arguments;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export const preferObjectHasOwn = {
|
|
20
|
+
meta: {
|
|
21
|
+
type: 'suggestion',
|
|
22
|
+
docs: {
|
|
23
|
+
description: 'Prefer Object.hasOwn() over Object.prototype.hasOwnProperty.call() and obj.hasOwnProperty()',
|
|
24
|
+
recommended: true
|
|
25
|
+
},
|
|
26
|
+
fixable: 'code',
|
|
27
|
+
schema: [],
|
|
28
|
+
messages: {
|
|
29
|
+
preferObjectHasOwn: 'Use Object.hasOwn() instead of hasOwnProperty'
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
create(context) {
|
|
33
|
+
const sourceCode = context.sourceCode;
|
|
34
|
+
return {
|
|
35
|
+
CallExpression(node) {
|
|
36
|
+
// Check for Object.prototype.hasOwnProperty.call(obj, prop)
|
|
37
|
+
const prototypeArgs = getObjectPrototypeHasOwnPropertyArgs(node);
|
|
38
|
+
if (prototypeArgs && prototypeArgs.length === 2) {
|
|
39
|
+
const [object, property] = prototypeArgs;
|
|
40
|
+
const objectText = sourceCode.getText(object);
|
|
41
|
+
const propertyText = sourceCode.getText(property);
|
|
42
|
+
context.report({
|
|
43
|
+
node,
|
|
44
|
+
messageId: 'preferObjectHasOwn',
|
|
45
|
+
fix(fixer) {
|
|
46
|
+
return fixer.replaceText(node, `Object.hasOwn(${objectText}, ${propertyText})`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Check for obj.hasOwnProperty(prop)
|
|
52
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
53
|
+
node.callee.property.type === 'Identifier' &&
|
|
54
|
+
node.callee.property.name === 'hasOwnProperty' &&
|
|
55
|
+
node.arguments.length === 1) {
|
|
56
|
+
const object = node.callee.object;
|
|
57
|
+
const property = node.arguments[0];
|
|
58
|
+
const objectText = sourceCode.getText(object);
|
|
59
|
+
const propertyText = sourceCode.getText(property);
|
|
60
|
+
context.report({
|
|
61
|
+
node,
|
|
62
|
+
messageId: 'preferObjectHasOwn',
|
|
63
|
+
fix(fixer) {
|
|
64
|
+
return fixer.replaceText(node, `Object.hasOwn(${objectText}, ${propertyText})`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
};
|