@agilebot/eslint-plugin 0.2.1 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,11 +1,519 @@
1
- /**
2
- * Copyright (c) Facebook, Inc. and its affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
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
- module.exports = {
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 && // Used to enable legacy behavior. Dangerous.
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) && // Is second tuple value the same reference we're checking?
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: [], // Pretend we don't know
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; // Handled
1556
+ return;
1286
1557
  case 'Identifier':
1287
1558
  if (!declaredDependenciesNode) {
1288
- // No deps, no problems.
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
- // If it's already in the list of deps, we don't care because
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
- // If it's not in scope, we don't care.
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; // Unhandled
1575
+ break;
1315
1576
  }
1316
1577
  if (def.type !== 'Variable' && def.type !== 'FunctionName') {
1317
- // Parameter or an unusual pattern. Bail out.
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; // Handled
1589
+ return;
1331
1590
  case 'VariableDeclarator':
1332
1591
  const init = def.node.init;
1333
1592
  if (!init) {
1334
- break; // Unhandled
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; // Handled
1605
+ return;
1350
1606
  }
1351
- break; // Unhandled
1607
+ break;
1352
1608
  }
1353
- break; // Unhandled
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; // Handled
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, // True if used in code
1412
- isSatisfiedRecursively: false, // True if specified in deps
1413
- isSubtreeUsed: false, // True if something deeper is used by code
1414
- children: new Map() // Nodes for properties
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' && // Ensure this is not destructed assignment
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 && // This reference is outside the Hook callback.
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;