@agilebot/eslint-plugin 0.5.2 → 0.5.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.
Files changed (2) hide show
  1. package/dist/index.js +79 -12
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license @agilebot/eslint-plugin v0.5.2
2
+ * @license @agilebot/eslint-plugin v0.5.4
3
3
  *
4
4
  * Copyright (c) Agilebot, Inc. and its affiliates.
5
5
  *
@@ -792,6 +792,14 @@ var reactBetterExhaustiveDeps = {
792
792
  enableDangerousAutofixThisMayCauseInfiniteLoops: {
793
793
  type: 'boolean'
794
794
  },
795
+ customHooks: {
796
+ type: 'object',
797
+ additionalProperties: {
798
+ callbackIndex: {
799
+ type: 'number'
800
+ }
801
+ }
802
+ },
795
803
  staticHooks: {
796
804
  type: 'object',
797
805
  additionalProperties: {
@@ -819,11 +827,13 @@ var reactBetterExhaustiveDeps = {
819
827
  create(context) {
820
828
  const additionalHooks = context.options && context.options[0] && context.options[0].additionalHooks ? new RegExp(context.options[0].additionalHooks) : undefined;
821
829
  const enableDangerousAutofixThisMayCauseInfiniteLoops = context.options && context.options[0] && context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops || false;
830
+ const customHooks = context.options && context.options[0] && context.options[0].customHooks || {};
822
831
  const staticHooks = context.options && context.options[0] && context.options[0].staticHooks || {};
823
832
  const checkMemoizedVariableIsStatic = context.options && context.options[0] && context.options[0].checkMemoizedVariableIsStatic || false;
824
833
  const options = {
825
834
  additionalHooks,
826
835
  enableDangerousAutofixThisMayCauseInfiniteLoops,
836
+ customHooks,
827
837
  staticHooks,
828
838
  checkMemoizedVariableIsStatic
829
839
  };
@@ -849,6 +859,7 @@ var reactBetterExhaustiveDeps = {
849
859
  const stateVariables = new WeakSet();
850
860
  const stableKnownValueCache = new WeakMap();
851
861
  const functionWithoutCapturedValueCache = new WeakMap();
862
+ const useEffectEventVariables = new WeakSet();
852
863
  function memoizeWithWeakMap(fn, map) {
853
864
  return function (arg) {
854
865
  if (map.has(arg)) {
@@ -861,10 +872,20 @@ var reactBetterExhaustiveDeps = {
861
872
  }
862
873
  function visitFunctionWithDependencies(node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect) {
863
874
  if (isEffect && node.async) {
864
- reportProblem({
865
- node: node,
866
- message: "Effect callbacks are synchronous to prevent race conditions. " + "Put the async function inside:\n\n" + 'useEffect(() => {\n' + ' async function fetchData() {\n' + ' // You can await here\n' + ' const response = await MyAPI.getData(someId);\n' + ' // ...\n' + ' }\n' + ' fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching'
867
- });
875
+ let isCustomHook = false;
876
+ if (options.customHooks) {
877
+ Object.entries(options.customHooks).forEach(([key, customParts]) => {
878
+ if (typeof customParts === 'object' && new RegExp(key).test(reactiveHookName)) {
879
+ isCustomHook = true;
880
+ }
881
+ });
882
+ }
883
+ if (!isCustomHook) {
884
+ reportProblem({
885
+ node: node,
886
+ message: "Effect callbacks are synchronous to prevent race conditions. " + "Put the async function inside:\n\n" + 'useEffect(() => {\n' + ' async function fetchData() {\n' + ' // You can await here\n' + ' const response = await MyAPI.getData(someId);\n' + ' // ...\n' + ' }\n' + ' fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching'
887
+ });
888
+ }
868
889
  }
869
890
  const scope = scopeManager.acquire(node);
870
891
  const pureScopes = new Set();
@@ -901,7 +922,7 @@ var reactBetterExhaustiveDeps = {
901
922
  if (init == null) {
902
923
  return false;
903
924
  }
904
- while (init.type === 'TSAsExpression') {
925
+ while (init.type === 'TSAsExpression' || init.type === 'AsExpression') {
905
926
  init = init.expression;
906
927
  }
907
928
  let declaration = def.node.parent;
@@ -931,7 +952,14 @@ var reactBetterExhaustiveDeps = {
931
952
  } = callee;
932
953
  if (name === 'useRef' && id.type === 'Identifier') {
933
954
  return true;
934
- } else if (name === 'useState' || name === 'useReducer') {
955
+ } else if (isUseEffectEventIdentifier() && id.type === 'Identifier') {
956
+ for (const ref of resolved.references) {
957
+ if (ref !== id) {
958
+ useEffectEventVariables.add(ref.identifier);
959
+ }
960
+ }
961
+ return true;
962
+ } else if (name === 'useState' || name === 'useReducer' || name === 'useActionState') {
935
963
  if (id.type === 'ArrayPattern' && id.elements.length === 2 && isArray(resolved.identifiers)) {
936
964
  if (id.elements[1] === resolved.identifiers[0]) {
937
965
  if (name === 'useState') {
@@ -1218,13 +1246,16 @@ var reactBetterExhaustiveDeps = {
1218
1246
  }
1219
1247
  const declaredDependencies = [];
1220
1248
  const externalDependencies = new Set();
1221
- if (declaredDependenciesNode.type !== 'ArrayExpression') {
1249
+ const isArrayExpression = declaredDependenciesNode.type === 'ArrayExpression';
1250
+ const isTSAsArrayExpression = declaredDependenciesNode.type === 'TSAsExpression' && declaredDependenciesNode.expression.type === 'ArrayExpression';
1251
+ if (!isArrayExpression && !isTSAsArrayExpression) {
1222
1252
  reportProblem({
1223
1253
  node: declaredDependenciesNode,
1224
1254
  message: "React Hook ".concat(getSource(reactiveHook), " was passed a ") + 'dependency list that is not an array literal. This means we ' + "can't statically verify whether you've passed the correct " + 'dependencies.'
1225
1255
  });
1226
1256
  } else {
1227
- declaredDependenciesNode.elements.forEach(declaredDependencyNode => {
1257
+ const arrayExpression = isTSAsArrayExpression ? declaredDependenciesNode.expression : declaredDependenciesNode;
1258
+ arrayExpression.elements.forEach(declaredDependencyNode => {
1228
1259
  if (declaredDependencyNode == null) {
1229
1260
  return;
1230
1261
  }
@@ -1235,6 +1266,18 @@ var reactBetterExhaustiveDeps = {
1235
1266
  });
1236
1267
  return;
1237
1268
  }
1269
+ if (useEffectEventVariables.has(declaredDependencyNode)) {
1270
+ reportProblem({
1271
+ node: declaredDependencyNode,
1272
+ message: 'Functions returned from `useEffectEvent` must not be included in the dependency array. ' + "Remove `".concat(getSource(declaredDependencyNode), "` from the list."),
1273
+ suggest: [{
1274
+ desc: "Remove the dependency `".concat(getSource(declaredDependencyNode), "`"),
1275
+ fix(fixer) {
1276
+ return fixer.removeRange(declaredDependencyNode.range);
1277
+ }
1278
+ }]
1279
+ });
1280
+ }
1238
1281
  let declaredDependency;
1239
1282
  try {
1240
1283
  declaredDependency = analyzePropertyChain(declaredDependencyNode, null);
@@ -1512,7 +1555,8 @@ var reactBetterExhaustiveDeps = {
1512
1555
  extraWarning = " If '".concat(setStateRecommendation.setter, "' needs the ") + "current value of '".concat(setStateRecommendation.missingDep, "', ") + "you can also switch to useReducer instead of useState and " + "read '".concat(setStateRecommendation.missingDep, "' in the reducer.");
1513
1556
  break;
1514
1557
  case 'updater':
1515
- extraWarning = " You can also do a functional update '".concat(setStateRecommendation.setter, "(").concat(setStateRecommendation.missingDep.slice(0, 1), " => ...)' if you only need '").concat(setStateRecommendation.missingDep, "' in the '").concat(setStateRecommendation.setter, "' call.");
1558
+ extraWarning = " You can also do a functional update '".concat(setStateRecommendation.setter, "(").concat(setStateRecommendation.missingDep.slice(0, 1), " => ...)' if you only need '").concat(setStateRecommendation.missingDep
1559
+ , "'") + " in the '".concat(setStateRecommendation.setter, "' call.");
1516
1560
  break;
1517
1561
  default:
1518
1562
  throw new Error('Unknown case.');
@@ -1539,7 +1583,8 @@ var reactBetterExhaustiveDeps = {
1539
1583
  const callback = node.arguments[callbackIndex];
1540
1584
  const reactiveHook = node.callee;
1541
1585
  const reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
1542
- const declaredDependenciesNode = node.arguments[callbackIndex + 1];
1586
+ const maybeNode = node.arguments[callbackIndex + 1];
1587
+ const declaredDependenciesNode = maybeNode && !(maybeNode.type === 'Identifier' && maybeNode.name === 'undefined') ? maybeNode : undefined;
1543
1588
  const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName);
1544
1589
  if (!callback) {
1545
1590
  reportProblem({
@@ -1562,6 +1607,9 @@ var reactBetterExhaustiveDeps = {
1562
1607
  case 'ArrowFunctionExpression':
1563
1608
  visitFunctionWithDependencies(callback, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect);
1564
1609
  return;
1610
+ case 'TSAsExpression':
1611
+ visitFunctionWithDependencies(callback.expression, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect);
1612
+ return;
1565
1613
  case 'Identifier':
1566
1614
  if (!declaredDependenciesNode) {
1567
1615
  return;
@@ -1766,7 +1814,7 @@ function getConstructionExpressionType(node) {
1766
1814
  }
1767
1815
  return null;
1768
1816
  case 'TypeCastExpression':
1769
- return getConstructionExpressionType(node.expression);
1817
+ case 'AsExpression':
1770
1818
  case 'TSAsExpression':
1771
1819
  return getConstructionExpressionType(node.expression);
1772
1820
  }
@@ -1904,6 +1952,22 @@ function getReactiveHookCallbackIndex(calleeNode, options) {
1904
1952
  case 'useImperativeHandle':
1905
1953
  return 1;
1906
1954
  default:
1955
+ if (node === calleeNode && options && options.customHooks) {
1956
+ let name;
1957
+ try {
1958
+ name = analyzePropertyChain(node, null);
1959
+ } catch (err) {
1960
+ if (/Unsupported node type/.test(err.message)) {
1961
+ return 0;
1962
+ }
1963
+ throw err;
1964
+ }
1965
+ for (const [key, customParts] of Object.entries(options.customHooks)) {
1966
+ if (typeof customParts === 'object' && typeof customParts.callbackIndex === 'number' && new RegExp(key).test(name)) {
1967
+ return customParts.callbackIndex;
1968
+ }
1969
+ }
1970
+ }
1907
1971
  if (node === calleeNode && options && options.additionalHooks) {
1908
1972
  let name;
1909
1973
  try {
@@ -1972,6 +2036,9 @@ function isSameIdentifier(a, b) {
1972
2036
  function isAncestorNodeOf(a, b) {
1973
2037
  return a.range[0] <= b.range[0] && a.range[1] >= b.range[1];
1974
2038
  }
2039
+ function isUseEffectEventIdentifier(node) {
2040
+ return false;
2041
+ }
1975
2042
 
1976
2043
  const Components = require('eslint-plugin-react/lib/util/Components');
1977
2044
  const RULE_NAME$7 = 'react-hook-use-ref';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agilebot/eslint-plugin",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Agilebot's ESLint plugin",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "eslint-plugin-deprecation": "^3.0.0",
24
24
  "eslint-plugin-react": "^7.35.0",
25
25
  "eslint-plugin-react-hooks": "^4.6.2",
26
- "@agilebot/eslint-utils": "0.5.2"
26
+ "@agilebot/eslint-utils": "0.5.4"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0"