eslint-plugin-react-x 5.8.17 → 5.8.19

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 +171 -131
  2. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -3,12 +3,12 @@ import { ESLintUtils } from "@typescript-eslint/utils";
3
3
  import { Check, Compare, Extract, Traverse, is, isOneOf } from "@eslint-react/ast";
4
4
  import * as core from "@eslint-react/core";
5
5
  import { merge } from "@eslint-react/eslint";
6
+ import { JsxDetectionHint, findParentAttribute, getElementFullType, hasAttribute } from "@eslint-react/jsx";
6
7
  import { AST_NODE_TYPES } from "@typescript-eslint/types";
7
8
  import { DefinitionType, ScopeType } from "@typescript-eslint/scope-manager";
8
9
  import { findVariable, getStaticValue } from "@typescript-eslint/utils/ast-utils";
9
10
  import { computeObjectType, isAssignmentTargetEqual, resolve, resolveEnclosingAssignmentTarget } from "@eslint-react/var";
10
- import { P, isMatching, match } from "ts-pattern";
11
- import { findParentAttribute, getElementFullType, hasAttribute } from "@eslint-react/jsx";
11
+ import { P, match } from "ts-pattern";
12
12
  import { compare } from "compare-versions";
13
13
  import { getConstrainedTypeAtLocation } from "@typescript-eslint/type-utils";
14
14
  import { unionConstituents } from "ts-api-utils";
