@agilebot/eslint-plugin 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;