@agilebot/eslint-plugin 0.2.2 → 0.2.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/LICENSE +9 -9
- package/dist/index.d.ts +30 -0
- package/{lib/rules/react/better-exhaustive-deps.js → dist/index.js} +1063 -402
- package/package.json +11 -6
- package/lib/index.js +0 -43
- package/lib/rules/import/enforce-icon-alias.js +0 -42
- package/lib/rules/import/monorepo.js +0 -49
- package/lib/rules/intl/id-missing.js +0 -111
- package/lib/rules/intl/id-prefix.js +0 -103
- package/lib/rules/intl/id-unused.js +0 -123
- package/lib/rules/intl/no-default.js +0 -63
- package/lib/rules/others/no-unnecessary-template-literals.js +0 -38
- package/lib/rules/react/hook-use-ref.js +0 -35
- package/lib/rules/react/no-inline-styles.js +0 -87
- package/lib/rules/react/prefer-named-property-access.js +0 -105
- package/lib/rules/tss/class-naming.js +0 -43
- package/lib/rules/tss/no-color-value.js +0 -58
- package/lib/rules/tss/unused-classes.js +0 -108
- package/lib/util/intl.js +0 -127
- package/lib/util/settings.js +0 -14
- package/lib/util/translations.js +0 -66
- package/lib/util/tss.js +0 -109
@@ -1,11 +1,527 @@
|
|
1
1
|
/**
|
2
|
-
*
|
2
|
+
* @license @agilebot/eslint-plugin v0.2.4
|
3
|
+
*
|
4
|
+
* Copyright (c) Agilebot, Inc. and its affiliates.
|
3
5
|
*
|
4
6
|
* This source code is licensed under the MIT license found in the
|
5
7
|
* LICENSE file in the root directory of this source tree.
|
6
8
|
*/
|
9
|
+
'use strict';
|
10
|
+
|
11
|
+
var fs = require('node:fs');
|
12
|
+
var path = require('node:path');
|
13
|
+
var eslintUtils = require('@agilebot/eslint-utils');
|
14
|
+
var utils = require('@typescript-eslint/utils');
|
15
|
+
|
16
|
+
function _interopNamespaceDefault(e) {
|
17
|
+
var n = Object.create(null);
|
18
|
+
if (e) {
|
19
|
+
Object.keys(e).forEach(function (k) {
|
20
|
+
if (k !== 'default') {
|
21
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
22
|
+
Object.defineProperty(n, k, d.get ? d : {
|
23
|
+
enumerable: true,
|
24
|
+
get: function () { return e[k]; }
|
25
|
+
});
|
26
|
+
}
|
27
|
+
});
|
28
|
+
}
|
29
|
+
n.default = e;
|
30
|
+
return Object.freeze(n);
|
31
|
+
}
|
32
|
+
|
33
|
+
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
|
34
|
+
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
35
|
+
|
36
|
+
var enforceIconAlias = {
|
37
|
+
meta: {
|
38
|
+
type: 'problem',
|
39
|
+
docs: {
|
40
|
+
description: 'Enforce alias for @mui/icons-material imports',
|
41
|
+
recommended: true
|
42
|
+
},
|
43
|
+
fixable: 'code',
|
44
|
+
schema: []
|
45
|
+
},
|
46
|
+
create(context) {
|
47
|
+
return {
|
48
|
+
ImportDeclaration(node) {
|
49
|
+
if (
|
50
|
+
node.source.value !== '@mui/icons-material' &&
|
51
|
+
node.source.value !== 'mdi-material-ui'
|
52
|
+
) {
|
53
|
+
return;
|
54
|
+
}
|
55
|
+
for (const specifier of node.specifiers) {
|
56
|
+
if (specifier.type !== 'ImportSpecifier') {
|
57
|
+
return;
|
58
|
+
}
|
59
|
+
if (specifier.imported.name === specifier.local.name) {
|
60
|
+
context.report({
|
61
|
+
node,
|
62
|
+
message: `Import for ${node.source.value} should be aliased.`,
|
63
|
+
fix: fixer =>
|
64
|
+
fixer.replaceText(
|
65
|
+
specifier,
|
66
|
+
`${specifier.imported.name} as ${specifier.imported.name}Icon`
|
67
|
+
)
|
68
|
+
});
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
};
|
73
|
+
}
|
74
|
+
};
|
75
|
+
|
76
|
+
function getSetting(context, name) {
|
77
|
+
return context.settings[`agilebot/${name}`];
|
78
|
+
}
|
79
|
+
|
80
|
+
let warnedForMissingPrefix = false;
|
81
|
+
var monorepo = {
|
82
|
+
meta: {
|
83
|
+
type: 'problem',
|
84
|
+
docs: {
|
85
|
+
description: 'Enforce import styles for monorepo',
|
86
|
+
recommended: true
|
87
|
+
},
|
88
|
+
fixable: 'code',
|
89
|
+
schema: []
|
90
|
+
},
|
91
|
+
create(context) {
|
92
|
+
return {
|
93
|
+
ImportDeclaration(node) {
|
94
|
+
let prefix = getSetting(context, 'monorepo-scope');
|
95
|
+
if (!prefix) {
|
96
|
+
if (!warnedForMissingPrefix) {
|
97
|
+
console.error('Warning: agilebot/monorepo-scope is not set.');
|
98
|
+
warnedForMissingPrefix = true;
|
99
|
+
}
|
100
|
+
return;
|
101
|
+
}
|
102
|
+
prefix = `${prefix}/`;
|
103
|
+
if (typeof node.source.value !== 'string') {
|
104
|
+
return;
|
105
|
+
}
|
106
|
+
if (!node.source.value.startsWith(prefix)) {
|
107
|
+
return;
|
108
|
+
}
|
109
|
+
const values = node.source.value.split('/');
|
110
|
+
if (values[2] === 'src') {
|
111
|
+
context.report({
|
112
|
+
node,
|
113
|
+
message: `Import for ${node.source.value} should not contains src folder.`,
|
114
|
+
fix: fixer => {
|
115
|
+
const correctedPath = values
|
116
|
+
.filter((_, index) => index !== 2)
|
117
|
+
.join('/');
|
118
|
+
return fixer.replaceText(node.source, `'${correctedPath}'`);
|
119
|
+
}
|
120
|
+
});
|
121
|
+
}
|
122
|
+
}
|
123
|
+
};
|
124
|
+
}
|
125
|
+
};
|
126
|
+
|
127
|
+
function findFormatMessageAttrNode(node, attrName) {
|
128
|
+
if (
|
129
|
+
node.type === 'CallExpression' &&
|
130
|
+
(node.callee.name === 'formatMessage' || node.callee.name === '$t') &&
|
131
|
+
node.arguments.length > 0 &&
|
132
|
+
node.arguments[0].properties
|
133
|
+
) {
|
134
|
+
return node.arguments[0].properties.find(
|
135
|
+
a => a.key && a.key.name === attrName
|
136
|
+
);
|
137
|
+
}
|
138
|
+
if (
|
139
|
+
node.type === 'CallExpression' &&
|
140
|
+
node.callee.type === 'MemberExpression' &&
|
141
|
+
(node.callee.object.name === 'intl' ||
|
142
|
+
(node.callee.object.name && node.callee.object.name.endsWith('Intl'))) &&
|
143
|
+
(node.callee.property.name === 'formatMessage' ||
|
144
|
+
node.callee.property.name === '$t')
|
145
|
+
) {
|
146
|
+
return node.arguments[0].properties.find(
|
147
|
+
a => a.key && a.key.name === attrName
|
148
|
+
);
|
149
|
+
}
|
150
|
+
}
|
151
|
+
function findFormattedMessageAttrNode(node, attrName) {
|
152
|
+
if (
|
153
|
+
node.type === 'JSXIdentifier' &&
|
154
|
+
node.name === 'FormattedMessage' &&
|
155
|
+
node.parent &&
|
156
|
+
node.parent.type === 'JSXOpeningElement'
|
157
|
+
) {
|
158
|
+
return node.parent.attributes.find(a => a.name && a.name.name === attrName);
|
159
|
+
}
|
160
|
+
}
|
161
|
+
function findAttrNodeInDefineMessages(node, attrName) {
|
162
|
+
if (
|
163
|
+
node.type === 'Property' &&
|
164
|
+
node.key.name === attrName &&
|
165
|
+
node.parent &&
|
166
|
+
node.parent.parent &&
|
167
|
+
node.parent.parent.parent &&
|
168
|
+
node.parent.parent.parent.parent &&
|
169
|
+
node.parent.parent.parent.parent.type === 'CallExpression' &&
|
170
|
+
node.parent.parent.parent.parent.callee.name === 'defineMessages'
|
171
|
+
) {
|
172
|
+
return node;
|
173
|
+
}
|
174
|
+
}
|
175
|
+
function findAttrNodeInDefineMessage(node, attrName) {
|
176
|
+
if (
|
177
|
+
node.type === 'Property' &&
|
178
|
+
node.key.name === attrName &&
|
179
|
+
node.parent &&
|
180
|
+
node.parent.parent &&
|
181
|
+
node.parent.parent.type === 'CallExpression' &&
|
182
|
+
node.parent.parent.callee.name === 'defineMessage'
|
183
|
+
) {
|
184
|
+
return node;
|
185
|
+
}
|
186
|
+
}
|
187
|
+
function sortedTemplateElements(node) {
|
188
|
+
return [...node.quasis, ...node.expressions].sort(
|
189
|
+
(a, b) => a.range[0] - b.range[0]
|
190
|
+
);
|
191
|
+
}
|
192
|
+
function templateLiteralDisplayStr(node) {
|
193
|
+
return sortedTemplateElements(node)
|
194
|
+
.map(e => (!e.value ? '*' : e.value.raw))
|
195
|
+
.join('');
|
196
|
+
}
|
197
|
+
|
198
|
+
const localeFilesKeys = {};
|
199
|
+
function getIntlIds(context) {
|
200
|
+
const projectRoot = getSetting(context, 'project-root');
|
201
|
+
const localeFiles = getSetting(context, 'locale-files');
|
202
|
+
if (!localeFiles) {
|
203
|
+
throw new Error('localeFiles not in settings');
|
204
|
+
}
|
205
|
+
const results = [];
|
206
|
+
localeFiles.forEach(f => {
|
207
|
+
const fullPath = projectRoot ? path__namespace.join(projectRoot, f) : f;
|
208
|
+
const mtime = fs__namespace.lstatSync(fullPath).mtime.getTime();
|
209
|
+
if (
|
210
|
+
!localeFilesKeys[fullPath] ||
|
211
|
+
mtime !== localeFilesKeys[fullPath].mtime
|
212
|
+
) {
|
213
|
+
let json;
|
214
|
+
if (fullPath.endsWith('.json')) {
|
215
|
+
json = JSON.parse(fs__namespace.readFileSync(fullPath, 'utf8'));
|
216
|
+
} else if (fullPath.endsWith('.ts')) {
|
217
|
+
json = eslintUtils.tsImport(fullPath);
|
218
|
+
if (typeof json === 'object' && json.default) {
|
219
|
+
json = json.default;
|
220
|
+
}
|
221
|
+
} else if (fullPath.endsWith('.js')) {
|
222
|
+
json = require(fullPath);
|
223
|
+
if (typeof json === 'object' && json.default) {
|
224
|
+
json = json.default;
|
225
|
+
}
|
226
|
+
} else {
|
227
|
+
throw new Error('unsupported file extension');
|
228
|
+
}
|
229
|
+
localeFilesKeys[fullPath] = {
|
230
|
+
keys: Object.keys(json),
|
231
|
+
mtime: mtime
|
232
|
+
};
|
233
|
+
}
|
234
|
+
results.push(...localeFilesKeys[fullPath].keys);
|
235
|
+
});
|
236
|
+
return results;
|
237
|
+
}
|
238
|
+
|
239
|
+
var idMissing = {
|
240
|
+
meta: {
|
241
|
+
docs: {
|
242
|
+
description: 'Validates intl message ids are in locale file',
|
243
|
+
category: 'Intl',
|
244
|
+
recommended: true
|
245
|
+
},
|
246
|
+
fixable: undefined,
|
247
|
+
schema: []
|
248
|
+
},
|
249
|
+
create: function (context) {
|
250
|
+
const translatedIds = getIntlIds(context);
|
251
|
+
const translatedIdSet = new Set(translatedIds);
|
252
|
+
function isLiteralTranslated(id) {
|
253
|
+
return translatedIdSet.has(id);
|
254
|
+
}
|
255
|
+
function isTemplateTranslated(re) {
|
256
|
+
return translatedIds.some(k => re.test(k));
|
257
|
+
}
|
258
|
+
function processLiteral(node) {
|
259
|
+
if (!isLiteralTranslated(node.value)) {
|
260
|
+
context.report({
|
261
|
+
node: node,
|
262
|
+
message: 'Missing id: ' + node.value
|
263
|
+
});
|
264
|
+
}
|
265
|
+
}
|
266
|
+
function processTemplateLiteral(node) {
|
267
|
+
const exStr = sortedTemplateElements(node)
|
268
|
+
.map(e => (!e.value ? '.*' : e.value.raw))
|
269
|
+
.join('');
|
270
|
+
const re = new RegExp(exStr);
|
271
|
+
if (!isTemplateTranslated(re)) {
|
272
|
+
context.report({
|
273
|
+
node: node,
|
274
|
+
message: 'Missing id pattern: ' + templateLiteralDisplayStr(node)
|
275
|
+
});
|
276
|
+
}
|
277
|
+
}
|
278
|
+
function processAttrNode(node) {
|
279
|
+
if (node.value.type === 'Literal') {
|
280
|
+
return processLiteral(node.value);
|
281
|
+
}
|
282
|
+
if (
|
283
|
+
node.value.type === 'JSXExpressionContainer' &&
|
284
|
+
node.value.expression.type === 'TemplateLiteral'
|
285
|
+
) {
|
286
|
+
return processTemplateLiteral(node.value.expression);
|
287
|
+
}
|
288
|
+
if (node.value.type === 'TemplateLiteral') {
|
289
|
+
return processTemplateLiteral(node.value);
|
290
|
+
}
|
291
|
+
context.report({
|
292
|
+
node: node,
|
293
|
+
message: 'Do not invoke intl by ' + node.value.type
|
294
|
+
});
|
295
|
+
}
|
296
|
+
return {
|
297
|
+
JSXIdentifier: function (node) {
|
298
|
+
const attrNode = findFormattedMessageAttrNode(node, 'id');
|
299
|
+
if (attrNode) {
|
300
|
+
return processAttrNode(attrNode);
|
301
|
+
}
|
302
|
+
},
|
303
|
+
CallExpression: function (node) {
|
304
|
+
const attrNode = findFormatMessageAttrNode(node, 'id');
|
305
|
+
if (attrNode) {
|
306
|
+
return processAttrNode(attrNode);
|
307
|
+
}
|
308
|
+
},
|
309
|
+
Property: function (node) {
|
310
|
+
const attrNode =
|
311
|
+
findAttrNodeInDefineMessages(node, 'id') ||
|
312
|
+
findAttrNodeInDefineMessage(node, 'id');
|
313
|
+
if (attrNode) {
|
314
|
+
return processAttrNode(attrNode);
|
315
|
+
}
|
316
|
+
}
|
317
|
+
};
|
318
|
+
}
|
319
|
+
};
|
320
|
+
|
321
|
+
var idPrefix = {
|
322
|
+
meta: {
|
323
|
+
docs: {
|
324
|
+
description: 'Validates intl message ids has correct prefixes',
|
325
|
+
category: 'Intl',
|
326
|
+
recommended: true
|
327
|
+
},
|
328
|
+
fixable: undefined,
|
329
|
+
schema: [
|
330
|
+
{
|
331
|
+
type: 'array',
|
332
|
+
items: {
|
333
|
+
type: 'string'
|
334
|
+
}
|
335
|
+
}
|
336
|
+
]
|
337
|
+
},
|
338
|
+
create: function (context) {
|
339
|
+
if (context.options[0].length === 0) {
|
340
|
+
throw new Error('Prefixes are required in settings');
|
341
|
+
}
|
342
|
+
const hasPrefix = value =>
|
343
|
+
context.options[0].some(p => value.startsWith(p));
|
344
|
+
function report(node, value) {
|
345
|
+
if (!hasPrefix(value)) {
|
346
|
+
context.report({
|
347
|
+
node: node,
|
348
|
+
message: `Invalid id prefix: ${value}`
|
349
|
+
});
|
350
|
+
}
|
351
|
+
}
|
352
|
+
function processLiteral(node) {
|
353
|
+
report(node, node.value);
|
354
|
+
}
|
355
|
+
function processTemplateLiteral(node) {
|
356
|
+
const displayStr = templateLiteralDisplayStr(node);
|
357
|
+
report(node, displayStr);
|
358
|
+
}
|
359
|
+
function processAttrNode(node) {
|
360
|
+
if (node.value.type === 'Literal') {
|
361
|
+
return processLiteral(node.value);
|
362
|
+
}
|
363
|
+
if (
|
364
|
+
node.value.type === 'JSXExpressionContainer' &&
|
365
|
+
node.value.expression.type === 'TemplateLiteral'
|
366
|
+
) {
|
367
|
+
return processTemplateLiteral(node.value.expression);
|
368
|
+
}
|
369
|
+
if (node.value.type === 'TemplateLiteral') {
|
370
|
+
return processTemplateLiteral(node.value);
|
371
|
+
}
|
372
|
+
}
|
373
|
+
return {
|
374
|
+
JSXIdentifier: function (node) {
|
375
|
+
const attrNode = findFormattedMessageAttrNode(node, 'id');
|
376
|
+
if (attrNode) {
|
377
|
+
return processAttrNode(attrNode);
|
378
|
+
}
|
379
|
+
},
|
380
|
+
CallExpression: function (node) {
|
381
|
+
const attrNode = findFormatMessageAttrNode(node, 'id');
|
382
|
+
if (attrNode) {
|
383
|
+
return processAttrNode(attrNode);
|
384
|
+
}
|
385
|
+
},
|
386
|
+
Property: function (node) {
|
387
|
+
const attrNode =
|
388
|
+
findAttrNodeInDefineMessages(node, 'id') ||
|
389
|
+
findAttrNodeInDefineMessage(node, 'id');
|
390
|
+
if (attrNode) {
|
391
|
+
return processAttrNode(attrNode);
|
392
|
+
}
|
393
|
+
}
|
394
|
+
};
|
395
|
+
}
|
396
|
+
};
|
397
|
+
|
398
|
+
const usedIds = new Map();
|
399
|
+
var idUnused = {
|
400
|
+
meta: {
|
401
|
+
docs: {
|
402
|
+
description: 'Finds unused intl message ids in locale file',
|
403
|
+
category: 'Intl',
|
404
|
+
recommended: true
|
405
|
+
},
|
406
|
+
fixable: undefined,
|
407
|
+
schema: []
|
408
|
+
},
|
409
|
+
create: function (context) {
|
410
|
+
const projectRoot = getSetting(context, 'project-root');
|
411
|
+
if (!projectRoot) {
|
412
|
+
throw new Error('projectRoot must be set in this rule');
|
413
|
+
}
|
414
|
+
if (!usedIds.has(projectRoot)) {
|
415
|
+
usedIds.set(projectRoot, new Set());
|
416
|
+
}
|
417
|
+
const usedIdSet = usedIds.get(projectRoot);
|
418
|
+
const translatedIds = getIntlIds(context);
|
419
|
+
const translatedIdSet = new Set(translatedIds);
|
420
|
+
function isLiteralTranslated(id) {
|
421
|
+
return translatedIdSet.has(id);
|
422
|
+
}
|
423
|
+
function isTemplateTranslated(re) {
|
424
|
+
return translatedIds.some(k => re.test(k));
|
425
|
+
}
|
426
|
+
function processLiteral(node) {
|
427
|
+
if (isLiteralTranslated(node.value)) {
|
428
|
+
usedIdSet.add(node.value);
|
429
|
+
}
|
430
|
+
}
|
431
|
+
function processTemplateLiteral(node) {
|
432
|
+
const exStr = sortedTemplateElements(node)
|
433
|
+
.map(e => (!e.value ? '.*' : e.value.raw))
|
434
|
+
.join('');
|
435
|
+
const re = new RegExp(exStr);
|
436
|
+
if (isTemplateTranslated(re)) ;
|
437
|
+
}
|
438
|
+
function processAttrNode(node) {
|
439
|
+
if (node.value.type === 'Literal') {
|
440
|
+
return processLiteral(node.value);
|
441
|
+
}
|
442
|
+
if (
|
443
|
+
node.value.type === 'JSXExpressionContainer' &&
|
444
|
+
node.value.expression.type === 'TemplateLiteral'
|
445
|
+
) {
|
446
|
+
return processTemplateLiteral(node.value.expression);
|
447
|
+
}
|
448
|
+
if (node.value.type === 'TemplateLiteral') {
|
449
|
+
return processTemplateLiteral(node.value);
|
450
|
+
}
|
451
|
+
}
|
452
|
+
return {
|
453
|
+
JSXIdentifier: function (node) {
|
454
|
+
const attrNode = findFormattedMessageAttrNode(node, 'id');
|
455
|
+
if (attrNode) {
|
456
|
+
return processAttrNode(attrNode);
|
457
|
+
}
|
458
|
+
},
|
459
|
+
CallExpression: function (node) {
|
460
|
+
const attrNode = findFormatMessageAttrNode(node, 'id');
|
461
|
+
if (attrNode) {
|
462
|
+
return processAttrNode(attrNode);
|
463
|
+
}
|
464
|
+
},
|
465
|
+
Property: function (node) {
|
466
|
+
const attrNode =
|
467
|
+
findAttrNodeInDefineMessages(node, 'id') ||
|
468
|
+
findAttrNodeInDefineMessage(node, 'id');
|
469
|
+
if (attrNode) {
|
470
|
+
return processAttrNode(attrNode);
|
471
|
+
}
|
472
|
+
},
|
473
|
+
'Program:exit': function () {
|
474
|
+
const unusedIds = [...translatedIdSet].filter(id => !usedIdSet.has(id));
|
475
|
+
const jsonPath = path__namespace.join(projectRoot, 'intl-unused.json');
|
476
|
+
fs__namespace.writeFileSync(jsonPath, JSON.stringify(unusedIds, null, 2));
|
477
|
+
}
|
478
|
+
};
|
479
|
+
}
|
480
|
+
};
|
481
|
+
|
482
|
+
var noDefault = {
|
483
|
+
meta: {
|
484
|
+
docs: {
|
485
|
+
description: 'Validates defaultMessage is not used with react-intl',
|
486
|
+
category: 'Intl',
|
487
|
+
recommended: true
|
488
|
+
},
|
489
|
+
fixable: undefined,
|
490
|
+
schema: []
|
491
|
+
},
|
492
|
+
create: function (context) {
|
493
|
+
function processAttrNode(node) {
|
494
|
+
context.report({
|
495
|
+
node: node,
|
496
|
+
message: 'Do not use defaultMessage'
|
497
|
+
});
|
498
|
+
}
|
499
|
+
return {
|
500
|
+
JSXIdentifier: function (node) {
|
501
|
+
const attrNode = findFormattedMessageAttrNode(node, 'defaultMessage');
|
502
|
+
if (attrNode) {
|
503
|
+
return processAttrNode(attrNode);
|
504
|
+
}
|
505
|
+
},
|
506
|
+
CallExpression: function (node) {
|
507
|
+
const attrNode = findFormatMessageAttrNode(node, 'defaultMessage');
|
508
|
+
if (attrNode) {
|
509
|
+
return processAttrNode(attrNode);
|
510
|
+
}
|
511
|
+
},
|
512
|
+
Property: function (node) {
|
513
|
+
const attrNode =
|
514
|
+
findAttrNodeInDefineMessages(node, 'defaultMessage') ||
|
515
|
+
findAttrNodeInDefineMessage(node, 'defaultMessage');
|
516
|
+
if (attrNode) {
|
517
|
+
return processAttrNode(attrNode);
|
518
|
+
}
|
519
|
+
}
|
520
|
+
};
|
521
|
+
}
|
522
|
+
};
|
7
523
|
|
8
|
-
|
524
|
+
var betterExhaustiveDeps = {
|
9
525
|
meta: {
|
10
526
|
type: 'suggestion',
|
11
527
|
docs: {
|
@@ -58,44 +574,36 @@ module.exports = {
|
|
58
574
|
]
|
59
575
|
},
|
60
576
|
create(context) {
|
61
|
-
// Parse the `additionalHooks` regex.
|
62
577
|
const additionalHooks =
|
63
578
|
context.options &&
|
64
579
|
context.options[0] &&
|
65
580
|
context.options[0].additionalHooks
|
66
581
|
? new RegExp(context.options[0].additionalHooks)
|
67
582
|
: undefined;
|
68
|
-
|
69
583
|
const enableDangerousAutofixThisMayCauseInfiniteLoops =
|
70
584
|
(context.options &&
|
71
585
|
context.options[0] &&
|
72
586
|
context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops) ||
|
73
587
|
false;
|
74
|
-
|
75
|
-
// Parse the `staticHooks` object.
|
76
588
|
const staticHooks =
|
77
589
|
(context.options &&
|
78
590
|
context.options[0] &&
|
79
591
|
context.options[0].staticHooks) ||
|
80
592
|
{};
|
81
|
-
|
82
593
|
const checkMemoizedVariableIsStatic =
|
83
594
|
(context.options &&
|
84
595
|
context.options[0] &&
|
85
596
|
context.options[0].checkMemoizedVariableIsStatic) ||
|
86
597
|
false;
|
87
|
-
|
88
598
|
const options = {
|
89
599
|
additionalHooks,
|
90
600
|
enableDangerousAutofixThisMayCauseInfiniteLoops,
|
91
601
|
staticHooks,
|
92
602
|
checkMemoizedVariableIsStatic
|
93
603
|
};
|
94
|
-
|
95
604
|
function reportProblem(problem) {
|
96
605
|
if (
|
97
|
-
enableDangerousAutofixThisMayCauseInfiniteLoops &&
|
98
|
-
// Keep this as an option until major IDEs upgrade (including VSCode FB ESLint extension).
|
606
|
+
enableDangerousAutofixThisMayCauseInfiniteLoops &&
|
99
607
|
Array.isArray(problem.suggest) &&
|
100
608
|
problem.suggest.length > 0
|
101
609
|
) {
|
@@ -103,10 +611,7 @@ module.exports = {
|
|
103
611
|
}
|
104
612
|
context.report(problem);
|
105
613
|
}
|
106
|
-
|
107
614
|
const scopeManager = context.getSourceCode().scopeManager;
|
108
|
-
|
109
|
-
// Should be shared between visitors.
|
110
615
|
const setStateCallSites = new WeakMap();
|
111
616
|
const stateVariables = new WeakSet();
|
112
617
|
const stableKnownValueCache = new WeakMap();
|
@@ -114,8 +619,6 @@ module.exports = {
|
|
114
619
|
function memoizeWithWeakMap(fn, map) {
|
115
620
|
return function (arg) {
|
116
621
|
if (map.has(arg)) {
|
117
|
-
// to verify cache hits:
|
118
|
-
// console.log(arg.name)
|
119
622
|
return map.get(arg);
|
120
623
|
}
|
121
624
|
const result = fn(arg);
|
@@ -123,9 +626,6 @@ module.exports = {
|
|
123
626
|
return result;
|
124
627
|
};
|
125
628
|
}
|
126
|
-
/**
|
127
|
-
* Visitor for both function expressions and arrow function expressions.
|
128
|
-
*/
|
129
629
|
function visitFunctionWithDependencies(
|
130
630
|
node,
|
131
631
|
declaredDependenciesNode,
|
@@ -150,18 +650,7 @@ module.exports = {
|
|
150
650
|
'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching'
|
151
651
|
});
|
152
652
|
}
|
153
|
-
|
154
|
-
// Get the current scope.
|
155
653
|
const scope = scopeManager.acquire(node);
|
156
|
-
|
157
|
-
// Find all our "pure scopes". On every re-render of a component these
|
158
|
-
// pure scopes may have changes to the variables declared within. So all
|
159
|
-
// variables used in our reactive hook callback but declared in a pure
|
160
|
-
// scope need to be listed as dependencies of our reactive hook callback.
|
161
|
-
//
|
162
|
-
// According to the rules of React you can't read a mutable value in pure
|
163
|
-
// scope. We can't enforce this in a lint so we trust that all variables
|
164
|
-
// declared outside of pure scope are indeed frozen.
|
165
654
|
const pureScopes = new Set();
|
166
655
|
let componentScope = null;
|
167
656
|
{
|
@@ -173,18 +662,12 @@ module.exports = {
|
|
173
662
|
}
|
174
663
|
currentScope = currentScope.upper;
|
175
664
|
}
|
176
|
-
// If there is no parent function scope then there are no pure scopes.
|
177
|
-
// The ones we've collected so far are incorrect. So don't continue with
|
178
|
-
// the lint.
|
179
665
|
if (!currentScope) {
|
180
666
|
return;
|
181
667
|
}
|
182
668
|
componentScope = currentScope;
|
183
669
|
}
|
184
|
-
|
185
670
|
const isArray = Array.isArray;
|
186
|
-
|
187
|
-
// Remember such values. Avoid re-running extra checks on them.
|
188
671
|
const memoizedIsStableKnownHookValue = memoizeWithWeakMap(
|
189
672
|
isStableKnownHookValue,
|
190
673
|
stableKnownValueCache
|
@@ -193,20 +676,6 @@ module.exports = {
|
|
193
676
|
isFunctionWithoutCapturedValues,
|
194
677
|
functionWithoutCapturedValueCache
|
195
678
|
);
|
196
|
-
|
197
|
-
// Next we'll define a few helpers that helps us
|
198
|
-
// tell if some values don't have to be declared as deps.
|
199
|
-
|
200
|
-
// Some are known to be stable based on Hook calls.
|
201
|
-
// const [state, setState] = useState() / React.useState()
|
202
|
-
// ^^^ true for this reference
|
203
|
-
// const [state, dispatch] = useReducer() / React.useReducer()
|
204
|
-
// ^^^ true for this reference
|
205
|
-
// const ref = useRef()
|
206
|
-
// ^^^ true for this reference
|
207
|
-
// False for everything else.
|
208
|
-
// True if the value is registered in staticHooks.
|
209
|
-
// True if the value is from useMemo() or useCallback() and has zero deps.
|
210
679
|
function isStableKnownHookValue(resolved) {
|
211
680
|
if (!isArray(resolved.defs)) {
|
212
681
|
return false;
|
@@ -215,7 +684,6 @@ module.exports = {
|
|
215
684
|
if (def == null) {
|
216
685
|
return false;
|
217
686
|
}
|
218
|
-
// Look for `let stuff = ...`
|
219
687
|
if (def.node.type !== 'VariableDeclarator') {
|
220
688
|
return false;
|
221
689
|
}
|
@@ -226,13 +694,8 @@ module.exports = {
|
|
226
694
|
while (init.type === 'TSAsExpression') {
|
227
695
|
init = init.expression;
|
228
696
|
}
|
229
|
-
// Detect primitive constants
|
230
|
-
// const foo = 42
|
231
697
|
let declaration = def.node.parent;
|
232
698
|
if (declaration == null) {
|
233
|
-
// This might happen if variable is declared after the callback.
|
234
|
-
// In that case ESLint won't set up .parent refs.
|
235
|
-
// So we'll set them up manually.
|
236
699
|
fastFindReferenceWithParent(componentScope.block, def.node.id);
|
237
700
|
declaration = def.node.parent;
|
238
701
|
if (declaration == null) {
|
@@ -246,16 +709,12 @@ module.exports = {
|
|
246
709
|
typeof init.value === 'number' ||
|
247
710
|
init.value == null)
|
248
711
|
) {
|
249
|
-
// Definitely stable
|
250
712
|
return true;
|
251
713
|
}
|
252
|
-
// Detect known Hook calls
|
253
|
-
// const [_, setState] = useState()
|
254
714
|
if (init.type !== 'CallExpression') {
|
255
715
|
return false;
|
256
716
|
}
|
257
717
|
let callee = init.callee;
|
258
|
-
// Step into `= React.something` initializer.
|
259
718
|
if (
|
260
719
|
callee.type === 'MemberExpression' &&
|
261
720
|
callee.object.name === 'React' &&
|
@@ -270,16 +729,13 @@ module.exports = {
|
|
270
729
|
const id = def.node.id;
|
271
730
|
const { name } = callee;
|
272
731
|
if (name === 'useRef' && id.type === 'Identifier') {
|
273
|
-
// useRef() return value is stable.
|
274
732
|
return true;
|
275
733
|
} else if (name === 'useState' || name === 'useReducer') {
|
276
|
-
// Only consider second value in initializing tuple stable.
|
277
734
|
if (
|
278
735
|
id.type === 'ArrayPattern' &&
|
279
736
|
id.elements.length === 2 &&
|
280
737
|
isArray(resolved.identifiers)
|
281
738
|
) {
|
282
|
-
// Is second tuple value the same reference we're checking?
|
283
739
|
if (id.elements[1] === resolved.identifiers[0]) {
|
284
740
|
if (name === 'useState') {
|
285
741
|
const references = resolved.references;
|
@@ -294,7 +750,6 @@ module.exports = {
|
|
294
750
|
setStateCallSites.set(reference.identifier, id.elements[0]);
|
295
751
|
}
|
296
752
|
}
|
297
|
-
// Setter is stable.
|
298
753
|
return true;
|
299
754
|
} else if (id.elements[0] === resolved.identifiers[0]) {
|
300
755
|
if (name === 'useState') {
|
@@ -303,47 +758,34 @@ module.exports = {
|
|
303
758
|
stateVariables.add(reference.identifier);
|
304
759
|
}
|
305
760
|
}
|
306
|
-
// State variable itself is dynamic.
|
307
761
|
return false;
|
308
762
|
}
|
309
763
|
}
|
310
764
|
} else if (name === 'useTransition') {
|
311
|
-
// Only consider second value in initializing tuple stable.
|
312
765
|
if (
|
313
766
|
id.type === 'ArrayPattern' &&
|
314
767
|
id.elements.length === 2 &&
|
315
|
-
Array.isArray(resolved.identifiers) &&
|
768
|
+
Array.isArray(resolved.identifiers) &&
|
316
769
|
id.elements[1] === resolved.identifiers[0]
|
317
770
|
) {
|
318
|
-
// Setter is stable.
|
319
771
|
return true;
|
320
772
|
}
|
321
773
|
} else if (
|
322
774
|
options.checkMemoizedVariableIsStatic &&
|
323
775
|
(name === 'useMemo' || name === 'useCallback')
|
324
776
|
) {
|
325
|
-
// check memoized value is stable
|
326
|
-
// useMemo(() => { ... }, []) / useCallback((...) => { ... }, [])
|
327
777
|
const hookArgs = callee.parent.arguments;
|
328
|
-
// check it has dependency list
|
329
778
|
if (hookArgs.length < 2) {
|
330
779
|
return false;
|
331
780
|
}
|
332
|
-
|
333
781
|
const dependencies = hookArgs[1].elements;
|
334
782
|
if (dependencies.length === 0) {
|
335
|
-
// no dependency, so it's stable
|
336
783
|
return true;
|
337
784
|
}
|
338
|
-
|
339
|
-
// check all dependency is stable
|
340
785
|
for (const dependencyNode of dependencies) {
|
341
|
-
// find resolved from resolved's scope
|
342
|
-
// TODO: check dependencyNode is in arguments?
|
343
786
|
const dependencyRefernece = resolved.scope.references.find(
|
344
787
|
reference => reference.identifier === dependencyNode
|
345
788
|
);
|
346
|
-
|
347
789
|
if (
|
348
790
|
dependencyRefernece !== undefined &&
|
349
791
|
memoizedIsStableKnownHookValue(dependencyRefernece.resolved)
|
@@ -353,10 +795,8 @@ module.exports = {
|
|
353
795
|
return false;
|
354
796
|
}
|
355
797
|
}
|
356
|
-
|
357
798
|
return true;
|
358
799
|
} else {
|
359
|
-
// filter regexp first
|
360
800
|
Object.entries(options.staticHooks).forEach(([key, staticParts]) => {
|
361
801
|
if (
|
362
802
|
typeof staticParts === 'object' &&
|
@@ -366,20 +806,16 @@ module.exports = {
|
|
366
806
|
options.staticHooks[name] = staticParts.value;
|
367
807
|
}
|
368
808
|
});
|
369
|
-
|
370
809
|
if (options.staticHooks[name]) {
|
371
810
|
const staticParts = options.staticHooks[name];
|
372
811
|
if (staticParts === true) {
|
373
|
-
// entire return value is static
|
374
812
|
return true;
|
375
813
|
} else if (Array.isArray(staticParts)) {
|
376
|
-
// destructured tuple return where some elements are static
|
377
814
|
if (
|
378
815
|
id.type === 'ArrayPattern' &&
|
379
816
|
id.elements.length <= staticParts.length &&
|
380
817
|
Array.isArray(resolved.identifiers)
|
381
818
|
) {
|
382
|
-
// find index of the resolved ident in the array pattern
|
383
819
|
const idx = id.elements.indexOf(resolved.identifiers[0]);
|
384
820
|
if (idx >= 0) {
|
385
821
|
return staticParts[idx];
|
@@ -389,7 +825,6 @@ module.exports = {
|
|
389
825
|
typeof staticParts === 'object' &&
|
390
826
|
id.type === 'ObjectPattern'
|
391
827
|
) {
|
392
|
-
// destructured object return where some properties are static
|
393
828
|
const property = id.properties.find(
|
394
829
|
p => p.key === resolved.identifiers[0]
|
395
830
|
);
|
@@ -399,11 +834,8 @@ module.exports = {
|
|
399
834
|
}
|
400
835
|
}
|
401
836
|
}
|
402
|
-
// By default assume it's dynamic.
|
403
837
|
return false;
|
404
838
|
}
|
405
|
-
|
406
|
-
// Some are just functions that don't reference anything dynamic.
|
407
839
|
function isFunctionWithoutCapturedValues(resolved) {
|
408
840
|
if (!isArray(resolved.defs)) {
|
409
841
|
return false;
|
@@ -415,8 +847,6 @@ module.exports = {
|
|
415
847
|
if (def.node == null || def.node.id == null) {
|
416
848
|
return false;
|
417
849
|
}
|
418
|
-
// Search the direct component subscopes for
|
419
|
-
// top-level function definitions matching this reference.
|
420
850
|
const fnNode = def.node;
|
421
851
|
const childScopes = componentScope.childScopes;
|
422
852
|
let fnScope = null;
|
@@ -425,15 +855,11 @@ module.exports = {
|
|
425
855
|
const childScope = childScopes[i];
|
426
856
|
const childScopeBlock = childScope.block;
|
427
857
|
if (
|
428
|
-
// function handleChange() {}
|
429
858
|
(fnNode.type === 'FunctionDeclaration' &&
|
430
859
|
childScopeBlock === fnNode) ||
|
431
|
-
// const handleChange = () => {}
|
432
|
-
// const handleChange = function() {}
|
433
860
|
(fnNode.type === 'VariableDeclarator' &&
|
434
861
|
childScopeBlock.parent === fnNode)
|
435
862
|
) {
|
436
|
-
// Found it!
|
437
863
|
fnScope = childScope;
|
438
864
|
break;
|
439
865
|
}
|
@@ -441,8 +867,6 @@ module.exports = {
|
|
441
867
|
if (fnScope == null) {
|
442
868
|
return false;
|
443
869
|
}
|
444
|
-
// Does this function capture any values
|
445
|
-
// that are in pure scopes (aka render)?
|
446
870
|
for (i = 0; i < fnScope.through.length; i++) {
|
447
871
|
const ref = fnScope.through[i];
|
448
872
|
if (ref.resolved == null) {
|
@@ -450,24 +874,14 @@ module.exports = {
|
|
450
874
|
}
|
451
875
|
if (
|
452
876
|
pureScopes.has(ref.resolved.scope) &&
|
453
|
-
// Stable values are fine though,
|
454
|
-
// although we won't check functions deeper.
|
455
877
|
!memoizedIsStableKnownHookValue(ref.resolved)
|
456
878
|
) {
|
457
879
|
return false;
|
458
880
|
}
|
459
881
|
}
|
460
|
-
// If we got here, this function doesn't capture anything
|
461
|
-
// from render--or everything it captures is known stable.
|
462
882
|
return true;
|
463
883
|
}
|
464
|
-
|
465
|
-
// These are usually mistaken. Collect them.
|
466
884
|
const currentRefsInEffectCleanup = new Map();
|
467
|
-
|
468
|
-
// Is this reference inside a cleanup function for this effect node?
|
469
|
-
// We can check by traversing scopes upwards from the reference, and checking
|
470
|
-
// if the last "return () => " we encounter is located directly inside the effect.
|
471
885
|
function isInsideEffectCleanup(reference) {
|
472
886
|
let curScope = reference.from;
|
473
887
|
let isInReturnedFunction = false;
|
@@ -481,26 +895,17 @@ module.exports = {
|
|
481
895
|
}
|
482
896
|
return isInReturnedFunction;
|
483
897
|
}
|
484
|
-
|
485
|
-
// Get dependencies from all our resolved references in pure scopes.
|
486
|
-
// Key is dependency string, value is whether it's stable.
|
487
898
|
const dependencies = new Map();
|
488
899
|
const optionalChains = new Map();
|
489
900
|
gatherDependenciesRecursively(scope);
|
490
|
-
|
491
901
|
function gatherDependenciesRecursively(currentScope) {
|
492
902
|
for (const reference of currentScope.references) {
|
493
|
-
// If this reference is not resolved or it is not declared in a pure
|
494
|
-
// scope then we don't care about this reference.
|
495
903
|
if (!reference.resolved) {
|
496
904
|
continue;
|
497
905
|
}
|
498
906
|
if (!pureScopes.has(reference.resolved.scope)) {
|
499
907
|
continue;
|
500
908
|
}
|
501
|
-
|
502
|
-
// Narrow the scope of a dependency if it is, say, a member expression.
|
503
|
-
// Then normalize the narrowed dependency.
|
504
909
|
const referenceNode = fastFindReferenceWithParent(
|
505
910
|
node,
|
506
911
|
reference.identifier
|
@@ -510,19 +915,14 @@ module.exports = {
|
|
510
915
|
dependencyNode,
|
511
916
|
optionalChains
|
512
917
|
);
|
513
|
-
|
514
|
-
// Accessing ref.current inside effect cleanup is bad.
|
515
918
|
if (
|
516
|
-
// We're in an effect...
|
517
919
|
isEffect &&
|
518
|
-
// ... and this look like accessing .current...
|
519
920
|
dependencyNode.type === 'Identifier' &&
|
520
921
|
(dependencyNode.parent.type === 'MemberExpression' ||
|
521
922
|
dependencyNode.parent.type === 'OptionalMemberExpression') &&
|
522
923
|
!dependencyNode.parent.computed &&
|
523
924
|
dependencyNode.parent.property.type === 'Identifier' &&
|
524
925
|
dependencyNode.parent.property.name === 'current' &&
|
525
|
-
// ...in a cleanup function or below...
|
526
926
|
isInsideEffectCleanup(reference)
|
527
927
|
) {
|
528
928
|
currentRefsInEffectCleanup.set(dependency, {
|
@@ -530,29 +930,22 @@ module.exports = {
|
|
530
930
|
dependencyNode
|
531
931
|
});
|
532
932
|
}
|
533
|
-
|
534
933
|
if (
|
535
934
|
dependencyNode.parent.type === 'TSTypeQuery' ||
|
536
935
|
dependencyNode.parent.type === 'TSTypeReference'
|
537
936
|
) {
|
538
937
|
continue;
|
539
938
|
}
|
540
|
-
|
541
939
|
const def = reference.resolved.defs[0];
|
542
940
|
if (def == null) {
|
543
941
|
continue;
|
544
942
|
}
|
545
|
-
// Ignore references to the function itself as it's not defined yet.
|
546
943
|
if (def.node != null && def.node.init === node.parent) {
|
547
944
|
continue;
|
548
945
|
}
|
549
|
-
// Ignore Flow type parameters
|
550
946
|
if (def.type === 'TypeParameter') {
|
551
947
|
continue;
|
552
948
|
}
|
553
|
-
|
554
|
-
// Add the dependency to a map so we can make sure it is referenced
|
555
|
-
// again in our dependencies array. Remember whether it's stable.
|
556
949
|
if (!dependencies.has(dependency)) {
|
557
950
|
const resolved = reference.resolved;
|
558
951
|
const isStable =
|
@@ -566,30 +959,22 @@ module.exports = {
|
|
566
959
|
dependencies.get(dependency).references.push(reference);
|
567
960
|
}
|
568
961
|
}
|
569
|
-
|
570
962
|
for (const childScope of currentScope.childScopes) {
|
571
963
|
gatherDependenciesRecursively(childScope);
|
572
964
|
}
|
573
965
|
}
|
574
|
-
|
575
|
-
// Warn about accessing .current in cleanup effects.
|
576
966
|
currentRefsInEffectCleanup.forEach(
|
577
967
|
({ reference, dependencyNode }, dependency) => {
|
578
968
|
const references = reference.resolved.references;
|
579
|
-
// Is React managing this ref or us?
|
580
|
-
// Let's see if we can find a .current assignment.
|
581
969
|
let foundCurrentAssignment = false;
|
582
970
|
for (const { identifier } of references) {
|
583
971
|
const { parent } = identifier;
|
584
972
|
if (
|
585
973
|
parent != null &&
|
586
|
-
// ref.current
|
587
|
-
// Note: no need to handle OptionalMemberExpression because it can't be LHS.
|
588
974
|
parent.type === 'MemberExpression' &&
|
589
975
|
!parent.computed &&
|
590
976
|
parent.property.type === 'Identifier' &&
|
591
977
|
parent.property.name === 'current' &&
|
592
|
-
// ref.current = <something>
|
593
978
|
parent.parent.type === 'AssignmentExpression' &&
|
594
979
|
parent.parent.left === parent
|
595
980
|
) {
|
@@ -597,7 +982,6 @@ module.exports = {
|
|
597
982
|
break;
|
598
983
|
}
|
599
984
|
}
|
600
|
-
// We only want to warn about React-managed refs.
|
601
985
|
if (foundCurrentAssignment) {
|
602
986
|
return;
|
603
987
|
}
|
@@ -612,9 +996,6 @@ module.exports = {
|
|
612
996
|
});
|
613
997
|
}
|
614
998
|
);
|
615
|
-
|
616
|
-
// Warn about assigning to variables in the outer scope.
|
617
|
-
// Those are usually bugs.
|
618
999
|
const staleAssignments = new Set();
|
619
1000
|
function reportStaleAssignment(writeExpr, key) {
|
620
1001
|
if (staleAssignments.has(key)) {
|
@@ -632,8 +1013,6 @@ module.exports = {
|
|
632
1013
|
`${context.getSource(reactiveHook)}.`
|
633
1014
|
});
|
634
1015
|
}
|
635
|
-
|
636
|
-
// Remember which deps are stable and report bad usage first.
|
637
1016
|
const stableDependencies = new Set();
|
638
1017
|
dependencies.forEach(({ isStable, references }, key) => {
|
639
1018
|
if (isStable) {
|
@@ -645,15 +1024,10 @@ module.exports = {
|
|
645
1024
|
}
|
646
1025
|
});
|
647
1026
|
});
|
648
|
-
|
649
1027
|
if (staleAssignments.size > 0) {
|
650
|
-
// The intent isn't clear so we'll wait until you fix those first.
|
651
1028
|
return;
|
652
1029
|
}
|
653
|
-
|
654
1030
|
if (!declaredDependenciesNode) {
|
655
|
-
// Check if there are any top-level setState() calls.
|
656
|
-
// Those tend to lead to infinite loops.
|
657
1031
|
let setStateInsideEffectWithoutDeps = null;
|
658
1032
|
dependencies.forEach(({ references }, key) => {
|
659
1033
|
if (setStateInsideEffectWithoutDeps) {
|
@@ -663,20 +1037,17 @@ module.exports = {
|
|
663
1037
|
if (setStateInsideEffectWithoutDeps) {
|
664
1038
|
return;
|
665
1039
|
}
|
666
|
-
|
667
1040
|
const id = reference.identifier;
|
668
1041
|
const isSetState = setStateCallSites.has(id);
|
669
1042
|
if (!isSetState) {
|
670
1043
|
return;
|
671
1044
|
}
|
672
|
-
|
673
1045
|
let fnScope = reference.from;
|
674
1046
|
while (fnScope.type !== 'function') {
|
675
1047
|
fnScope = fnScope.upper;
|
676
1048
|
}
|
677
1049
|
const isDirectlyInsideEffect = fnScope.block === node;
|
678
1050
|
if (isDirectlyInsideEffect) {
|
679
|
-
// TODO: we could potentially ignore early returns.
|
680
1051
|
setStateInsideEffectWithoutDeps = key;
|
681
1052
|
}
|
682
1053
|
});
|
@@ -714,13 +1085,9 @@ module.exports = {
|
|
714
1085
|
}
|
715
1086
|
return;
|
716
1087
|
}
|
717
|
-
|
718
1088
|
const declaredDependencies = [];
|
719
1089
|
const externalDependencies = new Set();
|
720
1090
|
if (declaredDependenciesNode.type !== 'ArrayExpression') {
|
721
|
-
// If the declared dependencies are not an array expression then we
|
722
|
-
// can't verify that the user provided the correct dependencies. Tell
|
723
|
-
// the user this in an error.
|
724
1091
|
reportProblem({
|
725
1092
|
node: declaredDependenciesNode,
|
726
1093
|
message:
|
@@ -731,11 +1098,9 @@ module.exports = {
|
|
731
1098
|
});
|
732
1099
|
} else {
|
733
1100
|
declaredDependenciesNode.elements.forEach(declaredDependencyNode => {
|
734
|
-
// Skip elided elements.
|
735
1101
|
if (declaredDependencyNode == null) {
|
736
1102
|
return;
|
737
1103
|
}
|
738
|
-
// If we see a spread element then add a special warning.
|
739
1104
|
if (declaredDependencyNode.type === 'SpreadElement') {
|
740
1105
|
reportProblem({
|
741
1106
|
node: declaredDependencyNode,
|
@@ -747,8 +1112,6 @@ module.exports = {
|
|
747
1112
|
});
|
748
1113
|
return;
|
749
1114
|
}
|
750
|
-
// Try to normalize the declared dependency. If we can't then an error
|
751
|
-
// will be thrown. We will catch that error and report an error.
|
752
1115
|
let declaredDependency;
|
753
1116
|
try {
|
754
1117
|
declaredDependency = analyzePropertyChain(
|
@@ -783,12 +1146,10 @@ module.exports = {
|
|
783
1146
|
'Extract it to a separate variable so it can be statically checked.'
|
784
1147
|
});
|
785
1148
|
}
|
786
|
-
|
787
1149
|
return;
|
788
1150
|
}
|
789
1151
|
throw err;
|
790
1152
|
}
|
791
|
-
|
792
1153
|
let maybeID = declaredDependencyNode;
|
793
1154
|
while (
|
794
1155
|
maybeID.type === 'MemberExpression' ||
|
@@ -800,19 +1161,15 @@ module.exports = {
|
|
800
1161
|
const isDeclaredInComponent = !componentScope.through.some(
|
801
1162
|
ref => ref.identifier === maybeID
|
802
1163
|
);
|
803
|
-
|
804
|
-
// Add the dependency to our declared dependency map.
|
805
1164
|
declaredDependencies.push({
|
806
1165
|
key: declaredDependency,
|
807
1166
|
node: declaredDependencyNode
|
808
1167
|
});
|
809
|
-
|
810
1168
|
if (!isDeclaredInComponent) {
|
811
1169
|
externalDependencies.add(declaredDependency);
|
812
1170
|
}
|
813
1171
|
});
|
814
1172
|
}
|
815
|
-
|
816
1173
|
const {
|
817
1174
|
suggestedDependencies,
|
818
1175
|
unnecessaryDependencies,
|
@@ -825,17 +1182,12 @@ module.exports = {
|
|
825
1182
|
externalDependencies,
|
826
1183
|
isEffect
|
827
1184
|
});
|
828
|
-
|
829
1185
|
let suggestedDeps = suggestedDependencies;
|
830
|
-
|
831
1186
|
const problemCount =
|
832
1187
|
duplicateDependencies.size +
|
833
1188
|
missingDependencies.size +
|
834
1189
|
unnecessaryDependencies.size;
|
835
|
-
|
836
1190
|
if (problemCount === 0) {
|
837
|
-
// If nothing else to report, check if some dependencies would
|
838
|
-
// invalidate on every render.
|
839
1191
|
const constructions = scanForConstructions({
|
840
1192
|
declaredDependencies,
|
841
1193
|
declaredDependenciesNode,
|
@@ -846,35 +1198,24 @@ module.exports = {
|
|
846
1198
|
({ construction, isUsedOutsideOfHook, depType }) => {
|
847
1199
|
const wrapperHook =
|
848
1200
|
depType === 'function' ? 'useCallback' : 'useMemo';
|
849
|
-
|
850
1201
|
const constructionType =
|
851
1202
|
depType === 'function' ? 'definition' : 'initialization';
|
852
|
-
|
853
1203
|
const defaultAdvice = `wrap the ${constructionType} of '${construction.name.name}' in its own ${wrapperHook}() Hook.`;
|
854
|
-
|
855
1204
|
const advice = isUsedOutsideOfHook
|
856
1205
|
? `To fix this, ${defaultAdvice}`
|
857
1206
|
: `Move it inside the ${reactiveHookName} callback. Alternatively, ${defaultAdvice}`;
|
858
|
-
|
859
1207
|
const causation =
|
860
1208
|
depType === 'conditional' || depType === 'logical expression'
|
861
1209
|
? 'could make'
|
862
1210
|
: 'makes';
|
863
|
-
|
864
1211
|
const message =
|
865
1212
|
`The '${construction.name.name}' ${depType} ${causation} the dependencies of ` +
|
866
1213
|
`${reactiveHookName} Hook (at line ${declaredDependenciesNode.loc.start.line}) ` +
|
867
1214
|
`change on every render. ${advice}`;
|
868
|
-
|
869
1215
|
let suggest;
|
870
|
-
// Only handle the simple case of variable assignments.
|
871
|
-
// Wrapping function declarations can mess up hoisting.
|
872
1216
|
if (
|
873
1217
|
isUsedOutsideOfHook &&
|
874
1218
|
construction.type === 'Variable' &&
|
875
|
-
// Objects may be mutated after construction, which would make this
|
876
|
-
// fix unsafe. Functions _probably_ won't be mutated, so we'll
|
877
|
-
// allow this fix for them.
|
878
1219
|
depType === 'function'
|
879
1220
|
) {
|
880
1221
|
suggest = [
|
@@ -886,22 +1227,14 @@ module.exports = {
|
|
886
1227
|
? [`useMemo(() => { return `, '; })']
|
887
1228
|
: ['useCallback(', ')'];
|
888
1229
|
return [
|
889
|
-
// TODO: also add an import?
|
890
1230
|
fixer.insertTextBefore(construction.node.init, before),
|
891
|
-
// TODO: ideally we'd gather deps here but it would require
|
892
|
-
// restructuring the rule code. This will cause a new lint
|
893
|
-
// error to appear immediately for useCallback. Note we're
|
894
|
-
// not adding [] because would that changes semantics.
|
895
1231
|
fixer.insertTextAfter(construction.node.init, after)
|
896
1232
|
];
|
897
1233
|
}
|
898
1234
|
}
|
899
1235
|
];
|
900
1236
|
}
|
901
|
-
// TODO: What if the function needs to change on every render anyway?
|
902
|
-
// Should we suggest removing effect deps as an appropriate fix too?
|
903
1237
|
reportProblem({
|
904
|
-
// TODO: Why not report this at the dependency site?
|
905
1238
|
node: construction.node,
|
906
1239
|
message,
|
907
1240
|
suggest
|
@@ -910,24 +1243,15 @@ module.exports = {
|
|
910
1243
|
);
|
911
1244
|
return;
|
912
1245
|
}
|
913
|
-
|
914
|
-
// If we're going to report a missing dependency,
|
915
|
-
// we might as well recalculate the list ignoring
|
916
|
-
// the currently specified deps. This can result
|
917
|
-
// in some extra deduplication. We can't do this
|
918
|
-
// for effects though because those have legit
|
919
|
-
// use cases for over-specifying deps.
|
920
1246
|
if (!isEffect && missingDependencies.size > 0) {
|
921
1247
|
suggestedDeps = collectRecommendations({
|
922
1248
|
dependencies,
|
923
|
-
declaredDependencies: [],
|
1249
|
+
declaredDependencies: [],
|
924
1250
|
stableDependencies,
|
925
1251
|
externalDependencies,
|
926
1252
|
isEffect
|
927
1253
|
}).suggestedDependencies;
|
928
1254
|
}
|
929
|
-
|
930
|
-
// Alphabetize the suggestions, but only if deps were already alphabetized.
|
931
1255
|
function areDeclaredDepsAlphabetized() {
|
932
1256
|
if (declaredDependencies.length === 0) {
|
933
1257
|
return true;
|
@@ -939,11 +1263,6 @@ module.exports = {
|
|
939
1263
|
if (areDeclaredDepsAlphabetized()) {
|
940
1264
|
suggestedDeps.sort();
|
941
1265
|
}
|
942
|
-
|
943
|
-
// Most of our algorithm deals with dependency paths with optional chaining stripped.
|
944
|
-
// This function is the last step before printing a dependency, so now is a good time to
|
945
|
-
// check whether any members in our path are always used as optional-only. In that case,
|
946
|
-
// we will use ?. instead of . to concatenate those parts of the path.
|
947
1266
|
function formatDependency(path) {
|
948
1267
|
const members = path.split('.');
|
949
1268
|
let finalPath = '';
|
@@ -957,7 +1276,6 @@ module.exports = {
|
|
957
1276
|
}
|
958
1277
|
return finalPath;
|
959
1278
|
}
|
960
|
-
|
961
1279
|
function getWarningMessage(deps, singlePrefix, label, fixVerb) {
|
962
1280
|
if (deps.size === 0) {
|
963
1281
|
return null;
|
@@ -976,7 +1294,6 @@ module.exports = {
|
|
976
1294
|
} or remove the dependency array.`
|
977
1295
|
);
|
978
1296
|
}
|
979
|
-
|
980
1297
|
let extraWarning = '';
|
981
1298
|
if (unnecessaryDependencies.size > 0) {
|
982
1299
|
let badRef = null;
|
@@ -994,8 +1311,6 @@ module.exports = {
|
|
994
1311
|
"because mutating them doesn't re-render the component.";
|
995
1312
|
} else if (externalDependencies.size > 0) {
|
996
1313
|
const dep = [...externalDependencies][0];
|
997
|
-
// Don't show this warning for things that likely just got moved *inside* the callback
|
998
|
-
// because in that case they're clearly not referring to globals.
|
999
1314
|
if (!scope.set.has(dep)) {
|
1000
1315
|
extraWarning =
|
1001
1316
|
` Outer scope values like '${dep}' aren't valid dependencies ` +
|
@@ -1003,10 +1318,6 @@ module.exports = {
|
|
1003
1318
|
}
|
1004
1319
|
}
|
1005
1320
|
}
|
1006
|
-
|
1007
|
-
// `props.foo()` marks `props` as a dependency because it has
|
1008
|
-
// a `this` value. This warning can be confusing.
|
1009
|
-
// So if we're going to show it, append a clarification.
|
1010
1321
|
if (!extraWarning && missingDependencies.has('props')) {
|
1011
1322
|
const propDep = dependencies.get('props');
|
1012
1323
|
if (propDep == null) {
|
@@ -1047,27 +1358,21 @@ module.exports = {
|
|
1047
1358
|
`inside ${context.getSource(reactiveHook)}.`;
|
1048
1359
|
}
|
1049
1360
|
}
|
1050
|
-
|
1051
1361
|
if (!extraWarning && missingDependencies.size > 0) {
|
1052
|
-
// See if the user is trying to avoid specifying a callable prop.
|
1053
|
-
// This usually means they're unaware of useCallback.
|
1054
1362
|
let missingCallbackDep = null;
|
1055
1363
|
missingDependencies.forEach(missingDep => {
|
1056
1364
|
if (missingCallbackDep) {
|
1057
1365
|
return;
|
1058
1366
|
}
|
1059
|
-
// Is this a variable from top scope?
|
1060
1367
|
const topScopeRef = componentScope.set.get(missingDep);
|
1061
1368
|
const usedDep = dependencies.get(missingDep);
|
1062
1369
|
if (usedDep.references[0].resolved !== topScopeRef) {
|
1063
1370
|
return;
|
1064
1371
|
}
|
1065
|
-
// Is this a destructured prop?
|
1066
1372
|
const def = topScopeRef.defs[0];
|
1067
1373
|
if (def == null || def.name == null || def.type !== 'Parameter') {
|
1068
1374
|
return;
|
1069
1375
|
}
|
1070
|
-
// Was it called in at least one case? Then it's a function.
|
1071
1376
|
let isFunctionCall = false;
|
1072
1377
|
let id;
|
1073
1378
|
for (let i = 0; i < usedDep.references.length; i++) {
|
@@ -1086,9 +1391,6 @@ module.exports = {
|
|
1086
1391
|
if (!isFunctionCall) {
|
1087
1392
|
return;
|
1088
1393
|
}
|
1089
|
-
// If it's missing (i.e. in component scope) *and* it's a parameter
|
1090
|
-
// then it is definitely coming from props destructuring.
|
1091
|
-
// (It could also be props itself but we wouldn't be calling it then.)
|
1092
1394
|
missingCallbackDep = missingDep;
|
1093
1395
|
});
|
1094
1396
|
if (missingCallbackDep != null) {
|
@@ -1098,7 +1400,6 @@ module.exports = {
|
|
1098
1400
|
`and wrap that definition in useCallback.`;
|
1099
1401
|
}
|
1100
1402
|
}
|
1101
|
-
|
1102
1403
|
if (!extraWarning && missingDependencies.size > 0) {
|
1103
1404
|
let setStateRecommendation = null;
|
1104
1405
|
missingDependencies.forEach(missingDep => {
|
@@ -1112,7 +1413,6 @@ module.exports = {
|
|
1112
1413
|
for (const reference of references) {
|
1113
1414
|
id = reference.identifier;
|
1114
1415
|
maybeCall = id.parent;
|
1115
|
-
// Try to see if we have setState(someExpr(missingDep)).
|
1116
1416
|
while (maybeCall != null && maybeCall !== componentScope.block) {
|
1117
1417
|
if (maybeCall.type === 'CallExpression') {
|
1118
1418
|
const correspondingStateVariable = setStateCallSites.get(
|
@@ -1120,14 +1420,12 @@ module.exports = {
|
|
1120
1420
|
);
|
1121
1421
|
if (correspondingStateVariable != null) {
|
1122
1422
|
if (correspondingStateVariable.name === missingDep) {
|
1123
|
-
// setCount(count + 1)
|
1124
1423
|
setStateRecommendation = {
|
1125
1424
|
missingDep,
|
1126
1425
|
setter: maybeCall.callee.name,
|
1127
1426
|
form: 'updater'
|
1128
1427
|
};
|
1129
1428
|
} else if (stateVariables.has(id)) {
|
1130
|
-
// setCount(count + increment)
|
1131
1429
|
setStateRecommendation = {
|
1132
1430
|
missingDep,
|
1133
1431
|
setter: maybeCall.callee.name,
|
@@ -1136,9 +1434,6 @@ module.exports = {
|
|
1136
1434
|
} else {
|
1137
1435
|
const resolved = reference.resolved;
|
1138
1436
|
if (resolved != null) {
|
1139
|
-
// If it's a parameter *and* a missing dep,
|
1140
|
-
// it must be a prop or something inside a prop.
|
1141
|
-
// Therefore, recommend an inline reducer.
|
1142
1437
|
const def = resolved.defs[0];
|
1143
1438
|
if (def != null && def.type === 'Parameter') {
|
1144
1439
|
setStateRecommendation = {
|
@@ -1189,12 +1484,10 @@ module.exports = {
|
|
1189
1484
|
}
|
1190
1485
|
}
|
1191
1486
|
}
|
1192
|
-
|
1193
1487
|
reportProblem({
|
1194
1488
|
node: declaredDependenciesNode,
|
1195
1489
|
message:
|
1196
1490
|
`React Hook ${context.getSource(reactiveHook)} has ` +
|
1197
|
-
// To avoid a long message, show the next actionable item.
|
1198
1491
|
(getWarningMessage(missingDependencies, 'a', 'missing', 'include') ||
|
1199
1492
|
getWarningMessage(
|
1200
1493
|
unnecessaryDependencies,
|
@@ -1215,7 +1508,6 @@ module.exports = {
|
|
1215
1508
|
.map(element => formatDependency(element))
|
1216
1509
|
.join(', ')}]`,
|
1217
1510
|
fix(fixer) {
|
1218
|
-
// TODO: consider preserving the comments or formatting?
|
1219
1511
|
return fixer.replaceText(
|
1220
1512
|
declaredDependenciesNode,
|
1221
1513
|
`[${suggestedDeps.map(element => formatDependency(element)).join(', ')}]`
|
@@ -1225,11 +1517,9 @@ module.exports = {
|
|
1225
1517
|
]
|
1226
1518
|
});
|
1227
1519
|
}
|
1228
|
-
|
1229
1520
|
function visitCallExpression(node) {
|
1230
1521
|
const callbackIndex = getReactiveHookCallbackIndex(node.callee, options);
|
1231
1522
|
if (callbackIndex === -1) {
|
1232
|
-
// Not a React Hook call that needs deps.
|
1233
1523
|
return;
|
1234
1524
|
}
|
1235
1525
|
const callback = node.arguments[callbackIndex];
|
@@ -1237,10 +1527,6 @@ module.exports = {
|
|
1237
1527
|
const reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
|
1238
1528
|
const declaredDependenciesNode = node.arguments[callbackIndex + 1];
|
1239
1529
|
const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName);
|
1240
|
-
|
1241
|
-
// Check whether a callback is supplied. If there is no callback supplied
|
1242
|
-
// then the hook will not work and React will throw a TypeError.
|
1243
|
-
// So no need to check for dependency inclusion.
|
1244
1530
|
if (!callback) {
|
1245
1531
|
reportProblem({
|
1246
1532
|
node: reactiveHook,
|
@@ -1250,17 +1536,11 @@ module.exports = {
|
|
1250
1536
|
});
|
1251
1537
|
return;
|
1252
1538
|
}
|
1253
|
-
|
1254
|
-
// Check the declared dependencies for this reactive hook. If there is no
|
1255
|
-
// second argument then the reactive callback will re-run on every render.
|
1256
|
-
// So no need to check for dependency inclusion.
|
1257
1539
|
if (!declaredDependenciesNode && !isEffect) {
|
1258
|
-
// These are only used for optimization.
|
1259
1540
|
if (
|
1260
1541
|
reactiveHookName === 'useMemo' ||
|
1261
1542
|
reactiveHookName === 'useCallback'
|
1262
1543
|
) {
|
1263
|
-
// TODO: Can this have a suggestion?
|
1264
1544
|
reportProblem({
|
1265
1545
|
node: reactiveHook,
|
1266
1546
|
message:
|
@@ -1271,7 +1551,6 @@ module.exports = {
|
|
1271
1551
|
}
|
1272
1552
|
return;
|
1273
1553
|
}
|
1274
|
-
|
1275
1554
|
switch (callback.type) {
|
1276
1555
|
case 'FunctionExpression':
|
1277
1556
|
case 'ArrowFunctionExpression':
|
@@ -1282,44 +1561,32 @@ module.exports = {
|
|
1282
1561
|
reactiveHookName,
|
1283
1562
|
isEffect
|
1284
1563
|
);
|
1285
|
-
return;
|
1564
|
+
return;
|
1286
1565
|
case 'Identifier':
|
1287
1566
|
if (!declaredDependenciesNode) {
|
1288
|
-
|
1289
|
-
return; // Handled
|
1567
|
+
return;
|
1290
1568
|
}
|
1291
|
-
// The function passed as a callback is not written inline.
|
1292
|
-
// But perhaps it's in the dependencies array?
|
1293
1569
|
if (
|
1294
1570
|
declaredDependenciesNode.elements &&
|
1295
1571
|
declaredDependenciesNode.elements.some(
|
1296
1572
|
el => el && el.type === 'Identifier' && el.name === callback.name
|
1297
1573
|
)
|
1298
1574
|
) {
|
1299
|
-
|
1300
|
-
// this is valid regardless.
|
1301
|
-
return; // Handled
|
1575
|
+
return;
|
1302
1576
|
}
|
1303
|
-
// We'll do our best effort to find it, complain otherwise.
|
1304
1577
|
const variable = context.getScope().set.get(callback.name);
|
1305
1578
|
if (variable == null || variable.defs == null) {
|
1306
|
-
|
1307
|
-
return; // Handled
|
1579
|
+
return;
|
1308
1580
|
}
|
1309
|
-
// The function passed as a callback is not written inline.
|
1310
|
-
// But it's defined somewhere in the render scope.
|
1311
|
-
// We'll do our best effort to find and check it, complain otherwise.
|
1312
1581
|
const def = variable.defs[0];
|
1313
1582
|
if (!def || !def.node) {
|
1314
|
-
break;
|
1583
|
+
break;
|
1315
1584
|
}
|
1316
1585
|
if (def.type !== 'Variable' && def.type !== 'FunctionName') {
|
1317
|
-
|
1318
|
-
break; // Unhandled
|
1586
|
+
break;
|
1319
1587
|
}
|
1320
1588
|
switch (def.node.type) {
|
1321
1589
|
case 'FunctionDeclaration':
|
1322
|
-
// useEffect(() => { ... }, []);
|
1323
1590
|
visitFunctionWithDependencies(
|
1324
1591
|
def.node,
|
1325
1592
|
declaredDependenciesNode,
|
@@ -1327,18 +1594,15 @@ module.exports = {
|
|
1327
1594
|
reactiveHookName,
|
1328
1595
|
isEffect
|
1329
1596
|
);
|
1330
|
-
return;
|
1597
|
+
return;
|
1331
1598
|
case 'VariableDeclarator':
|
1332
1599
|
const init = def.node.init;
|
1333
1600
|
if (!init) {
|
1334
|
-
break;
|
1601
|
+
break;
|
1335
1602
|
}
|
1336
1603
|
switch (init.type) {
|
1337
|
-
// const effectBody = () => {...};
|
1338
|
-
// useEffect(effectBody, []);
|
1339
1604
|
case 'ArrowFunctionExpression':
|
1340
1605
|
case 'FunctionExpression':
|
1341
|
-
// We can inspect this function as if it were inline.
|
1342
1606
|
visitFunctionWithDependencies(
|
1343
1607
|
init,
|
1344
1608
|
declaredDependenciesNode,
|
@@ -1346,23 +1610,20 @@ module.exports = {
|
|
1346
1610
|
reactiveHookName,
|
1347
1611
|
isEffect
|
1348
1612
|
);
|
1349
|
-
return;
|
1613
|
+
return;
|
1350
1614
|
}
|
1351
|
-
break;
|
1615
|
+
break;
|
1352
1616
|
}
|
1353
|
-
break;
|
1617
|
+
break;
|
1354
1618
|
default:
|
1355
|
-
// useEffect(generateEffectBody(), []);
|
1356
1619
|
reportProblem({
|
1357
1620
|
node: reactiveHook,
|
1358
1621
|
message:
|
1359
1622
|
`React Hook ${reactiveHookName} received a function whose dependencies ` +
|
1360
1623
|
`are unknown. Pass an inline function instead.`
|
1361
1624
|
});
|
1362
|
-
return;
|
1625
|
+
return;
|
1363
1626
|
}
|
1364
|
-
|
1365
|
-
// Something unusual. Fall back to suggesting to add the body itself as a dep.
|
1366
1627
|
reportProblem({
|
1367
1628
|
node: reactiveHook,
|
1368
1629
|
message:
|
@@ -1381,14 +1642,11 @@ module.exports = {
|
|
1381
1642
|
]
|
1382
1643
|
});
|
1383
1644
|
}
|
1384
|
-
|
1385
1645
|
return {
|
1386
1646
|
CallExpression: visitCallExpression
|
1387
1647
|
};
|
1388
1648
|
}
|
1389
1649
|
};
|
1390
|
-
|
1391
|
-
// The meat of the logic.
|
1392
1650
|
function collectRecommendations({
|
1393
1651
|
dependencies,
|
1394
1652
|
declaredDependencies,
|
@@ -1396,27 +1654,15 @@ function collectRecommendations({
|
|
1396
1654
|
externalDependencies,
|
1397
1655
|
isEffect
|
1398
1656
|
}) {
|
1399
|
-
// Our primary data structure.
|
1400
|
-
// It is a logical representation of property chains:
|
1401
|
-
// `props` -> `props.foo` -> `props.foo.bar` -> `props.foo.bar.baz`
|
1402
|
-
// -> `props.lol`
|
1403
|
-
// -> `props.huh` -> `props.huh.okay`
|
1404
|
-
// -> `props.wow`
|
1405
|
-
// We'll use it to mark nodes that are *used* by the programmer,
|
1406
|
-
// and the nodes that were *declared* as deps. Then we will
|
1407
|
-
// traverse it to learn which deps are missing or unnecessary.
|
1408
1657
|
const depTree = createDepTree();
|
1409
1658
|
function createDepTree() {
|
1410
1659
|
return {
|
1411
|
-
isUsed: false,
|
1412
|
-
isSatisfiedRecursively: false,
|
1413
|
-
isSubtreeUsed: false,
|
1414
|
-
children: new Map()
|
1660
|
+
isUsed: false,
|
1661
|
+
isSatisfiedRecursively: false,
|
1662
|
+
isSubtreeUsed: false,
|
1663
|
+
children: new Map()
|
1415
1664
|
};
|
1416
1665
|
}
|
1417
|
-
|
1418
|
-
// Mark all required nodes first.
|
1419
|
-
// Imagine exclamation marks next to each used deep property.
|
1420
1666
|
dependencies.forEach((_, key) => {
|
1421
1667
|
const node = getOrCreateNodeByPath(depTree, key);
|
1422
1668
|
node.isUsed = true;
|
@@ -1424,9 +1670,6 @@ function collectRecommendations({
|
|
1424
1670
|
parent.isSubtreeUsed = true;
|
1425
1671
|
});
|
1426
1672
|
});
|
1427
|
-
|
1428
|
-
// Mark all satisfied nodes.
|
1429
|
-
// Imagine checkmarks next to each declared dependency.
|
1430
1673
|
declaredDependencies.forEach(({ key }) => {
|
1431
1674
|
const node = getOrCreateNodeByPath(depTree, key);
|
1432
1675
|
node.isSatisfiedRecursively = true;
|
@@ -1435,8 +1678,6 @@ function collectRecommendations({
|
|
1435
1678
|
const node = getOrCreateNodeByPath(depTree, key);
|
1436
1679
|
node.isSatisfiedRecursively = true;
|
1437
1680
|
});
|
1438
|
-
|
1439
|
-
// Tree manipulation helpers.
|
1440
1681
|
function getOrCreateNodeByPath(rootNode, path) {
|
1441
1682
|
const keys = path.split('.');
|
1442
1683
|
let node = rootNode;
|
@@ -1462,8 +1703,6 @@ function collectRecommendations({
|
|
1462
1703
|
node = child;
|
1463
1704
|
}
|
1464
1705
|
}
|
1465
|
-
|
1466
|
-
// Now we can learn which dependencies are missing or necessary.
|
1467
1706
|
const missingDependencies = new Set();
|
1468
1707
|
const satisfyingDependencies = new Set();
|
1469
1708
|
scanTreeRecursively(
|
@@ -1477,19 +1716,12 @@ function collectRecommendations({
|
|
1477
1716
|
const path = keyToPath(key);
|
1478
1717
|
if (child.isSatisfiedRecursively) {
|
1479
1718
|
if (child.isSubtreeUsed) {
|
1480
|
-
// Remember this dep actually satisfied something.
|
1481
1719
|
satisfyingPaths.add(path);
|
1482
1720
|
}
|
1483
|
-
// It doesn't matter if there's something deeper.
|
1484
|
-
// It would be transitively satisfied since we assume immutability.
|
1485
|
-
// `props.foo` is enough if you read `props.foo.id`.
|
1486
1721
|
return;
|
1487
1722
|
}
|
1488
1723
|
if (child.isUsed) {
|
1489
|
-
// Remember that no declared deps satisfied this node.
|
1490
1724
|
missingPaths.add(path);
|
1491
|
-
// If we got here, nothing in its subtree was satisfied.
|
1492
|
-
// No need to search further.
|
1493
1725
|
return;
|
1494
1726
|
}
|
1495
1727
|
scanTreeRecursively(
|
@@ -1500,19 +1732,14 @@ function collectRecommendations({
|
|
1500
1732
|
);
|
1501
1733
|
});
|
1502
1734
|
}
|
1503
|
-
|
1504
|
-
// Collect suggestions in the order they were originally specified.
|
1505
1735
|
const suggestedDependencies = [];
|
1506
1736
|
const unnecessaryDependencies = new Set();
|
1507
1737
|
const duplicateDependencies = new Set();
|
1508
1738
|
declaredDependencies.forEach(({ key }) => {
|
1509
|
-
// Does this declared dep satisfy a real need?
|
1510
1739
|
if (satisfyingDependencies.has(key)) {
|
1511
1740
|
if (!suggestedDependencies.includes(key)) {
|
1512
|
-
// Good one.
|
1513
1741
|
suggestedDependencies.push(key);
|
1514
1742
|
} else {
|
1515
|
-
// Duplicate.
|
1516
1743
|
duplicateDependencies.add(key);
|
1517
1744
|
}
|
1518
1745
|
} else if (
|
@@ -1520,24 +1747,16 @@ function collectRecommendations({
|
|
1520
1747
|
!key.endsWith('.current') &&
|
1521
1748
|
!externalDependencies.has(key)
|
1522
1749
|
) {
|
1523
|
-
// Effects are allowed extra "unnecessary" deps.
|
1524
|
-
// Such as resetting scroll when ID changes.
|
1525
|
-
// Consider them legit.
|
1526
|
-
// The exception is ref.current which is always wrong.
|
1527
1750
|
if (!suggestedDependencies.includes(key)) {
|
1528
1751
|
suggestedDependencies.push(key);
|
1529
1752
|
}
|
1530
1753
|
} else {
|
1531
|
-
// It's definitely not needed.
|
1532
1754
|
unnecessaryDependencies.add(key);
|
1533
1755
|
}
|
1534
1756
|
});
|
1535
|
-
|
1536
|
-
// Then add the missing ones at the end.
|
1537
1757
|
missingDependencies.forEach(key => {
|
1538
1758
|
suggestedDependencies.push(key);
|
1539
1759
|
});
|
1540
|
-
|
1541
1760
|
return {
|
1542
1761
|
suggestedDependencies,
|
1543
1762
|
unnecessaryDependencies,
|
@@ -1545,9 +1764,6 @@ function collectRecommendations({
|
|
1545
1764
|
missingDependencies
|
1546
1765
|
};
|
1547
1766
|
}
|
1548
|
-
|
1549
|
-
// If the node will result in constructing a referentially unique value, return
|
1550
|
-
// its human readable type name, else return null.
|
1551
1767
|
function getConstructionExpressionType(node) {
|
1552
1768
|
switch (node.type) {
|
1553
1769
|
case 'ObjectExpression':
|
@@ -1598,9 +1814,6 @@ function getConstructionExpressionType(node) {
|
|
1598
1814
|
}
|
1599
1815
|
return null;
|
1600
1816
|
}
|
1601
|
-
|
1602
|
-
// Finds variables declared as dependencies
|
1603
|
-
// that would invalidate on every render.
|
1604
1817
|
function scanForConstructions({
|
1605
1818
|
declaredDependencies,
|
1606
1819
|
declaredDependenciesNode,
|
@@ -1613,20 +1826,14 @@ function scanForConstructions({
|
|
1613
1826
|
if (ref == null) {
|
1614
1827
|
return null;
|
1615
1828
|
}
|
1616
|
-
|
1617
1829
|
const node = ref.defs[0];
|
1618
1830
|
if (node == null) {
|
1619
1831
|
return null;
|
1620
1832
|
}
|
1621
|
-
// const handleChange = function () {}
|
1622
|
-
// const handleChange = () => {}
|
1623
|
-
// const foo = {}
|
1624
|
-
// const foo = []
|
1625
|
-
// etc.
|
1626
1833
|
if (
|
1627
1834
|
node.type === 'Variable' &&
|
1628
1835
|
node.node.type === 'VariableDeclarator' &&
|
1629
|
-
node.node.id.type === 'Identifier' &&
|
1836
|
+
node.node.id.type === 'Identifier' &&
|
1630
1837
|
node.node.init != null
|
1631
1838
|
) {
|
1632
1839
|
const constantExpressionType = getConstructionExpressionType(
|
@@ -1636,32 +1843,26 @@ function scanForConstructions({
|
|
1636
1843
|
return [ref, constantExpressionType];
|
1637
1844
|
}
|
1638
1845
|
}
|
1639
|
-
// function handleChange() {}
|
1640
1846
|
if (
|
1641
1847
|
node.type === 'FunctionName' &&
|
1642
1848
|
node.node.type === 'FunctionDeclaration'
|
1643
1849
|
) {
|
1644
1850
|
return [ref, 'function'];
|
1645
1851
|
}
|
1646
|
-
|
1647
|
-
// class Foo {}
|
1648
1852
|
if (node.type === 'ClassName' && node.node.type === 'ClassDeclaration') {
|
1649
1853
|
return [ref, 'class'];
|
1650
1854
|
}
|
1651
1855
|
return null;
|
1652
1856
|
})
|
1653
1857
|
.filter(Boolean);
|
1654
|
-
|
1655
1858
|
function isUsedOutsideOfHook(ref) {
|
1656
1859
|
let foundWriteExpr = false;
|
1657
1860
|
for (let i = 0; i < ref.references.length; i++) {
|
1658
1861
|
const reference = ref.references[i];
|
1659
1862
|
if (reference.writeExpr) {
|
1660
1863
|
if (foundWriteExpr) {
|
1661
|
-
// Two writes to the same function.
|
1662
1864
|
return true;
|
1663
1865
|
}
|
1664
|
-
// Ignore first write as it's not usage.
|
1665
1866
|
foundWriteExpr = true;
|
1666
1867
|
continue;
|
1667
1868
|
}
|
@@ -1670,8 +1871,7 @@ function scanForConstructions({
|
|
1670
1871
|
currentScope = currentScope.upper;
|
1671
1872
|
}
|
1672
1873
|
if (
|
1673
|
-
currentScope !== scope &&
|
1674
|
-
// It can only be legit if it's the deps array.
|
1874
|
+
currentScope !== scope &&
|
1675
1875
|
!isAncestorNodeOf(declaredDependenciesNode, reference.identifier)
|
1676
1876
|
) {
|
1677
1877
|
return true;
|
@@ -1679,21 +1879,12 @@ function scanForConstructions({
|
|
1679
1879
|
}
|
1680
1880
|
return false;
|
1681
1881
|
}
|
1682
|
-
|
1683
1882
|
return constructions.map(([ref, depType]) => ({
|
1684
1883
|
construction: ref.defs[0],
|
1685
1884
|
depType,
|
1686
1885
|
isUsedOutsideOfHook: isUsedOutsideOfHook(ref)
|
1687
1886
|
}));
|
1688
1887
|
}
|
1689
|
-
|
1690
|
-
/**
|
1691
|
-
* Assuming () means the passed/returned node:
|
1692
|
-
* (props) => (props)
|
1693
|
-
* props.(foo) => (props.foo)
|
1694
|
-
* props.foo.(bar) => (props).foo.bar
|
1695
|
-
* props.foo.bar.(baz) => (props).foo.bar.baz
|
1696
|
-
*/
|
1697
1888
|
function getDependency(node) {
|
1698
1889
|
if (
|
1699
1890
|
(node.parent.type === 'MemberExpression' ||
|
@@ -1710,7 +1901,6 @@ function getDependency(node) {
|
|
1710
1901
|
) {
|
1711
1902
|
return getDependency(node.parent);
|
1712
1903
|
} else if (
|
1713
|
-
// Note: we don't check OptionalMemberExpression because it can't be LHS.
|
1714
1904
|
node.type === 'MemberExpression' &&
|
1715
1905
|
node.parent &&
|
1716
1906
|
node.parent.type === 'AssignmentExpression' &&
|
@@ -1720,40 +1910,21 @@ function getDependency(node) {
|
|
1720
1910
|
}
|
1721
1911
|
return node;
|
1722
1912
|
}
|
1723
|
-
|
1724
|
-
/**
|
1725
|
-
* Mark a node as either optional or required.
|
1726
|
-
* Note: If the node argument is an OptionalMemberExpression, it doesn't necessarily mean it is optional.
|
1727
|
-
* It just means there is an optional member somewhere inside.
|
1728
|
-
* This particular node might still represent a required member, so check .optional field.
|
1729
|
-
*/
|
1730
1913
|
function markNode(node, optionalChains, result) {
|
1731
1914
|
if (optionalChains) {
|
1732
1915
|
if (node.optional) {
|
1733
|
-
// We only want to consider it optional if *all* usages were optional.
|
1734
1916
|
if (!optionalChains.has(result)) {
|
1735
|
-
// Mark as (maybe) optional. If there's a required usage, this will be overridden.
|
1736
1917
|
optionalChains.set(result, true);
|
1737
1918
|
}
|
1738
1919
|
} else {
|
1739
|
-
// Mark as required.
|
1740
1920
|
optionalChains.set(result, false);
|
1741
1921
|
}
|
1742
1922
|
}
|
1743
1923
|
}
|
1744
|
-
|
1745
|
-
/**
|
1746
|
-
* Assuming () means the passed node.
|
1747
|
-
* (foo) -> 'foo'
|
1748
|
-
* foo(.)bar -> 'foo.bar'
|
1749
|
-
* foo.bar(.)baz -> 'foo.bar.baz'
|
1750
|
-
* Otherwise throw.
|
1751
|
-
*/
|
1752
1924
|
function analyzePropertyChain(node, optionalChains) {
|
1753
1925
|
if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
|
1754
1926
|
const result = node.name;
|
1755
1927
|
if (optionalChains) {
|
1756
|
-
// Mark as required.
|
1757
1928
|
optionalChains.set(result, false);
|
1758
1929
|
}
|
1759
1930
|
return result;
|
@@ -1771,11 +1942,9 @@ function analyzePropertyChain(node, optionalChains) {
|
|
1771
1942
|
return result;
|
1772
1943
|
} else if (node.type === 'ChainExpression' && !node.computed) {
|
1773
1944
|
const expression = node.expression;
|
1774
|
-
|
1775
1945
|
if (expression.type === 'CallExpression') {
|
1776
1946
|
throw new Error(`Unsupported node type: ${expression.type}`);
|
1777
1947
|
}
|
1778
|
-
|
1779
1948
|
const object = analyzePropertyChain(expression.object, optionalChains);
|
1780
1949
|
const property = analyzePropertyChain(expression.property, null);
|
1781
1950
|
const result = `${object}.${property}`;
|
@@ -1784,7 +1953,6 @@ function analyzePropertyChain(node, optionalChains) {
|
|
1784
1953
|
}
|
1785
1954
|
throw new Error(`Unsupported node type: ${node.type}`);
|
1786
1955
|
}
|
1787
|
-
|
1788
1956
|
function getNodeWithoutReactNamespace(node) {
|
1789
1957
|
if (
|
1790
1958
|
node.type === 'MemberExpression' &&
|
@@ -1797,12 +1965,6 @@ function getNodeWithoutReactNamespace(node) {
|
|
1797
1965
|
}
|
1798
1966
|
return node;
|
1799
1967
|
}
|
1800
|
-
|
1801
|
-
// What's the index of callback that needs to be analyzed for a given Hook?
|
1802
|
-
// -1 if it's not a Hook we care about (e.g. useState).
|
1803
|
-
// 0 for useEffect/useMemo/useCallback(fn).
|
1804
|
-
// 1 for useImperativeHandle(ref, fn).
|
1805
|
-
// For additionally configured Hooks, assume that they're like useEffect (0).
|
1806
1968
|
function getReactiveHookCallbackIndex(calleeNode, options) {
|
1807
1969
|
const node = getNodeWithoutReactNamespace(calleeNode);
|
1808
1970
|
if (node.type !== 'Identifier') {
|
@@ -1813,15 +1975,11 @@ function getReactiveHookCallbackIndex(calleeNode, options) {
|
|
1813
1975
|
case 'useLayoutEffect':
|
1814
1976
|
case 'useCallback':
|
1815
1977
|
case 'useMemo':
|
1816
|
-
// useEffect(fn)
|
1817
1978
|
return 0;
|
1818
1979
|
case 'useImperativeHandle':
|
1819
|
-
// useImperativeHandle(ref, fn)
|
1820
1980
|
return 1;
|
1821
1981
|
default:
|
1822
1982
|
if (node === calleeNode && options && options.additionalHooks) {
|
1823
|
-
// Allow the user to provide a regular expression which enables the lint to
|
1824
|
-
// target custom reactive hooks.
|
1825
1983
|
let name;
|
1826
1984
|
try {
|
1827
1985
|
name = analyzePropertyChain(node, null);
|
@@ -1836,32 +1994,17 @@ function getReactiveHookCallbackIndex(calleeNode, options) {
|
|
1836
1994
|
return -1;
|
1837
1995
|
}
|
1838
1996
|
}
|
1839
|
-
|
1840
|
-
/**
|
1841
|
-
* ESLint won't assign node.parent to references from context.getScope()
|
1842
|
-
*
|
1843
|
-
* So instead we search for the node from an ancestor assigning node.parent
|
1844
|
-
* as we go. This mutates the AST.
|
1845
|
-
*
|
1846
|
-
* This traversal is:
|
1847
|
-
* - optimized by only searching nodes with a range surrounding our target node
|
1848
|
-
* - agnostic to AST node types, it looks for `{ type: string, ... }`
|
1849
|
-
*/
|
1850
1997
|
function fastFindReferenceWithParent(start, target) {
|
1851
1998
|
const queue = [start];
|
1852
1999
|
let item = null;
|
1853
|
-
|
1854
2000
|
while (queue.length > 0) {
|
1855
2001
|
item = queue.shift();
|
1856
|
-
|
1857
2002
|
if (isSameIdentifier(item, target)) {
|
1858
2003
|
return item;
|
1859
2004
|
}
|
1860
|
-
|
1861
2005
|
if (!isAncestorNodeOf(item, target)) {
|
1862
2006
|
continue;
|
1863
2007
|
}
|
1864
|
-
|
1865
2008
|
for (const [key, value] of Object.entries(item)) {
|
1866
2009
|
if (key === 'parent') {
|
1867
2010
|
continue;
|
@@ -1870,7 +2013,6 @@ function fastFindReferenceWithParent(start, target) {
|
|
1870
2013
|
value.parent = item;
|
1871
2014
|
queue.push(value);
|
1872
2015
|
} else if (Array.isArray(value)) {
|
1873
|
-
// eslint-disable-next-line no-loop-func -- FIXME
|
1874
2016
|
value.forEach(val => {
|
1875
2017
|
if (isNodeLike(val)) {
|
1876
2018
|
val.parent = item;
|
@@ -1880,10 +2022,8 @@ function fastFindReferenceWithParent(start, target) {
|
|
1880
2022
|
}
|
1881
2023
|
}
|
1882
2024
|
}
|
1883
|
-
|
1884
2025
|
return null;
|
1885
2026
|
}
|
1886
|
-
|
1887
2027
|
function joinEnglish(arr) {
|
1888
2028
|
let s = '';
|
1889
2029
|
for (let i = 0; i < arr.length; i++) {
|
@@ -1898,7 +2038,6 @@ function joinEnglish(arr) {
|
|
1898
2038
|
}
|
1899
2039
|
return s;
|
1900
2040
|
}
|
1901
|
-
|
1902
2041
|
function isNodeLike(val) {
|
1903
2042
|
return (
|
1904
2043
|
typeof val === 'object' &&
|
@@ -1907,7 +2046,6 @@ function isNodeLike(val) {
|
|
1907
2046
|
typeof val.type === 'string'
|
1908
2047
|
);
|
1909
2048
|
}
|
1910
|
-
|
1911
2049
|
function isSameIdentifier(a, b) {
|
1912
2050
|
return (
|
1913
2051
|
(a.type === 'Identifier' || a.type === 'JSXIdentifier') &&
|
@@ -1917,7 +2055,530 @@ function isSameIdentifier(a, b) {
|
|
1917
2055
|
a.range[1] === b.range[1]
|
1918
2056
|
);
|
1919
2057
|
}
|
1920
|
-
|
1921
2058
|
function isAncestorNodeOf(a, b) {
|
1922
2059
|
return a.range[0] <= b.range[0] && a.range[1] >= b.range[1];
|
1923
2060
|
}
|
2061
|
+
|
2062
|
+
const Components = require('eslint-plugin-react/lib/util/Components');
|
2063
|
+
var hookUseRef = {
|
2064
|
+
meta: {
|
2065
|
+
docs: {
|
2066
|
+
description: 'Ensure naming of useRef hook value.',
|
2067
|
+
recommended: false
|
2068
|
+
},
|
2069
|
+
schema: [],
|
2070
|
+
type: 'suggestion',
|
2071
|
+
hasSuggestions: true
|
2072
|
+
},
|
2073
|
+
create: Components.detect((context, component, util) => ({
|
2074
|
+
CallExpression(node) {
|
2075
|
+
const isImmediateReturn =
|
2076
|
+
node.parent && node.parent.type === 'ReturnStatement';
|
2077
|
+
if (isImmediateReturn || !util.isReactHookCall(node, ['useRef'])) {
|
2078
|
+
return;
|
2079
|
+
}
|
2080
|
+
if (node.parent.id.type !== 'Identifier') {
|
2081
|
+
return;
|
2082
|
+
}
|
2083
|
+
const variable = node.parent.id.name;
|
2084
|
+
if (!variable.endsWith('Ref')) {
|
2085
|
+
context.report({
|
2086
|
+
node: node,
|
2087
|
+
message: 'useRef call is not end with "Ref"'
|
2088
|
+
});
|
2089
|
+
}
|
2090
|
+
}
|
2091
|
+
}))
|
2092
|
+
};
|
2093
|
+
|
2094
|
+
function* updateImportStatement(context, fixer, key) {
|
2095
|
+
const sourceCode = context.sourceCode;
|
2096
|
+
const importNode = sourceCode.ast.body.find(
|
2097
|
+
node => node.type === 'ImportDeclaration' && node.source.value === 'react'
|
2098
|
+
);
|
2099
|
+
if (!importNode) {
|
2100
|
+
yield fixer.insertTextBefore(
|
2101
|
+
sourceCode.ast.body[0],
|
2102
|
+
`import { ${key} } from 'react';\n`
|
2103
|
+
);
|
2104
|
+
return;
|
2105
|
+
}
|
2106
|
+
if (
|
2107
|
+
importNode.specifiers.length === 1 &&
|
2108
|
+
importNode.specifiers[0].type === 'ImportDefaultSpecifier'
|
2109
|
+
) {
|
2110
|
+
yield fixer.insertTextAfter(importNode.specifiers[0], `, { ${key} }`);
|
2111
|
+
return;
|
2112
|
+
}
|
2113
|
+
const alreadyImportedKeys = importNode.specifiers
|
2114
|
+
.filter(specifier => specifier.type === 'ImportSpecifier')
|
2115
|
+
.map(specifier => specifier.imported.name);
|
2116
|
+
if (alreadyImportedKeys.includes(key)) {
|
2117
|
+
return;
|
2118
|
+
}
|
2119
|
+
yield fixer.insertTextAfter([...importNode.specifiers].pop(), `, ${key}`);
|
2120
|
+
}
|
2121
|
+
var preferNamedPropertyAccess = utils.ESLintUtils.RuleCreator.withoutDocs({
|
2122
|
+
defaultOptions: [],
|
2123
|
+
meta: {
|
2124
|
+
type: 'layout',
|
2125
|
+
fixable: 'code',
|
2126
|
+
docs: {
|
2127
|
+
description:
|
2128
|
+
'Enforce importing each member of React namespace separately instead of accessing them through React namespace'
|
2129
|
+
},
|
2130
|
+
messages: {
|
2131
|
+
illegalReactPropertyAccess:
|
2132
|
+
'Illegal React property access: {{name}}. Use named import instead.'
|
2133
|
+
},
|
2134
|
+
schema: []
|
2135
|
+
},
|
2136
|
+
create(context) {
|
2137
|
+
return {
|
2138
|
+
TSQualifiedName(node) {
|
2139
|
+
if (
|
2140
|
+
('name' in node.left && node.left.name !== 'React') ||
|
2141
|
+
('name' in node.right && node.right.name.endsWith('Event'))
|
2142
|
+
) {
|
2143
|
+
return;
|
2144
|
+
}
|
2145
|
+
context.report({
|
2146
|
+
node,
|
2147
|
+
messageId: 'illegalReactPropertyAccess',
|
2148
|
+
data: {
|
2149
|
+
name: node.right.name
|
2150
|
+
},
|
2151
|
+
*fix(fixer) {
|
2152
|
+
yield fixer.replaceText(node, node.right.name);
|
2153
|
+
yield* updateImportStatement(context, fixer, node.right.name);
|
2154
|
+
}
|
2155
|
+
});
|
2156
|
+
},
|
2157
|
+
MemberExpression(node) {
|
2158
|
+
if (node.object.name !== 'React') {
|
2159
|
+
return;
|
2160
|
+
}
|
2161
|
+
context.report({
|
2162
|
+
node,
|
2163
|
+
messageId: 'illegalReactPropertyAccess',
|
2164
|
+
data: {
|
2165
|
+
name: node.property.name
|
2166
|
+
},
|
2167
|
+
*fix(fixer) {
|
2168
|
+
yield fixer.replaceText(node, node.property.name);
|
2169
|
+
yield* updateImportStatement(context, fixer, node.property.name);
|
2170
|
+
}
|
2171
|
+
});
|
2172
|
+
}
|
2173
|
+
};
|
2174
|
+
}
|
2175
|
+
});
|
2176
|
+
|
2177
|
+
var preferSxProp = {
|
2178
|
+
meta: {
|
2179
|
+
docs: {
|
2180
|
+
description: 'Prefer using sx prop instead of inline styles',
|
2181
|
+
category: 'Best Practices',
|
2182
|
+
recommended: false
|
2183
|
+
},
|
2184
|
+
messages: {
|
2185
|
+
preferSxProp:
|
2186
|
+
'Avoid using inline styles, use sx prop or tss-react or styled-component instead'
|
2187
|
+
},
|
2188
|
+
schema: [
|
2189
|
+
{
|
2190
|
+
type: 'object',
|
2191
|
+
properties: {
|
2192
|
+
allowedFor: {
|
2193
|
+
type: 'array',
|
2194
|
+
uniqueItems: true,
|
2195
|
+
items: { type: 'string' }
|
2196
|
+
}
|
2197
|
+
}
|
2198
|
+
}
|
2199
|
+
]
|
2200
|
+
},
|
2201
|
+
create(context) {
|
2202
|
+
const configuration = context.options[0] || {};
|
2203
|
+
const allowedFor = configuration.allowedFor || [];
|
2204
|
+
function checkComponent(node) {
|
2205
|
+
const parentName = node.parent.name;
|
2206
|
+
const tag =
|
2207
|
+
parentName.name ||
|
2208
|
+
`${parentName.object.name}.${parentName.property.name}`;
|
2209
|
+
const componentName = parentName.name || parentName.property.name;
|
2210
|
+
if (
|
2211
|
+
componentName &&
|
2212
|
+
typeof componentName[0] === 'string' &&
|
2213
|
+
componentName[0] !== componentName[0].toUpperCase()
|
2214
|
+
) {
|
2215
|
+
return;
|
2216
|
+
}
|
2217
|
+
if (allowedFor.includes(tag)) {
|
2218
|
+
return;
|
2219
|
+
}
|
2220
|
+
const prop = node.name.name;
|
2221
|
+
if (prop === 'style') {
|
2222
|
+
context.report({
|
2223
|
+
node,
|
2224
|
+
messageId: 'preferSxProp'
|
2225
|
+
});
|
2226
|
+
}
|
2227
|
+
}
|
2228
|
+
function checkDOMNodes(node) {
|
2229
|
+
const tag = node.parent.name.name;
|
2230
|
+
if (
|
2231
|
+
!(tag && typeof tag === 'string' && tag[0] !== tag[0].toUpperCase())
|
2232
|
+
) {
|
2233
|
+
return;
|
2234
|
+
}
|
2235
|
+
if (allowedFor.includes(tag)) {
|
2236
|
+
return;
|
2237
|
+
}
|
2238
|
+
const prop = node.name.name;
|
2239
|
+
if (prop === 'style') {
|
2240
|
+
context.report({
|
2241
|
+
node,
|
2242
|
+
messageId: 'preferSxProp'
|
2243
|
+
});
|
2244
|
+
}
|
2245
|
+
}
|
2246
|
+
return {
|
2247
|
+
JSXAttribute(node) {
|
2248
|
+
checkComponent(node);
|
2249
|
+
checkDOMNodes(node);
|
2250
|
+
}
|
2251
|
+
};
|
2252
|
+
}
|
2253
|
+
};
|
2254
|
+
|
2255
|
+
var noUnnecessaryTemplateLiterals = {
|
2256
|
+
meta: {
|
2257
|
+
type: 'problem',
|
2258
|
+
docs: {
|
2259
|
+
description: 'Check if a template string contains only one ${}',
|
2260
|
+
recommended: true
|
2261
|
+
},
|
2262
|
+
fixable: 'code',
|
2263
|
+
schema: []
|
2264
|
+
},
|
2265
|
+
create(context) {
|
2266
|
+
return {
|
2267
|
+
TemplateLiteral(node) {
|
2268
|
+
const code = context.sourceCode.getText(node);
|
2269
|
+
if (
|
2270
|
+
code.startsWith('`${') &&
|
2271
|
+
code.endsWith('}`') &&
|
2272
|
+
code.split('${').length === 2
|
2273
|
+
) {
|
2274
|
+
context.report({
|
2275
|
+
node,
|
2276
|
+
message: 'Unnecessary template string with only one ${}.',
|
2277
|
+
fix(fixer) {
|
2278
|
+
return fixer.replaceText(
|
2279
|
+
node,
|
2280
|
+
code.substring(3, code.length - 2)
|
2281
|
+
);
|
2282
|
+
}
|
2283
|
+
});
|
2284
|
+
}
|
2285
|
+
}
|
2286
|
+
};
|
2287
|
+
}
|
2288
|
+
};
|
2289
|
+
|
2290
|
+
function getBasicIdentifier(node) {
|
2291
|
+
if (node.type === 'Identifier') {
|
2292
|
+
return node.name;
|
2293
|
+
}
|
2294
|
+
if (node.type === 'Literal') {
|
2295
|
+
return node.value;
|
2296
|
+
}
|
2297
|
+
if (node.type === 'TemplateLiteral') {
|
2298
|
+
if (node.expressions.length > 0) {
|
2299
|
+
return null;
|
2300
|
+
}
|
2301
|
+
return node.quasis[0].value.raw;
|
2302
|
+
}
|
2303
|
+
return null;
|
2304
|
+
}
|
2305
|
+
function getBaseIdentifier(node) {
|
2306
|
+
switch (node.type) {
|
2307
|
+
case 'Identifier': {
|
2308
|
+
return node;
|
2309
|
+
}
|
2310
|
+
case 'CallExpression': {
|
2311
|
+
return getBaseIdentifier(node.callee);
|
2312
|
+
}
|
2313
|
+
case 'MemberExpression': {
|
2314
|
+
return getBaseIdentifier(node.object);
|
2315
|
+
}
|
2316
|
+
}
|
2317
|
+
return null;
|
2318
|
+
}
|
2319
|
+
function getStyesObj(node) {
|
2320
|
+
const isMakeStyles = node.callee.name === 'makeStyles';
|
2321
|
+
const isModernApi =
|
2322
|
+
node.callee.type === 'MemberExpression' &&
|
2323
|
+
node.callee.property.name === 'create' &&
|
2324
|
+
getBaseIdentifier(node.callee.object) &&
|
2325
|
+
getBaseIdentifier(node.callee.object).name === 'tss';
|
2326
|
+
if (!isMakeStyles && !isModernApi) {
|
2327
|
+
return;
|
2328
|
+
}
|
2329
|
+
const styles = (() => {
|
2330
|
+
if (isMakeStyles) {
|
2331
|
+
return node.parent.arguments[0];
|
2332
|
+
}
|
2333
|
+
if (isModernApi) {
|
2334
|
+
return node.callee.parent.arguments[0];
|
2335
|
+
}
|
2336
|
+
})();
|
2337
|
+
if (!styles) {
|
2338
|
+
return;
|
2339
|
+
}
|
2340
|
+
switch (styles.type) {
|
2341
|
+
case 'ObjectExpression':
|
2342
|
+
return styles;
|
2343
|
+
case 'ArrowFunctionExpression':
|
2344
|
+
{
|
2345
|
+
const { body } = styles;
|
2346
|
+
switch (body.type) {
|
2347
|
+
case 'ObjectExpression':
|
2348
|
+
return body;
|
2349
|
+
case 'BlockStatement': {
|
2350
|
+
let stylesObj;
|
2351
|
+
body.body.forEach(bodyNode => {
|
2352
|
+
if (
|
2353
|
+
bodyNode.type === 'ReturnStatement' &&
|
2354
|
+
bodyNode.argument.type === 'ObjectExpression'
|
2355
|
+
) {
|
2356
|
+
stylesObj = bodyNode.argument;
|
2357
|
+
}
|
2358
|
+
});
|
2359
|
+
return stylesObj;
|
2360
|
+
}
|
2361
|
+
}
|
2362
|
+
}
|
2363
|
+
break;
|
2364
|
+
}
|
2365
|
+
}
|
2366
|
+
|
2367
|
+
var classNaming = {
|
2368
|
+
meta: {
|
2369
|
+
type: 'problem'
|
2370
|
+
},
|
2371
|
+
create: function rule(context) {
|
2372
|
+
return {
|
2373
|
+
CallExpression(node) {
|
2374
|
+
const stylesObj = getStyesObj(node);
|
2375
|
+
if (stylesObj === undefined) {
|
2376
|
+
return;
|
2377
|
+
}
|
2378
|
+
stylesObj.properties.forEach(property => {
|
2379
|
+
if (property.computed) {
|
2380
|
+
return;
|
2381
|
+
}
|
2382
|
+
if (
|
2383
|
+
property.type === 'ExperimentalSpreadProperty' ||
|
2384
|
+
property.type === 'SpreadElement'
|
2385
|
+
) {
|
2386
|
+
return;
|
2387
|
+
}
|
2388
|
+
const className = property.key.value || property.key.name;
|
2389
|
+
if (!eslintUtils.isCamelCase(className)) {
|
2390
|
+
context.report({
|
2391
|
+
node: property,
|
2392
|
+
message: `Class \`${className}\` must be camelCase in TSS.`
|
2393
|
+
});
|
2394
|
+
}
|
2395
|
+
});
|
2396
|
+
}
|
2397
|
+
};
|
2398
|
+
}
|
2399
|
+
};
|
2400
|
+
|
2401
|
+
var noColorValue = {
|
2402
|
+
meta: {
|
2403
|
+
type: 'problem',
|
2404
|
+
docs: {
|
2405
|
+
description:
|
2406
|
+
'Enforce the use of color variables instead of color codes within TSS'
|
2407
|
+
}
|
2408
|
+
},
|
2409
|
+
create: function (context) {
|
2410
|
+
const parserOptions = context.parserOptions;
|
2411
|
+
if (!parserOptions || !parserOptions.project) {
|
2412
|
+
return {};
|
2413
|
+
}
|
2414
|
+
return {
|
2415
|
+
CallExpression(node) {
|
2416
|
+
const stylesObj = getStyesObj(node);
|
2417
|
+
if (!stylesObj) {
|
2418
|
+
return;
|
2419
|
+
}
|
2420
|
+
function checkColorLiteral(value) {
|
2421
|
+
if (value.type === 'Literal' && typeof value.value === 'string') {
|
2422
|
+
const colorCodePattern =
|
2423
|
+
/#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\?\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}(?:\s*,\s*\d*(?:\.\d+)?)?\s*\)/g;
|
2424
|
+
const isColorCode = colorCodePattern.test(value.value);
|
2425
|
+
if (isColorCode) {
|
2426
|
+
context.report({
|
2427
|
+
node: value,
|
2428
|
+
message: 'Use color variables instead of color codes in TSS.'
|
2429
|
+
});
|
2430
|
+
}
|
2431
|
+
}
|
2432
|
+
}
|
2433
|
+
function loopStylesObj(obj) {
|
2434
|
+
if (obj && obj.type === 'ObjectExpression') {
|
2435
|
+
obj.properties.forEach(property => {
|
2436
|
+
if (property.type === 'Property' && property.value) {
|
2437
|
+
if (property.value.type === 'ObjectExpression') {
|
2438
|
+
loopStylesObj(property.value);
|
2439
|
+
} else {
|
2440
|
+
checkColorLiteral(property.value);
|
2441
|
+
}
|
2442
|
+
}
|
2443
|
+
});
|
2444
|
+
}
|
2445
|
+
}
|
2446
|
+
loopStylesObj(stylesObj);
|
2447
|
+
}
|
2448
|
+
};
|
2449
|
+
}
|
2450
|
+
};
|
2451
|
+
|
2452
|
+
var unusedClasses = {
|
2453
|
+
meta: {
|
2454
|
+
type: 'problem'
|
2455
|
+
},
|
2456
|
+
create: function rule(context) {
|
2457
|
+
const usedClasses = {};
|
2458
|
+
const definedClasses = {};
|
2459
|
+
return {
|
2460
|
+
CallExpression(node) {
|
2461
|
+
const stylesObj = getStyesObj(node);
|
2462
|
+
if (stylesObj === undefined) {
|
2463
|
+
return;
|
2464
|
+
}
|
2465
|
+
stylesObj.properties.forEach(property => {
|
2466
|
+
if (property.computed) {
|
2467
|
+
return;
|
2468
|
+
}
|
2469
|
+
if (
|
2470
|
+
property.type === 'ExperimentalSpreadProperty' ||
|
2471
|
+
property.type === 'SpreadElement'
|
2472
|
+
) {
|
2473
|
+
return;
|
2474
|
+
}
|
2475
|
+
definedClasses[property.key.value || property.key.name] = property;
|
2476
|
+
});
|
2477
|
+
},
|
2478
|
+
MemberExpression(node) {
|
2479
|
+
if (
|
2480
|
+
node.object.type === 'Identifier' &&
|
2481
|
+
node.object.name === 'classes'
|
2482
|
+
) {
|
2483
|
+
const whichClass = getBasicIdentifier(node.property);
|
2484
|
+
if (whichClass) {
|
2485
|
+
usedClasses[whichClass] = true;
|
2486
|
+
}
|
2487
|
+
return;
|
2488
|
+
}
|
2489
|
+
const classIdentifier = getBasicIdentifier(node.property);
|
2490
|
+
if (!classIdentifier) {
|
2491
|
+
return;
|
2492
|
+
}
|
2493
|
+
if (classIdentifier !== 'classes') {
|
2494
|
+
return;
|
2495
|
+
}
|
2496
|
+
const { parent } = node;
|
2497
|
+
if (parent.type !== 'MemberExpression') {
|
2498
|
+
return;
|
2499
|
+
}
|
2500
|
+
if (
|
2501
|
+
node.object.object &&
|
2502
|
+
node.object.object.type !== 'ThisExpression'
|
2503
|
+
) {
|
2504
|
+
return;
|
2505
|
+
}
|
2506
|
+
const propsIdentifier = getBasicIdentifier(parent.object);
|
2507
|
+
if (propsIdentifier && propsIdentifier !== 'props') {
|
2508
|
+
return;
|
2509
|
+
}
|
2510
|
+
if (!propsIdentifier && parent.object.type !== 'MemberExpression') {
|
2511
|
+
return;
|
2512
|
+
}
|
2513
|
+
if (parent.parent.type === 'MemberExpression') {
|
2514
|
+
return;
|
2515
|
+
}
|
2516
|
+
const parentClassIdentifier = getBasicIdentifier(parent.property);
|
2517
|
+
if (parentClassIdentifier) {
|
2518
|
+
usedClasses[parentClassIdentifier] = true;
|
2519
|
+
}
|
2520
|
+
},
|
2521
|
+
'Program:exit': () => {
|
2522
|
+
Object.keys(definedClasses).forEach(definedClassKey => {
|
2523
|
+
if (!usedClasses[definedClassKey]) {
|
2524
|
+
context.report({
|
2525
|
+
node: definedClasses[definedClassKey],
|
2526
|
+
message: `Class \`${definedClassKey}\` is unused`
|
2527
|
+
});
|
2528
|
+
}
|
2529
|
+
});
|
2530
|
+
}
|
2531
|
+
};
|
2532
|
+
}
|
2533
|
+
};
|
2534
|
+
|
2535
|
+
var ruleFiles = /*#__PURE__*/Object.freeze({
|
2536
|
+
__proto__: null,
|
2537
|
+
rules_import_enforce_icon_alias: enforceIconAlias,
|
2538
|
+
rules_import_monorepo: monorepo,
|
2539
|
+
rules_intl_id_missing: idMissing,
|
2540
|
+
rules_intl_id_prefix: idPrefix,
|
2541
|
+
rules_intl_id_unused: idUnused,
|
2542
|
+
rules_intl_no_default: noDefault,
|
2543
|
+
rules_react_better_exhaustive_deps: betterExhaustiveDeps,
|
2544
|
+
rules_react_hook_use_ref: hookUseRef,
|
2545
|
+
rules_react_prefer_named_property_access: preferNamedPropertyAccess,
|
2546
|
+
rules_react_prefer_sx_prop: preferSxProp,
|
2547
|
+
rules_stylistic_no_unnecessary_template_literals: noUnnecessaryTemplateLiterals,
|
2548
|
+
rules_tss_class_naming: classNaming,
|
2549
|
+
rules_tss_no_color_value: noColorValue,
|
2550
|
+
rules_tss_unused_classes: unusedClasses
|
2551
|
+
});
|
2552
|
+
|
2553
|
+
const rules = {};
|
2554
|
+
Object.keys(ruleFiles).forEach(key => {
|
2555
|
+
const ruleKey = key.replace(/^rules_/, '');
|
2556
|
+
const finalKey = ruleKey.replace(/_/g, '-');
|
2557
|
+
rules[finalKey] = ruleFiles[key];
|
2558
|
+
});
|
2559
|
+
var index = {
|
2560
|
+
rules,
|
2561
|
+
configs: {
|
2562
|
+
recommended: {
|
2563
|
+
plugins: ['@agilebot'],
|
2564
|
+
rules: {
|
2565
|
+
'@agilebot/react-prefer-named-property-access': 'error',
|
2566
|
+
'@agilebot/react-hook-use-ref': 'warn',
|
2567
|
+
'@agilebot/react-prefer-sx-prop': 'error',
|
2568
|
+
'@agilebot/tss-unused-classes': 'warn',
|
2569
|
+
'@agilebot/tss-no-color-value': 'error',
|
2570
|
+
'@agilebot/tss-class-naming': 'error',
|
2571
|
+
'@agilebot/import-enforce-icon-alias': 'error',
|
2572
|
+
'@agilebot/import-monorepo': 'error',
|
2573
|
+
'@agilebot/stylistic-no-unnecessary-template-literals': 'error'
|
2574
|
+
},
|
2575
|
+
settings: {
|
2576
|
+
react: {
|
2577
|
+
version: '18.0.0'
|
2578
|
+
}
|
2579
|
+
}
|
2580
|
+
}
|
2581
|
+
}
|
2582
|
+
};
|
2583
|
+
|
2584
|
+
module.exports = index;
|