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