@sarj/eslint-plugin 18.0.3 → 18.0.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.
package/dist/index.cjs CHANGED
@@ -893,6 +893,7 @@ function stableValueBinding(variable, acceptRead) {
893
893
 
894
894
  // src/rules/_react-prop-bindings.ts
895
895
  var VIEWS = /* @__PURE__ */ new WeakMap();
896
+ var PARAMETER_BINDINGS = /* @__PURE__ */ new WeakMap();
896
897
  function renderFunction(node) {
897
898
  let parent = node.parent;
898
899
  while (parent !== void 0 && parent !== null) {
@@ -909,6 +910,19 @@ function renderFunction(node) {
909
910
  return null;
910
911
  }
911
912
  function firstParameterBindings(source, node) {
913
+ let functions = PARAMETER_BINDINGS.get(source);
914
+ if (functions === void 0) {
915
+ functions = /* @__PURE__ */ new WeakMap();
916
+ PARAMETER_BINDINGS.set(source, functions);
917
+ }
918
+ let bindings = functions.get(node);
919
+ if (bindings === void 0) {
920
+ bindings = collectParameterBindings(source, node);
921
+ functions.set(node, bindings);
922
+ }
923
+ return bindings;
924
+ }
925
+ function collectParameterBindings(source, node) {
912
926
  const parameter = node.params[0];
913
927
  const identifiers = [];
914
928
  if (parameter?.type === import_utils8.AST_NODE_TYPES.Identifier)
@@ -1015,7 +1029,7 @@ var PREFER_SERVER_DATA_PREPARATION_DOCUMENTATION = {
1015
1029
  category: "architecture",
1016
1030
  limitations: [
1017
1031
  "Advisory only. Local syntax cannot establish whether a caller is a Server Component, whether props change interactively or whether server preparation reduces the total cost. This rule is a review lead, never proof that code can be moved to the server. No automatic fix is provided.",
1018
- "Requires an explicit use client directive and parser-owned TypeScript services resolving map/filter/flatMap to the standard Array or ReadonlyArray library signature. The containing view must have an independent React useState/useReducer call or an intrinsic DOM onClick/onChange/onInput/onSubmit handler. Noninteractive boundaries remain owned by no-unnecessary-use-client; custom hooks and other indirect client requirements are unsupported.",
1032
+ "Requires an explicit use client directive and parser-owned TypeScript services resolving map/filter/flatMap to the standard Array or ReadonlyArray library signature. The receiver beneath erased wrappers must itself have an Array, ReadonlyArray or tuple type after nullish narrowing; casts from opaque or custom collections do not establish this. Subclass and structural collection types outside those native array types are excluded. The containing view must have an independent React useState/useReducer call or a native callable type on an intrinsic DOM onClick/onChange/onInput/onSubmit handler. Nullish, noncallable and opaque handler types do not establish interaction. Noninteractive boundaries remain owned by no-unnecessary-use-client; custom hooks and indirect client requirements are unsupported.",
1019
1033
  "Every runtime reference to the source parameter binding must be a supported native data projection or a field-only JSX display loop. Inputs also needed for editing, whole-record forwarding, lookup, mutation, other fields or opaque helpers are excluded. Only direct first-parameter paths and synchronous single-expression callbacks are supported. Whole-row identity maps and constant maps are excluded; supported projections must read a fixed input field.",
1020
1034
  "JSX-rendering callbacks are never reported as preparation findings. Callback calls, captured values, destructuring defaults/rest/nesting, async callbacks, event handlers, effects, memo callbacks, opaque helpers, dynamic/optional paths, custom collection signatures, missing type services, generated source and tests are excluded. First-parameter provenance is syntactic; annotations cannot prove absence of runtime getters or patched array methods. Analysis does not claim callback purity or preserve semantics of a hypothetical move.",
1021
1035
  "Reports the direct source projection once; chained operations with a call receiver are not independently reported. A static subprojection may still be prepared by its producer while a union with pending local edits remains in the client."
@@ -1063,7 +1077,27 @@ var prefer_server_data_preparation_default = createRule({
1063
1077
  const services = getTypeServices(context);
1064
1078
  const checker = services.program.getTypeChecker();
1065
1079
  const interactions = /* @__PURE__ */ new WeakMap();
1080
+ const projectedInputs = /* @__PURE__ */ new WeakMap();
1081
+ const callable = (node) => {
1082
+ const type = checker.getTypeAtLocation(
1083
+ services.esTreeNodeToTSNodeMap.get(unwrapExpression(node))
1084
+ );
1085
+ const members = type.isUnion() ? type.types : [type];
1086
+ return members.some((member3) => member3.getCallSignatures().length > 0);
1087
+ };
1066
1088
  const nativeArray = (node) => {
1089
+ const callee = unwrapExpression(node.callee);
1090
+ if (callee.type !== import_utils9.AST_NODE_TYPES.MemberExpression) return false;
1091
+ const receiver = checker.getNonNullableType(
1092
+ checker.getTypeAtLocation(
1093
+ services.esTreeNodeToTSNodeMap.get(unwrapExpression(callee.object))
1094
+ )
1095
+ );
1096
+ const members = receiver.isUnion() ? receiver.types : [receiver];
1097
+ if (!members.every(
1098
+ (type) => checker.isArrayType(type) || checker.isTupleType(type)
1099
+ ))
1100
+ return false;
1067
1101
  const declaration = checker.getResolvedSignature(
1068
1102
  services.esTreeNodeToTSNodeMap.get(node)
1069
1103
  )?.declaration;
@@ -1084,14 +1118,24 @@ var prefer_server_data_preparation_default = createRule({
1084
1118
  context.sourceCode,
1085
1119
  callee.object,
1086
1120
  firstParameterBindings(context.sourceCode, component)
1087
- ) || !projection(context.sourceCode, callback) || !nativeArray(node) || !onlyProjectedInput(context.sourceCode, callee.object, nativeArray))
1121
+ ) || !projection(context.sourceCode, callback))
1088
1122
  return;
1089
1123
  let interactive = interactions.get(component);
1090
1124
  if (interactive === void 0) {
1091
- interactive = clientInteraction(context.sourceCode, component.body);
1125
+ interactive = clientInteraction(
1126
+ context.sourceCode,
1127
+ component.body,
1128
+ callable
1129
+ );
1092
1130
  interactions.set(component, interactive);
1093
1131
  }
1094
- if (!interactive) return;
1132
+ if (!interactive || !onlyProjectedInput(
1133
+ context.sourceCode,
1134
+ callee.object,
1135
+ nativeArray,
1136
+ projectedInputs
1137
+ ))
1138
+ return;
1095
1139
  context.report({ node, messageId: "prepareData" });
1096
1140
  }
1097
1141
  };
@@ -1161,13 +1205,16 @@ function displayProjection(node, inspectChild) {
1161
1205
  return node.expression.type === import_utils9.AST_NODE_TYPES.JSXEmptyExpression || inspectChild(node.expression);
1162
1206
  return false;
1163
1207
  }
1164
- function onlyProjectedInput(source, receiver, nativeArray) {
1208
+ function onlyProjectedInput(source, receiver, nativeArray, cache) {
1165
1209
  let root = unwrapExpression(receiver);
1166
1210
  while (root.type === import_utils9.AST_NODE_TYPES.MemberExpression)
1167
1211
  root = unwrapExpression(root.object);
1168
1212
  if (root.type !== import_utils9.AST_NODE_TYPES.Identifier) return false;
1169
1213
  const variable = import_utils9.ASTUtils.findVariable(source.getScope(root), root.name);
1170
- return variable !== null && variable.references.every((reference) => {
1214
+ if (variable === null) return false;
1215
+ const cached = cache.get(variable);
1216
+ if (cached !== void 0) return cached;
1217
+ const projected = variable.references.every((reference) => {
1171
1218
  if (typeOnlyReference(reference)) return true;
1172
1219
  if (!reference.isRead()) return false;
1173
1220
  let access = outerExpression(reference.identifier);
@@ -1184,19 +1231,21 @@ function onlyProjectedInput(source, receiver, nativeArray) {
1184
1231
  const callback = call.arguments[0];
1185
1232
  return (callback?.type === import_utils9.AST_NODE_TYPES.ArrowFunctionExpression || callback?.type === import_utils9.AST_NODE_TYPES.FunctionExpression) && (projection(source, callback) || projection(source, callback, "view")) && call.callee === access;
1186
1233
  });
1234
+ cache.set(variable, projected);
1235
+ return projected;
1187
1236
  }
1188
- function clientInteraction(source, node) {
1237
+ function clientInteraction(source, node, callable) {
1189
1238
  if (node.type === import_utils9.AST_NODE_TYPES.ArrowFunctionExpression || node.type === import_utils9.AST_NODE_TYPES.FunctionExpression || node.type === import_utils9.AST_NODE_TYPES.FunctionDeclaration)
1190
1239
  return false;
1191
1240
  if (node.type === import_utils9.AST_NODE_TYPES.CallExpression && (reactHook(source, node.callee, "useState") || reactHook(source, node.callee, "useReducer")))
1192
1241
  return true;
1193
1242
  if (node.type === import_utils9.AST_NODE_TYPES.JSXOpeningElement && node.name.type === import_utils9.AST_NODE_TYPES.JSXIdentifier && /^[a-z]/u.test(node.name.name) && node.attributes.some(
1194
- (attribute) => attribute.type === import_utils9.AST_NODE_TYPES.JSXAttribute && attribute.name.type === import_utils9.AST_NODE_TYPES.JSXIdentifier && DOM_EVENTS.has(attribute.name.name) && attribute.value !== null
1243
+ (attribute) => attribute.type === import_utils9.AST_NODE_TYPES.JSXAttribute && attribute.name.type === import_utils9.AST_NODE_TYPES.JSXIdentifier && DOM_EVENTS.has(attribute.name.name) && attribute.value?.type === import_utils9.AST_NODE_TYPES.JSXExpressionContainer && attribute.value.expression.type !== import_utils9.AST_NODE_TYPES.JSXEmptyExpression && callable(attribute.value.expression)
1195
1244
  ))
1196
1245
  return true;
1197
1246
  return forEachOwnAstChild(
1198
1247
  node,
1199
- (child) => clientInteraction(source, child),
1248
+ (child) => clientInteraction(source, child, callable),
1200
1249
  (key) => key !== "tokens" && key !== "comments"
1201
1250
  );
1202
1251
  }
@@ -1360,9 +1409,8 @@ var CONFIG2 = {
1360
1409
  path: "tsconfig.json",
1361
1410
  source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler","jsx":"preserve"},"include":["src/**/*"]}'
1362
1411
  };
1363
- var OPAQUE_TYPES = import_typescript4.default.TypeFlags.Any | import_typescript4.default.TypeFlags.Unknown | import_typescript4.default.TypeFlags.Never | import_typescript4.default.TypeFlags.TypeParameter | import_typescript4.default.TypeFlags.Conditional | import_typescript4.default.TypeFlags.IndexedAccess | import_typescript4.default.TypeFlags.Substitution;
1412
+ var PRIMITIVE_TYPES = import_typescript4.default.TypeFlags.StringLike | import_typescript4.default.TypeFlags.NumberLike | import_typescript4.default.TypeFlags.BigIntLike | import_typescript4.default.TypeFlags.BooleanLike | import_typescript4.default.TypeFlags.ESSymbolLike | import_typescript4.default.TypeFlags.Null | import_typescript4.default.TypeFlags.Undefined;
1364
1413
  var MAX_TYPE_DEPTH = 16;
1365
- var FUNCTION_MEMBERS = ["apply", "bind", "call"];
1366
1414
  var NO_READONLY_PROP_STATE_DOCUMENTATION = {
1367
1415
  defaultLevel: "warning",
1368
1416
  summary: "Review props copied into state without a usable updater.",
@@ -1372,11 +1420,12 @@ var NO_READONLY_PROP_STATE_DOCUMENTATION = {
1372
1420
  limitations: [
1373
1421
  "Advisory only: initial snapshots can be intentional. There is no automatic fix because replacing state with current props changes how later updates behave.",
1374
1422
  "Recognizes direct React useState imports and default/namespace member calls, including renamed imports, through lexical binding identity. Checks a direct array destructure in a function containing its own JSX and a direct first-parameter property path, or a zero-argument synchronous lazy initializer returning that path.",
1375
- "Eager initializers require parser-owned TypeScript services and a non-callable type without Any, unknown or unresolved generics. Callable unions, constructors and legacy function-like types exposing apply/bind/call are excluded because React executes function arguments as lazy initializers. Explicit zero-argument lazy callbacks returning the prop directly do not require type information. Type annotations cannot prove runtime identity under opaque casts; lifecycle changes stay manual.",
1423
+ "Eager initializers require parser-owned TypeScript services and primitive types, including unions composed entirely of primitives. Object and void types can hide callable values through structural assignment or ignored return values, so eager nonprimitive values, Any, unknown and unresolved generics are excluded. React executes function arguments as lazy initializers. Explicit zero-argument lazy callbacks returning a prop directly remain supported for all value types without type services. Opaque casts cannot prove runtime identity; lifecycle changes stay manual.",
1376
1424
  "An updater read, escape, reassignment or nontrivial destructuring excludes the finding. Unused state values remain owned by unused-variable checks. Opaque initialization, property aliases, optional/dynamic paths, rest/default bindings, dynamic eval, non-JSX hooks, generated source and tests are outside scope. This does not ban interactive useState or replace the upstream set-state-in-effect rule."
1377
1425
  ],
1378
1426
  references: [
1379
1427
  "https://react.dev/learn/choosing-the-state-structure",
1428
+ "https://react.dev/reference/react/useState",
1380
1429
  "https://react.dev/reference/eslint-plugin-react-hooks/lints/set-state-in-effect"
1381
1430
  ],
1382
1431
  examples: [
@@ -1441,7 +1490,7 @@ var no_readonly_prop_state_default = createRule({
1441
1490
  return;
1442
1491
  const services = getTypeServices(context);
1443
1492
  const type = services.program.getTypeChecker().getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(initial));
1444
- if (!isNonCallableType(type)) return;
1493
+ if (!isPrimitiveType(type)) return;
1445
1494
  }
1446
1495
  context.report({ node, messageId: "readonlyPropState" });
1447
1496
  }
@@ -1481,10 +1530,11 @@ function initializerValue(node) {
1481
1530
  const [statement] = node.body.body;
1482
1531
  return node.body.body.length === 1 && statement?.type === import_utils12.AST_NODE_TYPES.ReturnStatement ? statement.argument : null;
1483
1532
  }
1484
- function isNonCallableType(type, depth = 0) {
1485
- if (depth >= MAX_TYPE_DEPTH || (type.flags & OPAQUE_TYPES) !== 0 || type.getCallSignatures().length !== 0 || type.getConstructSignatures().length !== 0 || FUNCTION_MEMBERS.every((member3) => type.getProperty(member3) !== void 0))
1486
- return false;
1487
- return !type.isUnionOrIntersection() || type.types.every((member3) => isNonCallableType(member3, depth + 1));
1533
+ function isPrimitiveType(type, depth = 0) {
1534
+ if (depth >= MAX_TYPE_DEPTH) return false;
1535
+ if (type.isIntersection())
1536
+ return type.getCallSignatures().length === 0 && type.getConstructSignatures().length === 0 && type.types.some((member3) => isPrimitiveType(member3, depth + 1));
1537
+ return type.isUnion() ? type.types.every((member3) => isPrimitiveType(member3, depth + 1)) : (type.flags & PRIMITIVE_TYPES) !== 0;
1488
1538
  }
1489
1539
 
1490
1540
  // src/rules/no-runtime-typeof.ts
@@ -5539,7 +5589,7 @@ function isSearchParamReceiver(call, services) {
5539
5589
  // package.json
5540
5590
  var package_default = {
5541
5591
  name: "@sarj/eslint-plugin",
5542
- version: "18.0.3",
5592
+ version: "18.0.4",
5543
5593
  packageManager: "npm@12.2.1",
5544
5594
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
5545
5595
  type: "module",
@@ -28703,7 +28753,7 @@ var import_utils165 = require("@typescript-eslint/utils");
28703
28753
 
28704
28754
  // src/rules/_existing-type-alias-fix.ts
28705
28755
  var import_utils164 = require("@typescript-eslint/utils");
28706
- var PRIMITIVE_TYPES = /* @__PURE__ */ new Set([
28756
+ var PRIMITIVE_TYPES2 = /* @__PURE__ */ new Set([
28707
28757
  "TSStringKeyword",
28708
28758
  "TSNumberKeyword",
28709
28759
  "TSBooleanKeyword",
@@ -28760,7 +28810,7 @@ function tokenSignature(sourceCode, node) {
28760
28810
  );
28761
28811
  }
28762
28812
  function closedType(node) {
28763
- if (PRIMITIVE_TYPES.has(node.type)) return true;
28813
+ if (PRIMITIVE_TYPES2.has(node.type)) return true;
28764
28814
  switch (node.type) {
28765
28815
  case import_utils164.AST_NODE_TYPES.TSLiteralType:
28766
28816
  return node.literal.type === import_utils164.AST_NODE_TYPES.Literal || node.literal.type === import_utils164.AST_NODE_TYPES.UnaryExpression && node.literal.operator === "-" && node.literal.argument.type === import_utils164.AST_NODE_TYPES.Literal;
@@ -30748,8 +30798,8 @@ var memberValidationPolarity = (test, access) => {
30748
30798
  }
30749
30799
  if (test.type === import_utils173.AST_NODE_TYPES.BinaryExpression) {
30750
30800
  const isMatchingTypeof = (node) => node.type === import_utils173.AST_NODE_TYPES.UnaryExpression && node.operator === "typeof" && isSamePlainMember(node.argument, access);
30751
- const isPrimitiveType = (node) => node.type === import_utils173.AST_NODE_TYPES.Literal && typeof node.value === "string" && PRIMITIVE_TYPEOF_RESULTS.has(node.value);
30752
- if (!(isMatchingTypeof(test.left) && isPrimitiveType(test.right) || isMatchingTypeof(test.right) && isPrimitiveType(test.left))) {
30801
+ const isPrimitiveType2 = (node) => node.type === import_utils173.AST_NODE_TYPES.Literal && typeof node.value === "string" && PRIMITIVE_TYPEOF_RESULTS.has(node.value);
30802
+ if (!(isMatchingTypeof(test.left) && isPrimitiveType2(test.right) || isMatchingTypeof(test.right) && isPrimitiveType2(test.left))) {
30753
30803
  return null;
30754
30804
  }
30755
30805
  if (test.operator === "===" || test.operator === "==") {
package/dist/index.js CHANGED
@@ -848,6 +848,7 @@ function stableValueBinding(variable, acceptRead) {
848
848
 
849
849
  // src/rules/_react-prop-bindings.ts
850
850
  var VIEWS = /* @__PURE__ */ new WeakMap();
851
+ var PARAMETER_BINDINGS = /* @__PURE__ */ new WeakMap();
851
852
  function renderFunction(node) {
852
853
  let parent = node.parent;
853
854
  while (parent !== void 0 && parent !== null) {
@@ -864,6 +865,19 @@ function renderFunction(node) {
864
865
  return null;
865
866
  }
866
867
  function firstParameterBindings(source, node) {
868
+ let functions = PARAMETER_BINDINGS.get(source);
869
+ if (functions === void 0) {
870
+ functions = /* @__PURE__ */ new WeakMap();
871
+ PARAMETER_BINDINGS.set(source, functions);
872
+ }
873
+ let bindings = functions.get(node);
874
+ if (bindings === void 0) {
875
+ bindings = collectParameterBindings(source, node);
876
+ functions.set(node, bindings);
877
+ }
878
+ return bindings;
879
+ }
880
+ function collectParameterBindings(source, node) {
867
881
  const parameter = node.params[0];
868
882
  const identifiers = [];
869
883
  if (parameter?.type === AST_NODE_TYPES7.Identifier)
@@ -970,7 +984,7 @@ var PREFER_SERVER_DATA_PREPARATION_DOCUMENTATION = {
970
984
  category: "architecture",
971
985
  limitations: [
972
986
  "Advisory only. Local syntax cannot establish whether a caller is a Server Component, whether props change interactively or whether server preparation reduces the total cost. This rule is a review lead, never proof that code can be moved to the server. No automatic fix is provided.",
973
- "Requires an explicit use client directive and parser-owned TypeScript services resolving map/filter/flatMap to the standard Array or ReadonlyArray library signature. The containing view must have an independent React useState/useReducer call or an intrinsic DOM onClick/onChange/onInput/onSubmit handler. Noninteractive boundaries remain owned by no-unnecessary-use-client; custom hooks and other indirect client requirements are unsupported.",
987
+ "Requires an explicit use client directive and parser-owned TypeScript services resolving map/filter/flatMap to the standard Array or ReadonlyArray library signature. The receiver beneath erased wrappers must itself have an Array, ReadonlyArray or tuple type after nullish narrowing; casts from opaque or custom collections do not establish this. Subclass and structural collection types outside those native array types are excluded. The containing view must have an independent React useState/useReducer call or a native callable type on an intrinsic DOM onClick/onChange/onInput/onSubmit handler. Nullish, noncallable and opaque handler types do not establish interaction. Noninteractive boundaries remain owned by no-unnecessary-use-client; custom hooks and indirect client requirements are unsupported.",
974
988
  "Every runtime reference to the source parameter binding must be a supported native data projection or a field-only JSX display loop. Inputs also needed for editing, whole-record forwarding, lookup, mutation, other fields or opaque helpers are excluded. Only direct first-parameter paths and synchronous single-expression callbacks are supported. Whole-row identity maps and constant maps are excluded; supported projections must read a fixed input field.",
975
989
  "JSX-rendering callbacks are never reported as preparation findings. Callback calls, captured values, destructuring defaults/rest/nesting, async callbacks, event handlers, effects, memo callbacks, opaque helpers, dynamic/optional paths, custom collection signatures, missing type services, generated source and tests are excluded. First-parameter provenance is syntactic; annotations cannot prove absence of runtime getters or patched array methods. Analysis does not claim callback purity or preserve semantics of a hypothetical move.",
976
990
  "Reports the direct source projection once; chained operations with a call receiver are not independently reported. A static subprojection may still be prepared by its producer while a union with pending local edits remains in the client."
@@ -1018,7 +1032,27 @@ var prefer_server_data_preparation_default = createRule({
1018
1032
  const services = getTypeServices(context);
1019
1033
  const checker = services.program.getTypeChecker();
1020
1034
  const interactions = /* @__PURE__ */ new WeakMap();
1035
+ const projectedInputs = /* @__PURE__ */ new WeakMap();
1036
+ const callable = (node) => {
1037
+ const type = checker.getTypeAtLocation(
1038
+ services.esTreeNodeToTSNodeMap.get(unwrapExpression(node))
1039
+ );
1040
+ const members = type.isUnion() ? type.types : [type];
1041
+ return members.some((member3) => member3.getCallSignatures().length > 0);
1042
+ };
1021
1043
  const nativeArray = (node) => {
1044
+ const callee = unwrapExpression(node.callee);
1045
+ if (callee.type !== AST_NODE_TYPES8.MemberExpression) return false;
1046
+ const receiver = checker.getNonNullableType(
1047
+ checker.getTypeAtLocation(
1048
+ services.esTreeNodeToTSNodeMap.get(unwrapExpression(callee.object))
1049
+ )
1050
+ );
1051
+ const members = receiver.isUnion() ? receiver.types : [receiver];
1052
+ if (!members.every(
1053
+ (type) => checker.isArrayType(type) || checker.isTupleType(type)
1054
+ ))
1055
+ return false;
1022
1056
  const declaration = checker.getResolvedSignature(
1023
1057
  services.esTreeNodeToTSNodeMap.get(node)
1024
1058
  )?.declaration;
@@ -1039,14 +1073,24 @@ var prefer_server_data_preparation_default = createRule({
1039
1073
  context.sourceCode,
1040
1074
  callee.object,
1041
1075
  firstParameterBindings(context.sourceCode, component)
1042
- ) || !projection(context.sourceCode, callback) || !nativeArray(node) || !onlyProjectedInput(context.sourceCode, callee.object, nativeArray))
1076
+ ) || !projection(context.sourceCode, callback))
1043
1077
  return;
1044
1078
  let interactive = interactions.get(component);
1045
1079
  if (interactive === void 0) {
1046
- interactive = clientInteraction(context.sourceCode, component.body);
1080
+ interactive = clientInteraction(
1081
+ context.sourceCode,
1082
+ component.body,
1083
+ callable
1084
+ );
1047
1085
  interactions.set(component, interactive);
1048
1086
  }
1049
- if (!interactive) return;
1087
+ if (!interactive || !onlyProjectedInput(
1088
+ context.sourceCode,
1089
+ callee.object,
1090
+ nativeArray,
1091
+ projectedInputs
1092
+ ))
1093
+ return;
1050
1094
  context.report({ node, messageId: "prepareData" });
1051
1095
  }
1052
1096
  };
@@ -1116,13 +1160,16 @@ function displayProjection(node, inspectChild) {
1116
1160
  return node.expression.type === AST_NODE_TYPES8.JSXEmptyExpression || inspectChild(node.expression);
1117
1161
  return false;
1118
1162
  }
1119
- function onlyProjectedInput(source, receiver, nativeArray) {
1163
+ function onlyProjectedInput(source, receiver, nativeArray, cache) {
1120
1164
  let root = unwrapExpression(receiver);
1121
1165
  while (root.type === AST_NODE_TYPES8.MemberExpression)
1122
1166
  root = unwrapExpression(root.object);
1123
1167
  if (root.type !== AST_NODE_TYPES8.Identifier) return false;
1124
1168
  const variable = ASTUtils4.findVariable(source.getScope(root), root.name);
1125
- return variable !== null && variable.references.every((reference) => {
1169
+ if (variable === null) return false;
1170
+ const cached = cache.get(variable);
1171
+ if (cached !== void 0) return cached;
1172
+ const projected = variable.references.every((reference) => {
1126
1173
  if (typeOnlyReference(reference)) return true;
1127
1174
  if (!reference.isRead()) return false;
1128
1175
  let access = outerExpression(reference.identifier);
@@ -1139,19 +1186,21 @@ function onlyProjectedInput(source, receiver, nativeArray) {
1139
1186
  const callback = call.arguments[0];
1140
1187
  return (callback?.type === AST_NODE_TYPES8.ArrowFunctionExpression || callback?.type === AST_NODE_TYPES8.FunctionExpression) && (projection(source, callback) || projection(source, callback, "view")) && call.callee === access;
1141
1188
  });
1189
+ cache.set(variable, projected);
1190
+ return projected;
1142
1191
  }
1143
- function clientInteraction(source, node) {
1192
+ function clientInteraction(source, node, callable) {
1144
1193
  if (node.type === AST_NODE_TYPES8.ArrowFunctionExpression || node.type === AST_NODE_TYPES8.FunctionExpression || node.type === AST_NODE_TYPES8.FunctionDeclaration)
1145
1194
  return false;
1146
1195
  if (node.type === AST_NODE_TYPES8.CallExpression && (reactHook(source, node.callee, "useState") || reactHook(source, node.callee, "useReducer")))
1147
1196
  return true;
1148
1197
  if (node.type === AST_NODE_TYPES8.JSXOpeningElement && node.name.type === AST_NODE_TYPES8.JSXIdentifier && /^[a-z]/u.test(node.name.name) && node.attributes.some(
1149
- (attribute) => attribute.type === AST_NODE_TYPES8.JSXAttribute && attribute.name.type === AST_NODE_TYPES8.JSXIdentifier && DOM_EVENTS.has(attribute.name.name) && attribute.value !== null
1198
+ (attribute) => attribute.type === AST_NODE_TYPES8.JSXAttribute && attribute.name.type === AST_NODE_TYPES8.JSXIdentifier && DOM_EVENTS.has(attribute.name.name) && attribute.value?.type === AST_NODE_TYPES8.JSXExpressionContainer && attribute.value.expression.type !== AST_NODE_TYPES8.JSXEmptyExpression && callable(attribute.value.expression)
1150
1199
  ))
1151
1200
  return true;
1152
1201
  return forEachOwnAstChild(
1153
1202
  node,
1154
- (child) => clientInteraction(source, child),
1203
+ (child) => clientInteraction(source, child, callable),
1155
1204
  (key) => key !== "tokens" && key !== "comments"
1156
1205
  );
1157
1206
  }
@@ -1315,9 +1364,8 @@ var CONFIG2 = {
1315
1364
  path: "tsconfig.json",
1316
1365
  source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler","jsx":"preserve"},"include":["src/**/*"]}'
1317
1366
  };
1318
- var OPAQUE_TYPES = ts4.TypeFlags.Any | ts4.TypeFlags.Unknown | ts4.TypeFlags.Never | ts4.TypeFlags.TypeParameter | ts4.TypeFlags.Conditional | ts4.TypeFlags.IndexedAccess | ts4.TypeFlags.Substitution;
1367
+ var PRIMITIVE_TYPES = ts4.TypeFlags.StringLike | ts4.TypeFlags.NumberLike | ts4.TypeFlags.BigIntLike | ts4.TypeFlags.BooleanLike | ts4.TypeFlags.ESSymbolLike | ts4.TypeFlags.Null | ts4.TypeFlags.Undefined;
1319
1368
  var MAX_TYPE_DEPTH = 16;
1320
- var FUNCTION_MEMBERS = ["apply", "bind", "call"];
1321
1369
  var NO_READONLY_PROP_STATE_DOCUMENTATION = {
1322
1370
  defaultLevel: "warning",
1323
1371
  summary: "Review props copied into state without a usable updater.",
@@ -1327,11 +1375,12 @@ var NO_READONLY_PROP_STATE_DOCUMENTATION = {
1327
1375
  limitations: [
1328
1376
  "Advisory only: initial snapshots can be intentional. There is no automatic fix because replacing state with current props changes how later updates behave.",
1329
1377
  "Recognizes direct React useState imports and default/namespace member calls, including renamed imports, through lexical binding identity. Checks a direct array destructure in a function containing its own JSX and a direct first-parameter property path, or a zero-argument synchronous lazy initializer returning that path.",
1330
- "Eager initializers require parser-owned TypeScript services and a non-callable type without Any, unknown or unresolved generics. Callable unions, constructors and legacy function-like types exposing apply/bind/call are excluded because React executes function arguments as lazy initializers. Explicit zero-argument lazy callbacks returning the prop directly do not require type information. Type annotations cannot prove runtime identity under opaque casts; lifecycle changes stay manual.",
1378
+ "Eager initializers require parser-owned TypeScript services and primitive types, including unions composed entirely of primitives. Object and void types can hide callable values through structural assignment or ignored return values, so eager nonprimitive values, Any, unknown and unresolved generics are excluded. React executes function arguments as lazy initializers. Explicit zero-argument lazy callbacks returning a prop directly remain supported for all value types without type services. Opaque casts cannot prove runtime identity; lifecycle changes stay manual.",
1331
1379
  "An updater read, escape, reassignment or nontrivial destructuring excludes the finding. Unused state values remain owned by unused-variable checks. Opaque initialization, property aliases, optional/dynamic paths, rest/default bindings, dynamic eval, non-JSX hooks, generated source and tests are outside scope. This does not ban interactive useState or replace the upstream set-state-in-effect rule."
1332
1380
  ],
1333
1381
  references: [
1334
1382
  "https://react.dev/learn/choosing-the-state-structure",
1383
+ "https://react.dev/reference/react/useState",
1335
1384
  "https://react.dev/reference/eslint-plugin-react-hooks/lints/set-state-in-effect"
1336
1385
  ],
1337
1386
  examples: [
@@ -1396,7 +1445,7 @@ var no_readonly_prop_state_default = createRule({
1396
1445
  return;
1397
1446
  const services = getTypeServices(context);
1398
1447
  const type = services.program.getTypeChecker().getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(initial));
1399
- if (!isNonCallableType(type)) return;
1448
+ if (!isPrimitiveType(type)) return;
1400
1449
  }
1401
1450
  context.report({ node, messageId: "readonlyPropState" });
1402
1451
  }
@@ -1436,10 +1485,11 @@ function initializerValue(node) {
1436
1485
  const [statement] = node.body.body;
1437
1486
  return node.body.body.length === 1 && statement?.type === AST_NODE_TYPES11.ReturnStatement ? statement.argument : null;
1438
1487
  }
1439
- function isNonCallableType(type, depth = 0) {
1440
- if (depth >= MAX_TYPE_DEPTH || (type.flags & OPAQUE_TYPES) !== 0 || type.getCallSignatures().length !== 0 || type.getConstructSignatures().length !== 0 || FUNCTION_MEMBERS.every((member3) => type.getProperty(member3) !== void 0))
1441
- return false;
1442
- return !type.isUnionOrIntersection() || type.types.every((member3) => isNonCallableType(member3, depth + 1));
1488
+ function isPrimitiveType(type, depth = 0) {
1489
+ if (depth >= MAX_TYPE_DEPTH) return false;
1490
+ if (type.isIntersection())
1491
+ return type.getCallSignatures().length === 0 && type.getConstructSignatures().length === 0 && type.types.some((member3) => isPrimitiveType(member3, depth + 1));
1492
+ return type.isUnion() ? type.types.every((member3) => isPrimitiveType(member3, depth + 1)) : (type.flags & PRIMITIVE_TYPES) !== 0;
1443
1493
  }
1444
1494
 
1445
1495
  // src/rules/no-runtime-typeof.ts
@@ -5494,7 +5544,7 @@ function isSearchParamReceiver(call, services) {
5494
5544
  // package.json
5495
5545
  var package_default = {
5496
5546
  name: "@sarj/eslint-plugin",
5497
- version: "18.0.3",
5547
+ version: "18.0.4",
5498
5548
  packageManager: "npm@12.2.1",
5499
5549
  description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
5500
5550
  type: "module",
@@ -28689,7 +28739,7 @@ import {
28689
28739
  AST_NODE_TYPES as AST_NODE_TYPES148,
28690
28740
  ASTUtils as ASTUtils96
28691
28741
  } from "@typescript-eslint/utils";
28692
- var PRIMITIVE_TYPES = /* @__PURE__ */ new Set([
28742
+ var PRIMITIVE_TYPES2 = /* @__PURE__ */ new Set([
28693
28743
  "TSStringKeyword",
28694
28744
  "TSNumberKeyword",
28695
28745
  "TSBooleanKeyword",
@@ -28746,7 +28796,7 @@ function tokenSignature(sourceCode, node) {
28746
28796
  );
28747
28797
  }
28748
28798
  function closedType(node) {
28749
- if (PRIMITIVE_TYPES.has(node.type)) return true;
28799
+ if (PRIMITIVE_TYPES2.has(node.type)) return true;
28750
28800
  switch (node.type) {
28751
28801
  case AST_NODE_TYPES148.TSLiteralType:
28752
28802
  return node.literal.type === AST_NODE_TYPES148.Literal || node.literal.type === AST_NODE_TYPES148.UnaryExpression && node.literal.operator === "-" && node.literal.argument.type === AST_NODE_TYPES148.Literal;
@@ -30737,8 +30787,8 @@ var memberValidationPolarity = (test, access) => {
30737
30787
  }
30738
30788
  if (test.type === AST_NODE_TYPES157.BinaryExpression) {
30739
30789
  const isMatchingTypeof = (node) => node.type === AST_NODE_TYPES157.UnaryExpression && node.operator === "typeof" && isSamePlainMember(node.argument, access);
30740
- const isPrimitiveType = (node) => node.type === AST_NODE_TYPES157.Literal && typeof node.value === "string" && PRIMITIVE_TYPEOF_RESULTS.has(node.value);
30741
- if (!(isMatchingTypeof(test.left) && isPrimitiveType(test.right) || isMatchingTypeof(test.right) && isPrimitiveType(test.left))) {
30790
+ const isPrimitiveType2 = (node) => node.type === AST_NODE_TYPES157.Literal && typeof node.value === "string" && PRIMITIVE_TYPEOF_RESULTS.has(node.value);
30791
+ if (!(isMatchingTypeof(test.left) && isPrimitiveType2(test.right) || isMatchingTypeof(test.right) && isPrimitiveType2(test.left))) {
30742
30792
  return null;
30743
30793
  }
30744
30794
  if (test.operator === "===" || test.operator === "==") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sarj/eslint-plugin",
3
- "version": "18.0.3",
3
+ "version": "18.0.4",
4
4
  "packageManager": "npm@12.2.1",
5
5
  "description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
6
6
  "type": "module",