@@ -143,7 +143,7 @@ const rules$6 = {
143
143
  //#endregion
144
144
  //#region package.json
145
145
  var name$6 = "eslint-plugin-react-x";
146
- var version = "5.8.17";
146
+ var version = "5.8.19";
147
147
 
148
148
  //#endregion
149
149
  //#region src/utils/create-rule.ts
@@ -192,7 +192,7 @@ function getEnclosingTryBlock(node) {
192
192
  }
193
193
  function create$50(context) {
194
194
  if (!context.sourceCode.text.includes("try")) return {};
195
- const hint = core.JsxDetectionHint.DoNotIncludeJsxWithNullValue | core.JsxDetectionHint.DoNotIncludeJsxWithNumberValue | core.JsxDetectionHint.DoNotIncludeJsxWithBigIntValue | core.JsxDetectionHint.DoNotIncludeJsxWithStringValue | core.JsxDetectionHint.DoNotIncludeJsxWithBooleanValue | core.JsxDetectionHint.DoNotIncludeJsxWithUndefinedValue | core.JsxDetectionHint.DoNotIncludeJsxWithEmptyArrayValue;
195
+ const hint = JsxDetectionHint.DoNotIncludeJsxWithNullValue | JsxDetectionHint.DoNotIncludeJsxWithNumberValue | JsxDetectionHint.DoNotIncludeJsxWithBigIntValue | JsxDetectionHint.DoNotIncludeJsxWithStringValue | JsxDetectionHint.DoNotIncludeJsxWithBooleanValue | JsxDetectionHint.DoNotIncludeJsxWithUndefinedValue | JsxDetectionHint.DoNotIncludeJsxWithEmptyArrayValue;
196
196
  const fc = core.getFunctionComponentCollector(context);
197
197
  const hc = core.getHookCollector(context);
198
198
  const reported = /* @__PURE__ */ new Set();
@@ -1798,38 +1798,59 @@ function report$2(context) {
1798
1798
  context.report(descriptor);
1799
1799
  };
1800
1800
  }
1801
- function getIndexParamPosition(methodName) {
1802
- switch (methodName) {
1803
- case "every":
1804
- case "filter":
1805
- case "find":
1806
- case "findIndex":
1807
- case "findLast":
1808
- case "findLastIndex":
1809
- case "flatMap":
1810
- case "forEach":
1811
- case "map":
1812
- case "some": return 1;
1813
- case "reduce":
1814
- case "reduceRight": return 2;
1815
- default: return -1;
1816
- }
1817
- }
1818
- function getMapIndexParamName(context, node) {
1819
- const callee = Extract.unwrap(node.callee);
1820
- if (callee.type !== AST_NODE_TYPES.MemberExpression) return null;
1821
- if (callee.property.type !== AST_NODE_TYPES.Identifier) return null;
1822
- const { name } = callee.property;
1823
- const indexPosition = getIndexParamPosition(name);
1824
- if (indexPosition === -1) return null;
1825
- const callbackArg = node.arguments[core.isChildrenMap(context, callee) || core.isChildrenForEach(context, callee) ? 1 : 0];
1826
- if (callbackArg == null) return null;
1827
- if (!isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(callbackArg)) return null;
1828
- const { params } = callbackArg;
1829
- if (params.length < indexPosition + 1) return null;
1830
- const param = params.at(indexPosition);
1831
- return param != null && "name" in param ? param.name : null;
1801
+ /**
1802
+ * Iterator-like methods that pass the item's index to their callback,
1803
+ * mapped to the position of the index parameter in the callback's parameter list.
1804
+ * `map` and `forEach` also cover `Children.map` and `Children.forEach`,
1805
+ * whose callback is the second argument instead of the first.
1806
+ */
1807
+ const INDEX_PARAM_POSITIONS = new Map([
1808
+ ["every", 1],
1809
+ ["filter", 1],
1810
+ ["find", 1],
1811
+ ["findIndex", 1],
1812
+ ["findLast", 1],
1813
+ ["findLastIndex", 1],
1814
+ ["flatMap", 1],
1815
+ ["forEach", 1],
1816
+ ["map", 1],
1817
+ ["reduce", 2],
1818
+ ["reduceRight", 2],
1819
+ ["some", 1]
1820
+ ]);
1821
+ /**
1822
+ * Checks whether an identifier is a reference to the index parameter of an
1823
+ * iterator-like method's callback (e.g. `i` in `items.map((item, i) => ...)`).
1824
+ * @param context The ESLint rule context.
1825
+ * @param node The identifier to check.
1826
+ * @returns `true` if the identifier resolves to an array index parameter.
1827
+ */
1828
+ function isArrayIndexReference(context, node) {
1829
+ const def = findVariable(context.sourceCode.getScope(node), node)?.defs.at(0);
1830
+ if (def == null || def.type !== DefinitionType.Parameter) return false;
1831
+ const callback = def.node;
1832
+ if (!isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(callback)) return false;
1833
+ let argument = callback;
1834
+ while (Check.isTypeExpression(argument.parent)) argument = argument.parent;
1835
+ const call = argument.parent;
1836
+ if (call.type !== AST_NODE_TYPES.CallExpression) return false;
1837
+ const callee = Extract.unwrap(call.callee);
1838
+ if (callee.type !== AST_NODE_TYPES.MemberExpression) return false;
1839
+ if (callee.property.type !== AST_NODE_TYPES.Identifier) return false;
1840
+ const indexPosition = INDEX_PARAM_POSITIONS.get(callee.property.name);
1841
+ if (indexPosition == null) return false;
1842
+ const callbackPosition = core.isChildrenMap(context, callee) || core.isChildrenForEach(context, callee) ? 1 : 0;
1843
+ if (call.arguments[callbackPosition] !== argument) return false;
1844
+ const param = callback.params[indexPosition];
1845
+ if (param == null) return false;
1846
+ return param === def.name || param.type === AST_NODE_TYPES.AssignmentPattern && param.left === def.name;
1832
1847
  }
1848
+ /**
1849
+ * Recursively collects all identifiers from a binary expression.
1850
+ * e.g., for `a + b + c`, it returns identifiers for a, b, and c.
1851
+ * @param side The binary expression (or one of its sides) to collect from.
1852
+ * @returns The identifiers found in the expression.
1853
+ */
1833
1854
  function getIdentifiersFromBinaryExpression(side) {
1834
1855
  if (side.type === AST_NODE_TYPES.Identifier) return [side];
1835
1856
  if (side.type === AST_NODE_TYPES.BinaryExpression) return [...getIdentifiersFromBinaryExpression(side.left), ...getIdentifiersFromBinaryExpression(side.right)];
@@ -1839,6 +1860,8 @@ function getIdentifiersFromBinaryExpression(side) {
1839
1860
  //#endregion
1840
1861
  //#region src/rules/no-array-index-key/no-array-index-key.ts
1841
1862
  const RULE_NAME$46 = "no-array-index-key";
1863
+ /** Global functions that convert their first argument while preserving its identity as a key. */
1864
+ const COERCION_FUNCTIONS = new Set(["Number", "String"]);
1842
1865
  var no_array_index_key_default = createRule({
1843
1866
  meta: {
1844
1867
  type: "suggestion",
@@ -1850,69 +1873,74 @@ var no_array_index_key_default = createRule({
1850
1873
  create: create$46,
1851
1874
  defaultOptions: []
1852
1875
  });
1876
+ /**
1877
+ * Gets the value of an object property named 'key' (e.g. in `createElement('div', { key: ... })`).
1878
+ * @param property The object literal member to inspect.
1879
+ * @returns The value of the 'key' property, or `null` if the member is not one.
1880
+ */
1881
+ function getKeyPropValue(property) {
1882
+ if (property.type !== AST_NODE_TYPES.Property || property.computed) return null;
1883
+ return property.key.type === AST_NODE_TYPES.Identifier && property.key.name === "key" || property.key.type === AST_NODE_TYPES.Literal && property.key.value === "key" ? property.value : null;
1884
+ }
1853
1885
  function create$46(context) {
1854
- const indexParamNames = [];
1855
1886
  function isArrayIndex(node) {
1856
- return node.type === AST_NODE_TYPES.Identifier && indexParamNames.some((name) => name != null && name === node.name);
1887
+ return node.type === AST_NODE_TYPES.Identifier && isArrayIndexReference(context, node);
1857
1888
  }
1858
1889
  function isCreateOrCloneElementCall(node) {
1859
1890
  return core.isCreateElementCall(context, node) || core.isCloneElementCall(context, node);
1860
1891
  }
1861
- function getReportDescriptors(node) {
1892
+ /**
1893
+ * Checks an expression used as a 'key' value, recursing into the branches
1894
+ * it may evaluate to and reporting every array index it is derived from.
1895
+ * @param node The key value expression to check.
1896
+ * @returns The report descriptors for the violations found.
1897
+ */
1898
+ function checkKeyExpression(node) {
1862
1899
  switch (node.type) {
1863
- case AST_NODE_TYPES.Identifier:
1864
- if (indexParamNames.some((name) => name != null && name === node.name)) return [{
1900
+ case AST_NODE_TYPES.Identifier: return isArrayIndex(node) ? [{
1901
+ messageId: "default",
1902
+ node
1903
+ }] : [];
1904
+ case AST_NODE_TYPES.TemplateLiteral: return node.expressions.filter(isArrayIndex).map((expression) => ({
1905
+ messageId: "default",
1906
+ node: expression
1907
+ }));
1908
+ case AST_NODE_TYPES.BinaryExpression: return getIdentifiersFromBinaryExpression(node).filter(isArrayIndex).map((identifier) => ({
1909
+ messageId: "default",
1910
+ node: identifier
1911
+ }));
1912
+ case AST_NODE_TYPES.ConditionalExpression: return [...checkKeyExpression(node.consequent), ...checkKeyExpression(node.alternate)];
1913
+ case AST_NODE_TYPES.LogicalExpression: return [...checkKeyExpression(node.left), ...checkKeyExpression(node.right)];
1914
+ case AST_NODE_TYPES.CallExpression: {
1915
+ const callee = Extract.unwrap(node.callee);
1916
+ if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "toString" && isArrayIndex(callee.object)) return [{
1865
1917
  messageId: "default",
1866
- node
1918
+ node: callee.object
1867
1919
  }];
1868
- return [];
1869
- case AST_NODE_TYPES.TemplateLiteral:
1870
- case AST_NODE_TYPES.BinaryExpression: {
1871
- const descriptors = [];
1872
- const expressions = node.type === AST_NODE_TYPES.TemplateLiteral ? node.expressions : getIdentifiersFromBinaryExpression(node);
1873
- for (const expression of expressions) if (isArrayIndex(expression)) descriptors.push({
1920
+ const argument = node.arguments.at(0);
1921
+ if (callee.type === AST_NODE_TYPES.Identifier && COERCION_FUNCTIONS.has(callee.name) && argument != null && isArrayIndex(argument)) return [{
1874
1922
  messageId: "default",
1875
- node: expression
1876
- });
1877
- return descriptors;
1878
- }
1879
- case AST_NODE_TYPES.CallExpression: {
1880
- const callee = Extract.unwrap(node.callee);
1881
- switch (true) {
1882
- case callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "toString" && isArrayIndex(callee.object): return [{
1883
- messageId: "default",
1884
- node: callee.object
1885
- }];
1886
- case callee.type === AST_NODE_TYPES.Identifier && callee.name === "String" && node.arguments[0] != null && isArrayIndex(node.arguments[0]): return [{
1887
- messageId: "default",
1888
- node: node.arguments[0]
1889
- }];
1890
- }
1923
+ node: argument
1924
+ }];
1925
+ return [];
1891
1926
  }
1927
+ default: return [];
1892
1928
  }
1893
- return [];
1894
1929
  }
1895
1930
  return {
1896
1931
  CallExpression(node) {
1897
- indexParamNames.push(getMapIndexParamName(context, node));
1898
- if (node.arguments.length === 0) return;
1899
1932
  if (!isCreateOrCloneElementCall(node)) return;
1900
1933
  const [, props] = node.arguments;
1901
1934
  if (props?.type !== AST_NODE_TYPES.ObjectExpression) return;
1902
- for (const prop of props.properties) {
1903
- if (!isMatching({ key: { name: "key" } })(prop)) continue;
1904
- if (!("value" in prop)) continue;
1905
- for (const descriptor of getReportDescriptors(prop.value)) report$2(context)(descriptor);
1935
+ for (const property of props.properties) {
1936
+ const value = getKeyPropValue(property);
1937
+ if (value == null) continue;
1938
+ checkKeyExpression(value).forEach(report$2(context));
1906
1939
  }
1907
1940
  },
1908
- "CallExpression:exit"() {
1909
- indexParamNames.pop();
1910
- },
1911
- JSXAttribute(node) {
1912
- if (node.name.name !== "key") return;
1913
- if (indexParamNames.length === 0) return;
1941
+ "JSXAttribute[name.name='key']"(node) {
1914
1942
  if (node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) return;
1915
- for (const descriptor of getReportDescriptors(node.value.expression)) report$2(context)(descriptor);
1943
+ checkKeyExpression(node.value.expression).forEach(report$2(context));
1916
1944
  }
1917
1945
  };
1918
1946
  }
@@ -2819,6 +2847,16 @@ function getNestedReturnStatements$1(node) {
2819
2847
  //#endregion
2820
2848
  //#region src/rules/no-missing-key/no-missing-key.ts
2821
2849
  const RULE_NAME$24 = "no-missing-key";
2850
+ /**
2851
+ * Iterator-like methods whose callback's return value becomes an item in a rendered list,
2852
+ * mapped to the position of the callback in the call's argument list.
2853
+ * `from` covers `Array.from(iterable, mapFn)`.
2854
+ */
2855
+ const ITERATOR_METHOD_CALLBACK_POSITIONS = new Map([
2856
+ ["flatMap", 0],
2857
+ ["from", 1],
2858
+ ["map", 0]
2859
+ ]);
2822
2860
  var no_missing_key_default = createRule({
2823
2861
  meta: {
2824
2862
  type: "problem",
@@ -2833,72 +2871,74 @@ var no_missing_key_default = createRule({
2833
2871
  create: create$24,
2834
2872
  defaultOptions: []
2835
2873
  });
2874
+ /**
2875
+ * Gets the mapping callback of an iterator-like call expression (e.g. `.map(cb)`, `.flatMap(cb)`, `Array.from(it, cb)`).
2876
+ * @param node The call expression to inspect.
2877
+ * @returns The callback function node, or `null` if the call is not an iterator-like call with a function callback.
2878
+ */
2879
+ function getIteratorCallback(node) {
2880
+ const callee = Extract.unwrap(node.callee);
2881
+ if (callee.type !== AST_NODE_TYPES.MemberExpression) return null;
2882
+ if (callee.property.type !== AST_NODE_TYPES.Identifier) return null;
2883
+ const position = ITERATOR_METHOD_CALLBACK_POSITIONS.get(callee.property.name);
2884
+ if (position == null) return null;
2885
+ const callback = node.arguments[position];
2886
+ if (callback == null || !Check.isFunction(callback)) return null;
2887
+ return callback;
2888
+ }
2836
2889
  function create$24(context) {
2837
- let inChildrenToArray = false;
2838
- function check(node) {
2839
- if (node.type === AST_NODE_TYPES.JSXElement) return !hasAttribute(context, node, "key") ? {
2840
- messageId: "default",
2841
- node
2842
- } : null;
2843
- if (node.type === AST_NODE_TYPES.JSXFragment) return {
2844
- messageId: "unexpectedFragmentSyntax",
2845
- node
2846
- };
2847
- return null;
2848
- }
2849
- function checkExpr(node) {
2890
+ let childrenToArrayDepth = 0;
2891
+ /**
2892
+ * Checks an expression that is rendered as an item of a list,
2893
+ * recursing into the branches it may evaluate to.
2894
+ * @param node The expression to check.
2895
+ * @returns The report descriptors for the violations found.
2896
+ */
2897
+ function checkItemExpression(node) {
2850
2898
  switch (node.type) {
2851
- case AST_NODE_TYPES.ConditionalExpression: return [...checkExpr(node.consequent), ...checkExpr(node.alternate)];
2852
- case AST_NODE_TYPES.LogicalExpression: return [...checkExpr(node.left), ...checkExpr(node.right)];
2853
- case AST_NODE_TYPES.JSXElement:
2854
- case AST_NODE_TYPES.JSXFragment: {
2855
- const desc = check(node);
2856
- return desc == null ? [] : [desc];
2857
- }
2899
+ case AST_NODE_TYPES.JSXElement: return hasAttribute(context, node, "key") ? [] : [{
2900
+ messageId: "default",
2901
+ node
2902
+ }];
2903
+ case AST_NODE_TYPES.JSXFragment: return [{
2904
+ messageId: "unexpectedFragmentSyntax",
2905
+ node
2906
+ }];
2907
+ case AST_NODE_TYPES.ConditionalExpression: return [...checkItemExpression(node.consequent), ...checkItemExpression(node.alternate)];
2908
+ case AST_NODE_TYPES.LogicalExpression: return [...checkItemExpression(node.left), ...checkItemExpression(node.right)];
2858
2909
  default: return [];
2859
2910
  }
2860
2911
  }
2861
- function checkBlock(node) {
2862
- const descriptors = [];
2863
- for (const stmt of getNestedReturnStatements$1(node)) {
2864
- if (stmt.argument == null) continue;
2865
- descriptors.push(...checkExpr(stmt.argument));
2866
- }
2867
- return descriptors;
2912
+ /**
2913
+ * Checks every value an iterator callback may produce as a list item,
2914
+ * whether from an expression body or from return statements in a block body.
2915
+ * @param node The callback function to check.
2916
+ * @returns The report descriptors for the violations found.
2917
+ */
2918
+ function checkIteratorCallback(node) {
2919
+ if (node.body.type !== AST_NODE_TYPES.BlockStatement) return checkItemExpression(node.body);
2920
+ return getNestedReturnStatements$1(node.body).flatMap((stmt) => stmt.argument == null ? [] : checkItemExpression(stmt.argument));
2868
2921
  }
2869
2922
  return {
2870
2923
  ArrayExpression(node) {
2871
- if (inChildrenToArray) return;
2872
- const elements = node.elements.filter(is(AST_NODE_TYPES.JSXElement));
2873
- if (elements.length === 0) return;
2874
- for (const el of elements) if (!hasAttribute(context, el, "key")) context.report({
2875
- messageId: "default",
2876
- node: el
2877
- });
2924
+ if (childrenToArrayDepth > 0) return;
2925
+ for (const element of node.elements) {
2926
+ if (element == null || element.type === AST_NODE_TYPES.SpreadElement) continue;
2927
+ checkItemExpression(element).forEach(report(context));
2928
+ }
2878
2929
  },
2879
2930
  CallExpression(node) {
2880
- inChildrenToArray ||= core.isChildrenToArrayCall(context, node);
2881
- if (inChildrenToArray) return;
2882
- const callee = Extract.unwrap(node.callee);
2883
- if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
2884
- if (callee.property.type !== AST_NODE_TYPES.Identifier) return;
2885
- const name = callee.property.name;
2886
- const idx = name === "from" ? 1 : name === "map" ? 0 : -1;
2887
- if (idx < 0) return;
2888
- const cb = node.arguments[idx];
2889
- if (!Check.isFunction(cb)) return;
2890
- if (cb.body.type === AST_NODE_TYPES.BlockStatement) checkBlock(cb.body).forEach(report(context));
2891
- else checkExpr(cb.body).forEach(report(context));
2931
+ if (core.isChildrenToArrayCall(context, node)) {
2932
+ childrenToArrayDepth += 1;
2933
+ return;
2934
+ }
2935
+ if (childrenToArrayDepth > 0) return;
2936
+ const callback = getIteratorCallback(node);
2937
+ if (callback == null) return;
2938
+ checkIteratorCallback(callback).forEach(report(context));
2892
2939
  },
2893
2940
  "CallExpression:exit"(node) {
2894
- if (core.isChildrenToArrayCall(context, node)) inChildrenToArray = false;
2895
- },
2896
- JSXFragment(node) {
2897
- if (inChildrenToArray) return;
2898
- if (node.parent.type === AST_NODE_TYPES.ArrayExpression) context.report({
2899
- messageId: "unexpectedFragmentSyntax",
2900
- node
2901
- });
2941
+ if (core.isChildrenToArrayCall(context, node)) childrenToArrayDepth -= 1;
2902
2942
  }
2903
2943
  };
2904
2944
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-x",
3
- "version": "5.8.17",
3
+ "version": "5.8.19",
4
4
  "description": "A set of composable ESLint rules for libraries and frameworks that use React as a UI runtime.",
5
5
  "keywords": [
6
6
  "react",
@@ -45,12 +45,12 @@
45
45
  "string-ts": "^2.3.1",
46
46
  "ts-api-utils": "^2.5.0",
47
47
  "ts-pattern": "^5.9.0",
48
- "@eslint-react/eslint": "5.8.17",
49
- "@eslint-react/core": "5.8.17",
50
- "@eslint-react/jsx": "5.8.17",
51
- "@eslint-react/shared": "5.8.17",
52
- "@eslint-react/var": "5.8.17",
53
- "@eslint-react/ast": "5.8.17"
48
+ "@eslint-react/ast": "5.8.19",
49
+ "@eslint-react/core": "5.8.19",
50
+ "@eslint-react/jsx": "5.8.19",
51
+ "@eslint-react/shared": "5.8.19",
52
+ "@eslint-react/var": "5.8.19",
53
+ "@eslint-react/eslint": "5.8.19"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/react": "^19.2.17",
@@ -67,7 +67,7 @@
67
67
  "@local/eff": "0.0.0"
68
68
  },
69
69
  "peerDependencies": {
70
- "eslint": "^10.3.0",
70
+ "eslint": "*",
71
71
  "typescript": "*"
72
72
  },
73
73
  "engines": {