@e18e/eslint-plugin 0.1.4 → 0.2.0
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 +11 -1
- package/lib/configs/performance-improvements.js +2 -1
- package/lib/main.js +4 -0
- package/lib/rules/prefer-array-to-sorted.d.ts +4 -2
- package/lib/rules/prefer-array-to-sorted.js +6 -2
- package/lib/rules/prefer-inline-equality.d.ts +4 -0
- package/lib/rules/prefer-inline-equality.js +170 -0
- package/lib/rules/prefer-static-regex.d.ts +2 -0
- package/lib/rules/prefer-static-regex.js +34 -0
- package/lib/utils/ast.d.ts +5 -4
- package/lib/utils/ast.js +3 -7
- package/lib/utils/typescript.d.ts +5 -0
- package/lib/utils/typescript.js +18 -0
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -142,7 +142,7 @@ Read more at the
|
|
|
142
142
|
| [prefer-array-fill](./src/rules/prefer-array-fill.ts) | Prefer `Array.prototype.fill()` over `Array.from()` or `map()` with constant values | ✅ | ✅ | ✖️ |
|
|
143
143
|
| [prefer-includes](./src/rules/prefer-includes.ts) | Prefer `.includes()` over `indexOf()` comparisons for arrays and strings | ✅ | ✅ | ✖️ |
|
|
144
144
|
| [prefer-array-to-reversed](./src/rules/prefer-array-to-reversed.ts) | Prefer `Array.prototype.toReversed()` over copying and reversing arrays | ✅ | ✅ | ✖️ |
|
|
145
|
-
| [prefer-array-to-sorted](./src/rules/prefer-array-to-sorted.ts) | Prefer `Array.prototype.toSorted()` over copying and sorting arrays | ✅ | ✅ |
|
|
145
|
+
| [prefer-array-to-sorted](./src/rules/prefer-array-to-sorted.ts) | Prefer `Array.prototype.toSorted()` over copying and sorting arrays | ✅ | ✅ | 🔶 |
|
|
146
146
|
| [prefer-array-to-spliced](./src/rules/prefer-array-to-spliced.ts) | Prefer `Array.prototype.toSpliced()` over copying and splicing arrays | ✅ | ✅ | ✖️ |
|
|
147
147
|
| [prefer-exponentiation-operator](./src/rules/prefer-exponentiation-operator.ts) | Prefer the exponentiation operator `**` over `Math.pow()` | ✅ | ✅ | ✖️ |
|
|
148
148
|
| [prefer-nullish-coalescing](./src/rules/prefer-nullish-coalescing.ts) | Prefer nullish coalescing operator (`??` and `??=`) over verbose null checks | ✅ | ✅ | ✖️ |
|
|
@@ -166,6 +166,16 @@ Read more at the
|
|
|
166
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
167
|
| [prefer-date-now](./src/rules/prefer-date-now.ts) | Prefer `Date.now()` over `new Date().getTime()` and `+new Date()` | ✅ | ✅ | ✖️ |
|
|
168
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 | ✅ | ✅ | 🔶 |
|
|
169
|
+
| [prefer-static-regex](./src/rules/prefer-static-regex.ts) | Prefer defining regular expressions at module scope to avoid re-compilation on every function call | ✅ | ✖️ | ✖️ |
|
|
170
|
+
| [prefer-inline-equality](./src/rules/prefer-inline-equality.ts) | Prefer inline equality checks over temporary object creation for simple comparisons | ✖️ | ✅ | 🔶 |
|
|
171
|
+
|
|
172
|
+
## Sponsors
|
|
173
|
+
|
|
174
|
+
<p align="center">
|
|
175
|
+
<a href="https://github.com/sponsors/e18e">
|
|
176
|
+
<img src="https://e18e.dev/sponsors.svg" alt="e18e community sponsors" />
|
|
177
|
+
</a>
|
|
178
|
+
</p>
|
|
169
179
|
|
|
170
180
|
## License
|
|
171
181
|
|
|
@@ -7,6 +7,7 @@ export const performanceImprovements = (plugin) => ({
|
|
|
7
7
|
'e18e/prefer-timer-args': 'error',
|
|
8
8
|
'e18e/prefer-date-now': 'error',
|
|
9
9
|
'e18e/prefer-regex-test': 'error',
|
|
10
|
-
'e18e/prefer-array-some': 'error'
|
|
10
|
+
'e18e/prefer-array-some': 'error',
|
|
11
|
+
'e18e/prefer-static-regex': 'error'
|
|
11
12
|
}
|
|
12
13
|
});
|
package/lib/main.js
CHANGED
|
@@ -19,6 +19,8 @@ import { preferTimerArgs } from './rules/prefer-timer-args.js';
|
|
|
19
19
|
import { preferDateNow } from './rules/prefer-date-now.js';
|
|
20
20
|
import { preferRegexTest } from './rules/prefer-regex-test.js';
|
|
21
21
|
import { preferArraySome } from './rules/prefer-array-some.js';
|
|
22
|
+
import { preferStaticRegex } from './rules/prefer-static-regex.js';
|
|
23
|
+
import { preferInlineEquality } from './rules/prefer-inline-equality.js';
|
|
22
24
|
import { rules as dependRules } from 'eslint-plugin-depend';
|
|
23
25
|
const plugin = {
|
|
24
26
|
meta: {
|
|
@@ -44,6 +46,8 @@ const plugin = {
|
|
|
44
46
|
'prefer-date-now': preferDateNow,
|
|
45
47
|
'prefer-regex-test': preferRegexTest,
|
|
46
48
|
'prefer-array-some': preferArraySome,
|
|
49
|
+
'prefer-static-regex': preferStaticRegex,
|
|
50
|
+
'prefer-inline-equality': preferInlineEquality,
|
|
47
51
|
...dependRules
|
|
48
52
|
}
|
|
49
53
|
};
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type MessageIds = 'preferToSorted';
|
|
3
|
+
export declare const preferArrayToSorted: TSESLint.RuleModule<MessageIds, []>;
|
|
4
|
+
export {};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { getArrayFromCopyPattern, formatArguments } from '../utils/ast.js';
|
|
2
|
+
import { isArrayType } from '../utils/typescript.js';
|
|
2
3
|
export const preferArrayToSorted = {
|
|
3
4
|
meta: {
|
|
4
5
|
type: 'suggestion',
|
|
5
6
|
docs: {
|
|
6
|
-
description: 'Prefer Array.prototype.toSorted() over copying and sorting arrays'
|
|
7
|
-
recommended: true
|
|
7
|
+
description: 'Prefer Array.prototype.toSorted() over copying and sorting arrays'
|
|
8
8
|
},
|
|
9
9
|
fixable: 'code',
|
|
10
10
|
schema: [],
|
|
@@ -12,6 +12,7 @@ export const preferArrayToSorted = {
|
|
|
12
12
|
preferToSorted: 'Use {{array}}.toSorted() instead of copying and sorting'
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
+
defaultOptions: [],
|
|
15
16
|
create(context) {
|
|
16
17
|
const sourceCode = context.sourceCode;
|
|
17
18
|
return {
|
|
@@ -24,6 +25,9 @@ export const preferArrayToSorted = {
|
|
|
24
25
|
const sortCallee = node.callee.object;
|
|
25
26
|
const arrayNode = getArrayFromCopyPattern(sortCallee);
|
|
26
27
|
if (arrayNode) {
|
|
28
|
+
if (!isArrayType(arrayNode, context)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
27
31
|
const arrayText = sourceCode.getText(arrayNode);
|
|
28
32
|
const argsText = formatArguments(node.arguments, sourceCode);
|
|
29
33
|
context.report({
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { isArrayType, isSetType, tryGetTypedParserServices } from '../utils/typescript.js';
|
|
2
|
+
/**
|
|
3
|
+
* Checks if a node is safe to repeat (i.e. no side effects)
|
|
4
|
+
*/
|
|
5
|
+
function isSafeToRepeat(node) {
|
|
6
|
+
if (node.type === 'Identifier') {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (node.type === 'Literal') {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (node.type === 'MemberExpression' && !node.computed) {
|
|
16
|
+
return isSafeToRepeat(node.object);
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Checks if a node is NaN
|
|
22
|
+
*/
|
|
23
|
+
function isNaN(node) {
|
|
24
|
+
return node.type === 'Identifier' && node.name === 'NaN';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Checks if an array element is an identifier or literal
|
|
28
|
+
*/
|
|
29
|
+
function isSimpleElement(node) {
|
|
30
|
+
if (isNaN(node)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return node.type === 'Identifier' || node.type === 'Literal';
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Checks if the node is negated
|
|
37
|
+
*/
|
|
38
|
+
function isNegated(node) {
|
|
39
|
+
return (node.parent !== undefined &&
|
|
40
|
+
node.parent.type === 'UnaryExpression' &&
|
|
41
|
+
node.parent.operator === '!');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Checks if the replacement expression needs to be wrapped in parentheses
|
|
45
|
+
* based on the parent node context
|
|
46
|
+
*/
|
|
47
|
+
function needsParentheses(node) {
|
|
48
|
+
if (!node.parent) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
switch (node.parent.type) {
|
|
52
|
+
case 'CallExpression':
|
|
53
|
+
case 'NewExpression':
|
|
54
|
+
case 'MemberExpression':
|
|
55
|
+
case 'ConditionalExpression':
|
|
56
|
+
case 'BinaryExpression':
|
|
57
|
+
case 'LogicalExpression':
|
|
58
|
+
case 'UnaryExpression':
|
|
59
|
+
case 'TaggedTemplateExpression':
|
|
60
|
+
case 'SpreadElement':
|
|
61
|
+
case 'AwaitExpression':
|
|
62
|
+
return true;
|
|
63
|
+
default:
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function checkArrayIncludes(node, context) {
|
|
68
|
+
const { callee } = node;
|
|
69
|
+
if (callee.type !== 'MemberExpression') {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const property = callee.property;
|
|
73
|
+
if (property.type !== 'Identifier' ||
|
|
74
|
+
property.name !== 'includes' ||
|
|
75
|
+
callee.computed) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (node.arguments.length !== 1) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const arrayNode = callee.object;
|
|
82
|
+
if (arrayNode.type !== 'ArrayExpression') {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const elements = arrayNode.elements;
|
|
86
|
+
if (elements.length === 0 || elements.length > 6) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const val = node.arguments[0];
|
|
90
|
+
if (!isSafeToRepeat(val)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const hasTypes = tryGetTypedParserServices(context) !== null;
|
|
94
|
+
for (const element of elements) {
|
|
95
|
+
if (element === null) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (element.type === 'SpreadElement') {
|
|
99
|
+
const arg = element.argument;
|
|
100
|
+
if (!isSafeToRepeat(arg)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!hasTypes ||
|
|
104
|
+
(!isArrayType(arg, context) && !isSetType(arg, context))) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (!isSimpleElement(element)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const sourceCode = context.sourceCode;
|
|
113
|
+
const negated = isNegated(node);
|
|
114
|
+
const operator = negated ? '!==' : '===';
|
|
115
|
+
const joiner = negated ? ' && ' : ' || ';
|
|
116
|
+
const parts = [];
|
|
117
|
+
for (const element of elements) {
|
|
118
|
+
if (element === null) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (element.type === 'SpreadElement') {
|
|
122
|
+
const argText = sourceCode.getText(element.argument);
|
|
123
|
+
const valText = sourceCode.getText(val);
|
|
124
|
+
const method = isSetType(element.argument, context) ? 'has' : 'includes';
|
|
125
|
+
if (negated) {
|
|
126
|
+
parts.push(`!${argText}.${method}(${valText})`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
parts.push(`${argText}.${method}(${valText})`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const elemText = sourceCode.getText(element);
|
|
134
|
+
const valText = sourceCode.getText(val);
|
|
135
|
+
parts.push(`${elemText} ${operator} ${valText}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const replacement = parts.join(joiner);
|
|
139
|
+
const reportNode = negated ? node.parent : node;
|
|
140
|
+
const needsParens = needsParentheses(reportNode);
|
|
141
|
+
const fixText = needsParens ? `(${replacement})` : replacement;
|
|
142
|
+
context.report({
|
|
143
|
+
node: reportNode,
|
|
144
|
+
messageId: 'preferEquality',
|
|
145
|
+
fix(fixer) {
|
|
146
|
+
return fixer.replaceText(reportNode, fixText);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
export const preferInlineEquality = {
|
|
151
|
+
meta: {
|
|
152
|
+
type: 'suggestion',
|
|
153
|
+
docs: {
|
|
154
|
+
description: 'Prefer inline equality checks over temporary object creation for simple comparisons'
|
|
155
|
+
},
|
|
156
|
+
fixable: 'code',
|
|
157
|
+
messages: {
|
|
158
|
+
preferEquality: 'Avoid creating a temporary array just to call `.includes()`. Use equality checks instead.'
|
|
159
|
+
},
|
|
160
|
+
schema: []
|
|
161
|
+
},
|
|
162
|
+
defaultOptions: [],
|
|
163
|
+
create(context) {
|
|
164
|
+
return {
|
|
165
|
+
CallExpression(node) {
|
|
166
|
+
checkArrayIncludes(node, context);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function isStaticNewRegExp(node) {
|
|
2
|
+
if (node.callee.type !== 'Identifier' ||
|
|
3
|
+
node.callee.name !== 'RegExp' ||
|
|
4
|
+
node.arguments.length === 0 ||
|
|
5
|
+
node.arguments.length > 2) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
return node.arguments.every((arg) => arg.type === 'Literal' && typeof arg.value === 'string');
|
|
9
|
+
}
|
|
10
|
+
export const preferStaticRegex = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'suggestion',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Prefer defining regular expressions at module scope to avoid re-compilation on every function call',
|
|
15
|
+
recommended: true
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
preferStatic: 'Move this regular expression to module scope to avoid re-compilation on every call.'
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
return {
|
|
24
|
+
':function Literal[regex]'(node) {
|
|
25
|
+
context.report({ node, messageId: 'preferStatic' });
|
|
26
|
+
},
|
|
27
|
+
':function NewExpression'(node) {
|
|
28
|
+
if (isStaticNewRegExp(node)) {
|
|
29
|
+
context.report({ node, messageId: 'preferStatic' });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
};
|
package/lib/utils/ast.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CallExpression, Node, Expression } from 'estree';
|
|
2
|
-
import type { TSESTree } from '@typescript-eslint/utils';
|
|
3
|
-
import type {
|
|
4
|
-
type AnyNode =
|
|
2
|
+
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
3
|
+
import type { SourceCode } from 'eslint';
|
|
4
|
+
export type AnyNode = Node | TSESTree.Node;
|
|
5
5
|
/**
|
|
6
6
|
* Checks if a node is in a boolean context (where the result is only used as truthy/falsy).
|
|
7
7
|
* e.g. if conditions, while loops, ternary tests, logical operators
|
|
@@ -20,9 +20,10 @@ export declare function isCopyCall(node: CallExpression): boolean;
|
|
|
20
20
|
/**
|
|
21
21
|
* Extracts the array node from array copy patterns.
|
|
22
22
|
*/
|
|
23
|
+
export declare function getArrayFromCopyPattern(node: TSESTree.Node): TSESTree.Node | null;
|
|
23
24
|
export declare function getArrayFromCopyPattern(node: Node): Node | null;
|
|
24
25
|
/**
|
|
25
26
|
* Formats arguments from a CallExpression as a comma-separated string.
|
|
26
27
|
*/
|
|
28
|
+
export declare function formatArguments(args: TSESTree.CallExpression['arguments'], sourceCode: Readonly<TSESLint.SourceCode>): string;
|
|
27
29
|
export declare function formatArguments(args: CallExpression['arguments'], sourceCode: SourceCode): string;
|
|
28
|
-
export {};
|
package/lib/utils/ast.js
CHANGED
|
@@ -68,9 +68,6 @@ export function isCopyCall(node) {
|
|
|
68
68
|
}
|
|
69
69
|
return false;
|
|
70
70
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Extracts the array node from array copy patterns.
|
|
73
|
-
*/
|
|
74
71
|
export function getArrayFromCopyPattern(node) {
|
|
75
72
|
if (node.type === 'CallExpression' &&
|
|
76
73
|
isCopyCall(node) &&
|
|
@@ -84,12 +81,11 @@ export function getArrayFromCopyPattern(node) {
|
|
|
84
81
|
}
|
|
85
82
|
return null;
|
|
86
83
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Formats arguments from a CallExpression as a comma-separated string.
|
|
89
|
-
*/
|
|
90
84
|
export function formatArguments(args, sourceCode) {
|
|
91
85
|
if (args.length === 0) {
|
|
92
86
|
return '';
|
|
93
87
|
}
|
|
94
|
-
return args
|
|
88
|
+
return args
|
|
89
|
+
.map((arg) => sourceCode.getText(arg))
|
|
90
|
+
.join(', ');
|
|
95
91
|
}
|
|
@@ -18,6 +18,11 @@ export declare function getTypedParserServices(context: Readonly<TSESLint.RuleCo
|
|
|
18
18
|
* Returns true if types are unavailable (to avoid false negatives)
|
|
19
19
|
*/
|
|
20
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 Set
|
|
23
|
+
* Returns true if types are unavailable (to avoid false negatives)
|
|
24
|
+
*/
|
|
25
|
+
export declare function isSetType(node: TSESTree.Node, context: Readonly<TSESLint.RuleContext<string, unknown[]>>): boolean;
|
|
21
26
|
/**
|
|
22
27
|
* Checks if a node's type is a string
|
|
23
28
|
* Returns false if types are unavailable
|
package/lib/utils/typescript.js
CHANGED
|
@@ -50,6 +50,24 @@ export function isArrayType(node, context) {
|
|
|
50
50
|
}
|
|
51
51
|
return false;
|
|
52
52
|
}
|
|
53
|
+
const setTypePattern = /^(Readonly)?Set</;
|
|
54
|
+
/**
|
|
55
|
+
* Checks if a node's type is a Set
|
|
56
|
+
* Returns true if types are unavailable (to avoid false negatives)
|
|
57
|
+
*/
|
|
58
|
+
export function isSetType(node, context) {
|
|
59
|
+
const services = tryGetTypedParserServices(context);
|
|
60
|
+
if (!services) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
const type = services.getTypeAtLocation(node);
|
|
64
|
+
if (!type) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const checker = services.program.getTypeChecker();
|
|
68
|
+
const typeString = checker.typeToString(type);
|
|
69
|
+
return setTypePattern.test(typeString);
|
|
70
|
+
}
|
|
53
71
|
/**
|
|
54
72
|
* Checks if a node's type is a string
|
|
55
73
|
* Returns false if types are unavailable
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e18e/eslint-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The official e18e ESLint plugin for modernizing code and improving performance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
@@ -51,7 +51,16 @@
|
|
|
51
51
|
"vitest": "^4.0.14"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"eslint": "^9.0.0"
|
|
54
|
+
"eslint": "^9.0.0 || ^10.0.0",
|
|
55
|
+
"oxlint": "^1.41.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"eslint": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"oxlint": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
55
64
|
},
|
|
56
65
|
"dependencies": {
|
|
57
66
|
"eslint-plugin-depend": "^1.4.0"
|