@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 +72 -22
- package/dist/index.js +72 -22
- package/package.json +1 -1
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
|
|
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)
|
|
1121
|
+
) || !projection(context.sourceCode, callback))
|
|
1088
1122
|
return;
|
|
1089
1123
|
let interactive = interactions.get(component);
|
|
1090
1124
|
if (interactive === void 0) {
|
|
1091
|
-
interactive = clientInteraction(
|
|
1125
|
+
interactive = clientInteraction(
|
|
1126
|
+
context.sourceCode,
|
|
1127
|
+
component.body,
|
|
1128
|
+
callable
|
|
1129
|
+
);
|
|
1092
1130
|
interactions.set(component, interactive);
|
|
1093
1131
|
}
|
|
1094
|
-
if (!interactive
|
|
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
|
-
|
|
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 !==
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
|
1485
|
-
if (depth >= MAX_TYPE_DEPTH
|
|
1486
|
-
|
|
1487
|
-
|
|
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.
|
|
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
|
|
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 (
|
|
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
|
|
30752
|
-
if (!(isMatchingTypeof(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
|
|
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)
|
|
1076
|
+
) || !projection(context.sourceCode, callback))
|
|
1043
1077
|
return;
|
|
1044
1078
|
let interactive = interactions.get(component);
|
|
1045
1079
|
if (interactive === void 0) {
|
|
1046
|
-
interactive = clientInteraction(
|
|
1080
|
+
interactive = clientInteraction(
|
|
1081
|
+
context.sourceCode,
|
|
1082
|
+
component.body,
|
|
1083
|
+
callable
|
|
1084
|
+
);
|
|
1047
1085
|
interactions.set(component, interactive);
|
|
1048
1086
|
}
|
|
1049
|
-
if (!interactive
|
|
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
|
-
|
|
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 !==
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
|
1440
|
-
if (depth >= MAX_TYPE_DEPTH
|
|
1441
|
-
|
|
1442
|
-
|
|
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.
|
|
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
|
|
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 (
|
|
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
|
|
30741
|
-
if (!(isMatchingTypeof(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 === "==") {
|