@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.
@@ -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;