@agilebot/eslint-plugin 0.2.2 → 0.2.4

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