@blumintinc/eslint-plugin-blumint 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -39
- package/lib/index.js +22 -1
- package/lib/rules/enforce-assert-throws.js +23 -0
- package/lib/rules/enforce-centralized-mock-firestore.d.ts +1 -0
- package/lib/rules/enforce-centralized-mock-firestore.js +55 -0
- package/lib/rules/enforce-exported-function-types.js +307 -8
- package/lib/rules/enforce-render-hits-memoization.d.ts +3 -0
- package/lib/rules/enforce-render-hits-memoization.js +96 -0
- package/lib/rules/enforce-verb-noun-naming.js +1 -0
- package/lib/rules/extract-global-constants.js +4 -0
- package/lib/rules/no-entire-object-hook-deps.js +10 -0
- package/lib/rules/no-firestore-jest-mock.d.ts +1 -0
- package/lib/rules/no-firestore-jest-mock.js +59 -0
- package/lib/rules/no-mock-firebase-admin.d.ts +1 -0
- package/lib/rules/no-mock-firebase-admin.js +50 -0
- package/lib/rules/prefer-batch-operations.js +44 -0
- package/lib/rules/prefer-clone-deep.d.ts +1 -0
- package/lib/rules/prefer-clone-deep.js +72 -0
- package/lib/rules/prefer-settings-object.js +28 -0
- package/lib/rules/require-hooks-default-params.js +121 -97
- package/package.json +2 -2
|
@@ -69,6 +69,40 @@ function findLoopNode(node) {
|
|
|
69
69
|
}
|
|
70
70
|
return undefined;
|
|
71
71
|
}
|
|
72
|
+
function isFirestoreSetterInstance(node) {
|
|
73
|
+
// Check if it's a DocSetter instance
|
|
74
|
+
if (node.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
75
|
+
return (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
76
|
+
node.callee.name === 'DocSetter');
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function isMapInstance(node) {
|
|
81
|
+
// Check if it's a Map instance
|
|
82
|
+
if (node.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
83
|
+
return (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
84
|
+
node.callee.name === 'Map');
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
function findVariableDeclaration(node, varName) {
|
|
89
|
+
let current = node;
|
|
90
|
+
while (current) {
|
|
91
|
+
if (current.type === utils_1.AST_NODE_TYPES.Program) {
|
|
92
|
+
for (const statement of current.body) {
|
|
93
|
+
if (statement.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
94
|
+
for (const decl of statement.declarations) {
|
|
95
|
+
if (decl.id.type === utils_1.AST_NODE_TYPES.Identifier && decl.id.name === varName) {
|
|
96
|
+
return decl;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
current = current.parent;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
72
106
|
function isSetterMethodCall(node) {
|
|
73
107
|
if (node.type !== utils_1.AST_NODE_TYPES.CallExpression)
|
|
74
108
|
return { isValid: false };
|
|
@@ -84,6 +118,16 @@ function isSetterMethodCall(node) {
|
|
|
84
118
|
if (object.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
85
119
|
return { isValid: false };
|
|
86
120
|
const setterInstance = object.name;
|
|
121
|
+
// Find the variable declaration
|
|
122
|
+
const decl = findVariableDeclaration(node, setterInstance);
|
|
123
|
+
if (!decl || !decl.init)
|
|
124
|
+
return { isValid: false };
|
|
125
|
+
// Skip if it's a Map instance
|
|
126
|
+
if (isMapInstance(decl.init))
|
|
127
|
+
return { isValid: false };
|
|
128
|
+
// Check if it's a Firestore setter instance
|
|
129
|
+
if (!isFirestoreSetterInstance(decl.init))
|
|
130
|
+
return { isValid: false };
|
|
87
131
|
// Get the method name
|
|
88
132
|
const methodName = callee.property.name;
|
|
89
133
|
return { isValid: true, methodName, setterInstance };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const preferCloneDeep: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferCloneDeep", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preferCloneDeep = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
exports.preferCloneDeep = (0, createRule_1.createRule)({
|
|
7
|
+
name: 'prefer-clone-deep',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Prefer using cloneDeep over nested spread copying',
|
|
12
|
+
recommended: 'error',
|
|
13
|
+
},
|
|
14
|
+
fixable: 'code',
|
|
15
|
+
schema: [],
|
|
16
|
+
messages: {
|
|
17
|
+
preferCloneDeep: 'Use cloneDeep from functions/src/util/cloneDeep.ts instead of nested spread operators for deep object copying',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: [],
|
|
21
|
+
create(context) {
|
|
22
|
+
function hasNestedSpread(node) {
|
|
23
|
+
let spreadCount = 0;
|
|
24
|
+
let hasFunction = false;
|
|
25
|
+
let hasSymbol = false;
|
|
26
|
+
function visit(node) {
|
|
27
|
+
if (node.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
28
|
+
spreadCount++;
|
|
29
|
+
}
|
|
30
|
+
else if (node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
31
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
32
|
+
hasFunction = true;
|
|
33
|
+
}
|
|
34
|
+
else if (node.type === utils_1.AST_NODE_TYPES.Property &&
|
|
35
|
+
node.computed &&
|
|
36
|
+
node.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
37
|
+
node.key.name === 'Symbol') {
|
|
38
|
+
hasSymbol = true;
|
|
39
|
+
}
|
|
40
|
+
for (const key in node) {
|
|
41
|
+
const value = node[key];
|
|
42
|
+
if (value && typeof value === 'object') {
|
|
43
|
+
visit(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
visit(node);
|
|
48
|
+
return spreadCount > 1 && !hasFunction && !hasSymbol;
|
|
49
|
+
}
|
|
50
|
+
function getSourceText(node) {
|
|
51
|
+
return context.getSourceCode().getText(node);
|
|
52
|
+
}
|
|
53
|
+
function generateCloneDeepFix(node) {
|
|
54
|
+
const sourceText = getSourceText(node);
|
|
55
|
+
return `cloneDeep(${sourceText.replace(/\.\.\./g, '')}, {} as const)`;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
ObjectExpression(node) {
|
|
59
|
+
if (hasNestedSpread(node)) {
|
|
60
|
+
context.report({
|
|
61
|
+
node,
|
|
62
|
+
messageId: 'preferCloneDeep',
|
|
63
|
+
fix(fixer) {
|
|
64
|
+
return fixer.replaceText(node, generateCloneDeepFix(node));
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=prefer-clone-deep.js.map
|
|
@@ -208,6 +208,24 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
208
208
|
}
|
|
209
209
|
return false;
|
|
210
210
|
}
|
|
211
|
+
function hasABPattern(params) {
|
|
212
|
+
if (params.length !== 2)
|
|
213
|
+
return false;
|
|
214
|
+
const paramNames = params.map(param => {
|
|
215
|
+
if (param.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
216
|
+
return param.name;
|
|
217
|
+
}
|
|
218
|
+
return '';
|
|
219
|
+
});
|
|
220
|
+
// Check if both parameters end with 'A' and 'B' respectively and have the same prefix
|
|
221
|
+
const [first, second] = paramNames;
|
|
222
|
+
if (first.endsWith('A') && second.endsWith('B')) {
|
|
223
|
+
const firstPrefix = first.slice(0, -1);
|
|
224
|
+
const secondPrefix = second.slice(0, -1);
|
|
225
|
+
return firstPrefix === secondPrefix;
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
211
229
|
function shouldIgnoreNode(node) {
|
|
212
230
|
// Ignore built-in objects and third-party modules
|
|
213
231
|
if (isBuiltInOrThirdParty(node))
|
|
@@ -230,6 +248,16 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
230
248
|
parent = parent.parent;
|
|
231
249
|
}
|
|
232
250
|
}
|
|
251
|
+
// Ignore functions with A/B pattern parameters
|
|
252
|
+
if ((node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
253
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
254
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
255
|
+
node.type === utils_1.AST_NODE_TYPES.TSMethodSignature ||
|
|
256
|
+
node.type === utils_1.AST_NODE_TYPES.TSFunctionType) &&
|
|
257
|
+
Array.isArray(node.params)) {
|
|
258
|
+
if (hasABPattern(node.params))
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
233
261
|
return false;
|
|
234
262
|
}
|
|
235
263
|
function checkFunction(node) {
|
|
@@ -41,9 +41,9 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
41
41
|
const scope = context.getScope();
|
|
42
42
|
const variable = scope.variables.find(v => v.name === typeName.name);
|
|
43
43
|
if (!variable || !variable.defs[0]?.node) {
|
|
44
|
-
// If we can't find the type definition, assume it's a type with
|
|
44
|
+
// If we can't find the type definition, assume it's a type with required properties
|
|
45
45
|
// This handles cases where the type is imported from another module
|
|
46
|
-
return
|
|
46
|
+
return false;
|
|
47
47
|
}
|
|
48
48
|
const def = variable.defs[0].node;
|
|
49
49
|
if (def.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
@@ -58,9 +58,9 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
// If we found the type definition but it's not a type alias or interface declaration,
|
|
61
|
-
// assume it's a type with
|
|
61
|
+
// assume it's a type with required properties
|
|
62
62
|
// This handles cases where the type is imported from another module
|
|
63
|
-
return
|
|
63
|
+
return false;
|
|
64
64
|
}
|
|
65
65
|
// Handle type alias declarations
|
|
66
66
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
@@ -77,71 +77,130 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
77
77
|
}
|
|
78
78
|
return false;
|
|
79
79
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
return {
|
|
81
|
+
'ArrowFunctionExpression, FunctionDeclaration'(node) {
|
|
82
|
+
// Check if it's a hook function
|
|
83
|
+
let isHook = false;
|
|
84
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
85
|
+
isHook = node.id ? isHookName(node.id.name) : false;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const parent = node.parent;
|
|
89
|
+
if (parent &&
|
|
90
|
+
parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
91
|
+
parent.id &&
|
|
92
|
+
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
93
|
+
isHook = isHookName(parent.id.name);
|
|
86
94
|
}
|
|
87
95
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
}
|
|
96
|
+
if (!isHook) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Check if it has exactly one parameter
|
|
100
|
+
if (node.params.length !== 1) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Check if the parameter is already an assignment pattern
|
|
104
|
+
const param = node.params[0];
|
|
105
|
+
if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Check if the parameter has a type annotation
|
|
109
|
+
if (param.type === utils_1.AST_NODE_TYPES.ObjectPattern && param.typeAnnotation) {
|
|
110
|
+
const typeAnnotation = param.typeAnnotation.typeAnnotation;
|
|
111
|
+
if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
112
|
+
const typeName = typeAnnotation.typeName;
|
|
113
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
114
|
+
const scope = context.getScope();
|
|
115
|
+
const variable = scope.variables.find(v => v.name === typeName.name);
|
|
116
|
+
if (variable && variable.defs[0]?.node) {
|
|
117
|
+
const def = variable.defs[0].node;
|
|
118
|
+
if (def.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
119
|
+
if (hasAllOptionalProperties(def.typeAnnotation)) {
|
|
120
|
+
context.report({
|
|
121
|
+
node: param,
|
|
122
|
+
messageId: 'requireDefaultParams',
|
|
123
|
+
fix(fixer) {
|
|
124
|
+
const paramText = context.getSourceCode().getText(param);
|
|
125
|
+
return fixer.replaceText(param, `${paramText} = {}`);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (def.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
131
|
+
if (def.body.body.every(member => {
|
|
132
|
+
if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return member.optional === true;
|
|
136
|
+
})) {
|
|
137
|
+
context.report({
|
|
138
|
+
node: param,
|
|
139
|
+
messageId: 'requireDefaultParams',
|
|
140
|
+
fix(fixer) {
|
|
141
|
+
const paramText = context.getSourceCode().getText(param);
|
|
142
|
+
return fixer.replaceText(param, `${paramText} = {}`);
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
122
147
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
148
|
+
else {
|
|
149
|
+
// If we can't find the type definition, check if it's defined in the same file
|
|
150
|
+
const program = context.getSourceCode().ast;
|
|
151
|
+
const typeDefinitions = program.body.filter(node => {
|
|
152
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration || node.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
153
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
154
|
+
return node.id.name === typeName.name;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
return node.id.name === typeName.name;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
127
160
|
return false;
|
|
128
|
-
}
|
|
129
|
-
return member.optional === true;
|
|
130
|
-
})) {
|
|
131
|
-
context.report({
|
|
132
|
-
node: param,
|
|
133
|
-
messageId: 'requireDefaultParams',
|
|
134
|
-
fix(fixer) {
|
|
135
|
-
const paramText = context.getSourceCode().getText(param);
|
|
136
|
-
return fixer.replaceText(param, `${paramText} = {}`);
|
|
137
|
-
},
|
|
138
161
|
});
|
|
162
|
+
if (typeDefinitions.length > 0) {
|
|
163
|
+
const def = typeDefinitions[0];
|
|
164
|
+
if (def.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
165
|
+
if (hasAllOptionalProperties(def.typeAnnotation)) {
|
|
166
|
+
context.report({
|
|
167
|
+
node: param,
|
|
168
|
+
messageId: 'requireDefaultParams',
|
|
169
|
+
fix(fixer) {
|
|
170
|
+
const paramText = context.getSourceCode().getText(param);
|
|
171
|
+
return fixer.replaceText(param, `${paramText} = {}`);
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (def.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
177
|
+
if (def.body.body.every(member => {
|
|
178
|
+
if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return member.optional === true;
|
|
182
|
+
})) {
|
|
183
|
+
context.report({
|
|
184
|
+
node: param,
|
|
185
|
+
messageId: 'requireDefaultParams',
|
|
186
|
+
fix(fixer) {
|
|
187
|
+
const paramText = context.getSourceCode().getText(param);
|
|
188
|
+
return fixer.replaceText(param, `${paramText} = {}`);
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
139
194
|
}
|
|
140
195
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
196
|
+
}
|
|
197
|
+
else if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
198
|
+
if (typeAnnotation.members.every(member => {
|
|
199
|
+
if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return member.optional === true;
|
|
203
|
+
})) {
|
|
145
204
|
context.report({
|
|
146
205
|
node: param,
|
|
147
206
|
messageId: 'requireDefaultParams',
|
|
@@ -153,41 +212,6 @@ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
|
|
|
153
212
|
}
|
|
154
213
|
}
|
|
155
214
|
}
|
|
156
|
-
else if (hasAllOptionalProperties(typeAnnotation)) {
|
|
157
|
-
context.report({
|
|
158
|
-
node: param,
|
|
159
|
-
messageId: 'requireDefaultParams',
|
|
160
|
-
fix(fixer) {
|
|
161
|
-
const paramText = context.getSourceCode().getText(param);
|
|
162
|
-
return fixer.replaceText(param, `${paramText} = {}`);
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return {
|
|
169
|
-
FunctionDeclaration(node) {
|
|
170
|
-
if (!node.id || !isHookName(node.id.name)) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (node.params.length !== 1) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
checkHookParam(node.params[0]);
|
|
177
|
-
},
|
|
178
|
-
ArrowFunctionExpression(node) {
|
|
179
|
-
const parent = node.parent;
|
|
180
|
-
if (!parent ||
|
|
181
|
-
parent.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
182
|
-
!parent.id ||
|
|
183
|
-
parent.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
184
|
-
!isHookName(parent.id.name)) {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
if (node.params.length !== 1) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
checkHookParam(node.params[0]);
|
|
191
215
|
},
|
|
192
216
|
};
|
|
193
217
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blumintinc/eslint-plugin-blumint",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Custom eslint rules for use within BluMint",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Brodie McGuire",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"dotenv-cli": "5.0.0",
|
|
72
72
|
"eslint": "8.57.0",
|
|
73
73
|
"eslint-config-prettier": "8.5.0",
|
|
74
|
-
"eslint-doc-generator": "1.7.1",
|
|
74
|
+
"eslint-doc-generator": "^1.7.1",
|
|
75
75
|
"eslint-import-resolver-typescript": "3.5.5",
|
|
76
76
|
"eslint-plugin-eslint-plugin": "5.0.0",
|
|
77
77
|
"eslint-plugin-import": "2.26.0",
|