oxlint-plugin-react-doctor 0.8.1-dev.1839566 → 0.8.1-dev.26b4a0c
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.d.ts +2 -2
- package/dist/index.js +437 -87
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -9437,7 +9437,7 @@ declare const EXTERNAL_RULES: readonly [{
|
|
|
9437
9437
|
}, {
|
|
9438
9438
|
readonly key: "react-hooks-js/set-state-in-effect";
|
|
9439
9439
|
readonly source: "react-compiler";
|
|
9440
|
-
readonly severity: "
|
|
9440
|
+
readonly severity: "warn";
|
|
9441
9441
|
}, {
|
|
9442
9442
|
readonly key: "react-hooks-js/globals";
|
|
9443
9443
|
readonly source: "react-compiler";
|
|
@@ -18748,7 +18748,7 @@ declare const RULES: readonly [{
|
|
|
18748
18748
|
}, {
|
|
18749
18749
|
readonly key: "react-hooks-js/set-state-in-effect";
|
|
18750
18750
|
readonly source: "react-compiler";
|
|
18751
|
-
readonly severity: "
|
|
18751
|
+
readonly severity: "warn";
|
|
18752
18752
|
}, {
|
|
18753
18753
|
readonly key: "react-hooks-js/globals";
|
|
18754
18754
|
readonly source: "react-compiler";
|
package/dist/index.js
CHANGED
|
@@ -13961,6 +13961,18 @@ const isNodeReachableWithinFunction = (node, context) => {
|
|
|
13961
13961
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
13962
13962
|
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
13963
13963
|
const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
|
|
13964
|
+
const REPLAY_ENTRY_DROPPING_ARRAY_METHOD_NAMES = new Set([
|
|
13965
|
+
"pop",
|
|
13966
|
+
"shift",
|
|
13967
|
+
"splice",
|
|
13968
|
+
"fill",
|
|
13969
|
+
"copyWithin"
|
|
13970
|
+
]);
|
|
13971
|
+
const REPLAY_ENTRY_DROPPING_COLLECTION_METHOD_NAMES = new Set([
|
|
13972
|
+
"clear",
|
|
13973
|
+
"delete",
|
|
13974
|
+
"set"
|
|
13975
|
+
]);
|
|
13964
13976
|
const REACT_REF_EFFECT_ANALYSIS_CACHE = /* @__PURE__ */ new WeakMap();
|
|
13965
13977
|
const RESOURCE_NOUN_BY_KIND = {
|
|
13966
13978
|
subscribe: "subscription",
|
|
@@ -14001,6 +14013,92 @@ const resolveExpressionKey = (expression, context, visitedSymbolIds = /* @__PURE
|
|
|
14001
14013
|
}
|
|
14002
14014
|
return null;
|
|
14003
14015
|
};
|
|
14016
|
+
const resolveForEachProjection = (expression, context) => {
|
|
14017
|
+
if (!expression) return null;
|
|
14018
|
+
let currentExpression = stripParenExpression(expression);
|
|
14019
|
+
const memberNames = [];
|
|
14020
|
+
while (isNodeOfType(currentExpression, "MemberExpression") && !currentExpression.computed) {
|
|
14021
|
+
if (!isNodeOfType(currentExpression.property, "Identifier")) return null;
|
|
14022
|
+
memberNames.unshift(currentExpression.property.name);
|
|
14023
|
+
currentExpression = stripParenExpression(currentExpression.object);
|
|
14024
|
+
}
|
|
14025
|
+
if (!isNodeOfType(currentExpression, "Identifier")) return null;
|
|
14026
|
+
const symbol = context.scopes.symbolFor(currentExpression);
|
|
14027
|
+
if (!symbol || symbol.kind !== "parameter") return null;
|
|
14028
|
+
let callbackNode = symbol.bindingIdentifier.parent;
|
|
14029
|
+
while (callbackNode && !isFunctionLike$1(callbackNode)) callbackNode = callbackNode.parent;
|
|
14030
|
+
if (!callbackNode || !isFunctionLike$1(callbackNode)) return null;
|
|
14031
|
+
const forEachCall = findEnclosingForEachCall(callbackNode);
|
|
14032
|
+
if (!forEachCall) return null;
|
|
14033
|
+
const forEachCallee = stripParenExpression(forEachCall.callee);
|
|
14034
|
+
if (!isNodeOfType(forEachCallee, "MemberExpression")) return null;
|
|
14035
|
+
const collectionKey = resolveExpressionKey(forEachCallee.object, context);
|
|
14036
|
+
if (!collectionKey) return null;
|
|
14037
|
+
const firstParameter = callbackNode.params[0];
|
|
14038
|
+
const bindingProperty = symbol.bindingIdentifier.parent;
|
|
14039
|
+
const propertyName = isNodeOfType(firstParameter, "ObjectPattern") && isNodeOfType(bindingProperty, "Property") && bindingProperty.parent === firstParameter && bindingProperty.value === symbol.bindingIdentifier ? getStaticPropertyKeyName(bindingProperty) : null;
|
|
14040
|
+
const parameterProjection = firstParameter === symbol.bindingIdentifier ? "value" : propertyName;
|
|
14041
|
+
if (!parameterProjection) return null;
|
|
14042
|
+
return {
|
|
14043
|
+
collectionKey,
|
|
14044
|
+
projectionKey: [parameterProjection, ...memberNames].join(".")
|
|
14045
|
+
};
|
|
14046
|
+
};
|
|
14047
|
+
const resolveForEachProjectionKey = (expression, context) => {
|
|
14048
|
+
const projection = resolveForEachProjection(expression, context);
|
|
14049
|
+
return projection ? `forEach:${projection.collectionKey}:${projection.projectionKey}` : null;
|
|
14050
|
+
};
|
|
14051
|
+
const resolveResourceIdentityKey = (expression, context) => resolveForEachProjectionKey(expression, context) ?? resolveExpressionKey(expression, context);
|
|
14052
|
+
const resolveEventListenerCaptureIdentityKey = (optionsNode, context, allowOpaqueOptionsIdentity) => {
|
|
14053
|
+
const capture = resolveEventListenerCapture(optionsNode, { allowIndeterminateEntries: true });
|
|
14054
|
+
if (capture !== null) return `capture:${String(capture)}`;
|
|
14055
|
+
if (!optionsNode) return null;
|
|
14056
|
+
const unwrappedOptions = stripParenExpression(optionsNode);
|
|
14057
|
+
if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) {
|
|
14058
|
+
const optionsKey = allowOpaqueOptionsIdentity ? resolveResourceIdentityKey(unwrappedOptions, context) : null;
|
|
14059
|
+
return optionsKey ? `options:${optionsKey}` : null;
|
|
14060
|
+
}
|
|
14061
|
+
let captureKey = "capture:false";
|
|
14062
|
+
for (const property of unwrappedOptions.properties ?? []) {
|
|
14063
|
+
if (!isNodeOfType(property, "Property")) {
|
|
14064
|
+
captureKey = null;
|
|
14065
|
+
continue;
|
|
14066
|
+
}
|
|
14067
|
+
const propertyName = getStaticPropertyKeyName(property);
|
|
14068
|
+
if (propertyName === null || !property.computed && propertyName === "__proto__") {
|
|
14069
|
+
captureKey = null;
|
|
14070
|
+
continue;
|
|
14071
|
+
}
|
|
14072
|
+
if (propertyName === "capture") {
|
|
14073
|
+
const propertyValueKey = resolveResourceIdentityKey(property.value, context);
|
|
14074
|
+
captureKey = propertyValueKey ? `capture-value:${propertyValueKey}` : null;
|
|
14075
|
+
}
|
|
14076
|
+
}
|
|
14077
|
+
return captureKey;
|
|
14078
|
+
};
|
|
14079
|
+
const resolveEventListenerCaptureProjection = (optionsNode, context) => {
|
|
14080
|
+
if (!optionsNode) return null;
|
|
14081
|
+
const unwrappedOptions = stripParenExpression(optionsNode);
|
|
14082
|
+
if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) return resolveForEachProjection(unwrappedOptions, context);
|
|
14083
|
+
let captureProjection = null;
|
|
14084
|
+
for (const property of unwrappedOptions.properties ?? []) {
|
|
14085
|
+
if (!isNodeOfType(property, "Property")) {
|
|
14086
|
+
captureProjection = null;
|
|
14087
|
+
continue;
|
|
14088
|
+
}
|
|
14089
|
+
const propertyName = getStaticPropertyKeyName(property);
|
|
14090
|
+
if (propertyName === null || !property.computed && propertyName === "__proto__") {
|
|
14091
|
+
captureProjection = null;
|
|
14092
|
+
continue;
|
|
14093
|
+
}
|
|
14094
|
+
if (propertyName === "capture") captureProjection = resolveForEachProjection(property.value, context);
|
|
14095
|
+
}
|
|
14096
|
+
return captureProjection;
|
|
14097
|
+
};
|
|
14098
|
+
const doEventListenerCapturesMatch = (registrationOptions, releaseOptions, context, allowOpaqueOptionsIdentity = false) => {
|
|
14099
|
+
const registrationCaptureKey = resolveEventListenerCaptureIdentityKey(registrationOptions, context, allowOpaqueOptionsIdentity);
|
|
14100
|
+
return registrationCaptureKey !== null && registrationCaptureKey === resolveEventListenerCaptureIdentityKey(releaseOptions, context, allowOpaqueOptionsIdentity);
|
|
14101
|
+
};
|
|
14004
14102
|
const findAssignedResourceKey = (resourceNode, context) => {
|
|
14005
14103
|
let currentNode = resourceNode;
|
|
14006
14104
|
let parentNode = currentNode.parent;
|
|
@@ -14021,10 +14119,10 @@ const getCallRegistrationDetails = (callNode, context) => {
|
|
|
14021
14119
|
handlerKey: null
|
|
14022
14120
|
};
|
|
14023
14121
|
return {
|
|
14024
|
-
receiverKey:
|
|
14122
|
+
receiverKey: resolveResourceIdentityKey(callee.object, context),
|
|
14025
14123
|
registrationVerbName: callee.property.name,
|
|
14026
|
-
eventKey:
|
|
14027
|
-
handlerKey:
|
|
14124
|
+
eventKey: resolveResourceIdentityKey(callNode.arguments?.[0], context),
|
|
14125
|
+
handlerKey: resolveResourceIdentityKey(callNode.arguments?.[1], context)
|
|
14028
14126
|
};
|
|
14029
14127
|
};
|
|
14030
14128
|
const findSubscribeLikeUsages = (callback, context) => {
|
|
@@ -14449,8 +14547,8 @@ const isSynchronousIteratorCallback = (functionNode) => {
|
|
|
14449
14547
|
return SYNCHRONOUS_ITERATOR_METHOD_NAMES$2.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
|
|
14450
14548
|
};
|
|
14451
14549
|
const findEnclosingForEachCall = (node) => {
|
|
14452
|
-
const callbackNode = findEnclosingFunction$1(node);
|
|
14453
|
-
if (!callbackNode) return null;
|
|
14550
|
+
const callbackNode = isFunctionLike$1(node) ? node : findEnclosingFunction$1(node);
|
|
14551
|
+
if (!callbackNode || !isFunctionLike$1(callbackNode) || callbackNode.async || callbackNode.generator) return null;
|
|
14454
14552
|
const callNode = callbackNode.parent;
|
|
14455
14553
|
if (!isNodeOfType(callNode, "CallExpression") || callNode.arguments?.[0] !== callbackNode) return null;
|
|
14456
14554
|
const callee = stripParenExpression(callNode.callee);
|
|
@@ -15099,9 +15197,7 @@ const isReactRefListenerReplacementRelease = (releaseCall, usage, context) => {
|
|
|
15099
15197
|
const nodeParameterKey = resolveExpressionKey(usageFunction.params?.[0], context);
|
|
15100
15198
|
const releaseReceiverKey = resolveExpressionKey(releaseCallee.object, context);
|
|
15101
15199
|
if (registrationReceiverKey === null || registrationReceiverKey !== nodeParameterKey || releaseReceiverKey === null || usage.eventKey === null || usage.eventKey !== resolveExpressionKey(releaseCall.arguments?.[0], context) || usage.handlerKey === null || usage.handlerKey !== resolveExpressionKey(releaseCall.arguments?.[1], context)) return false;
|
|
15102
|
-
|
|
15103
|
-
const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15104
|
-
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
15200
|
+
if (!doEventListenerCapturesMatch(usage.node.arguments?.[2], releaseCall.arguments?.[2], context)) return false;
|
|
15105
15201
|
const releaseStart = getRangeStart(releaseCall);
|
|
15106
15202
|
const matchingOwnershipAssignments = [];
|
|
15107
15203
|
const usageFunctionBody = usageFunction.body;
|
|
@@ -15113,6 +15209,126 @@ const isReactRefListenerReplacementRelease = (releaseCall, usage, context) => {
|
|
|
15113
15209
|
const safeOwnershipAssignments = matchingOwnershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, [releaseAnchor], usageFunction, context));
|
|
15114
15210
|
return doMatchingNodesCoverEveryPathFromFunctionEntry(usageFunction, [releaseAnchor], context) && doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
|
|
15115
15211
|
};
|
|
15212
|
+
const findDirectExhaustiveForEachCleanupFunction = (releaseNode, requiredCollectionKeys, context) => {
|
|
15213
|
+
let currentNode = findTransparentExpressionRoot(releaseNode);
|
|
15214
|
+
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
15215
|
+
const replayedCollectionKeys = /* @__PURE__ */ new Set();
|
|
15216
|
+
while (true) {
|
|
15217
|
+
const ownerFunction = findEnclosingFunction$1(currentNode);
|
|
15218
|
+
if (!ownerFunction || !isFunctionLike$1(ownerFunction) || visitedFunctions.has(ownerFunction)) return null;
|
|
15219
|
+
visitedFunctions.add(ownerFunction);
|
|
15220
|
+
const isDirectConciseBody = ownerFunction.body === currentNode;
|
|
15221
|
+
const statementNode = currentNode.parent;
|
|
15222
|
+
const isDirectBlockStatement = isNodeOfType(ownerFunction.body, "BlockStatement") && isNodeOfType(statementNode, "ExpressionStatement") && statementNode.parent === ownerFunction.body;
|
|
15223
|
+
if (!isDirectConciseBody && !isDirectBlockStatement || !doMatchingNodesCoverEveryPathFromFunctionEntry(ownerFunction, [isDirectBlockStatement ? statementNode : currentNode], context)) return null;
|
|
15224
|
+
const forEachCall = findEnclosingForEachCall(ownerFunction);
|
|
15225
|
+
if (!forEachCall) return replayedCollectionKeys.size === requiredCollectionKeys.size && isReturnedEffectCleanupFunction(ownerFunction, context) ? ownerFunction : null;
|
|
15226
|
+
const forEachCallee = stripParenExpression(forEachCall.callee);
|
|
15227
|
+
if (!isNodeOfType(forEachCallee, "MemberExpression")) return null;
|
|
15228
|
+
const collectionKey = resolveExpressionKey(forEachCallee.object, context);
|
|
15229
|
+
if (!collectionKey || !requiredCollectionKeys.has(collectionKey)) return null;
|
|
15230
|
+
replayedCollectionKeys.add(collectionKey);
|
|
15231
|
+
currentNode = findTransparentExpressionRoot(forEachCall);
|
|
15232
|
+
}
|
|
15233
|
+
};
|
|
15234
|
+
const collectReplayOwnerFunctions = (usageNode) => {
|
|
15235
|
+
const ownerFunctions = /* @__PURE__ */ new Set();
|
|
15236
|
+
let currentNode = usageNode;
|
|
15237
|
+
while (true) {
|
|
15238
|
+
const ownerFunction = findEnclosingFunction$1(currentNode);
|
|
15239
|
+
if (!ownerFunction || !isFunctionLike$1(ownerFunction) || ownerFunctions.has(ownerFunction)) break;
|
|
15240
|
+
ownerFunctions.add(ownerFunction);
|
|
15241
|
+
const forEachCall = findEnclosingForEachCall(ownerFunction);
|
|
15242
|
+
if (!forEachCall) break;
|
|
15243
|
+
currentNode = forEachCall;
|
|
15244
|
+
}
|
|
15245
|
+
return ownerFunctions;
|
|
15246
|
+
};
|
|
15247
|
+
const hasCollectionMutationBeforeRelease = (usageNode, releaseNode, collectionKeys, context) => {
|
|
15248
|
+
const usageStart = getRangeStart(usageNode);
|
|
15249
|
+
const releaseStart = getRangeStart(releaseNode);
|
|
15250
|
+
if (usageStart === null || releaseStart === null) return true;
|
|
15251
|
+
const setupOwnerFunctions = collectReplayOwnerFunctions(usageNode);
|
|
15252
|
+
const cleanupOwnerFunctions = collectReplayOwnerFunctions(releaseNode);
|
|
15253
|
+
let programNode = usageNode;
|
|
15254
|
+
while (programNode.parent) programNode = programNode.parent;
|
|
15255
|
+
let didFindMutation = false;
|
|
15256
|
+
walkAst(programNode, (child) => {
|
|
15257
|
+
if (didFindMutation) return false;
|
|
15258
|
+
const childStart = getRangeStart(child);
|
|
15259
|
+
if (childStart === null) return;
|
|
15260
|
+
const ownerFunction = context.cfg.enclosingFunction(child);
|
|
15261
|
+
if (!ownerFunction) return;
|
|
15262
|
+
const isAfterRegistration = setupOwnerFunctions.has(ownerFunction) && childStart > usageStart;
|
|
15263
|
+
const isBeforeRelease = cleanupOwnerFunctions.has(ownerFunction) && childStart < releaseStart;
|
|
15264
|
+
if (!isAfterRegistration && !isBeforeRelease) return;
|
|
15265
|
+
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
15266
|
+
const assignmentKey = resolveExpressionKey(child.left, context);
|
|
15267
|
+
const assignmentTarget = stripParenExpression(child.left);
|
|
15268
|
+
if (assignmentKey && [...collectionKeys].some((collectionKey) => assignmentKey === collectionKey || assignmentKey === `${collectionKey}.length`) || isNodeOfType(assignmentTarget, "MemberExpression") && assignmentTarget.computed && collectionKeys.has(resolveExpressionKey(assignmentTarget.object, context) ?? "")) {
|
|
15269
|
+
didFindMutation = true;
|
|
15270
|
+
return false;
|
|
15271
|
+
}
|
|
15272
|
+
return;
|
|
15273
|
+
}
|
|
15274
|
+
if (isNodeOfType(child, "UnaryExpression") && child.operator === "delete") {
|
|
15275
|
+
const deletedMember = stripParenExpression(child.argument);
|
|
15276
|
+
if (!isNodeOfType(deletedMember, "MemberExpression")) return;
|
|
15277
|
+
if (collectionKeys.has(resolveExpressionKey(deletedMember.object, context) ?? "")) {
|
|
15278
|
+
didFindMutation = true;
|
|
15279
|
+
return false;
|
|
15280
|
+
}
|
|
15281
|
+
return;
|
|
15282
|
+
}
|
|
15283
|
+
if (isNodeOfType(child, "UpdateExpression")) {
|
|
15284
|
+
const updatedKey = resolveExpressionKey(child.argument, context);
|
|
15285
|
+
if (updatedKey && [...collectionKeys].some((collectionKey) => updatedKey === `${collectionKey}.length`)) {
|
|
15286
|
+
didFindMutation = true;
|
|
15287
|
+
return false;
|
|
15288
|
+
}
|
|
15289
|
+
return;
|
|
15290
|
+
}
|
|
15291
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
15292
|
+
const callee = stripParenExpression(child.callee);
|
|
15293
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !REPLAY_ENTRY_DROPPING_ARRAY_METHOD_NAMES.has(callee.property.name) && !REPLAY_ENTRY_DROPPING_COLLECTION_METHOD_NAMES.has(callee.property.name) || !collectionKeys.has(resolveExpressionKey(callee.object, context) ?? "")) return;
|
|
15294
|
+
didFindMutation = true;
|
|
15295
|
+
return false;
|
|
15296
|
+
});
|
|
15297
|
+
return didFindMutation;
|
|
15298
|
+
};
|
|
15299
|
+
const usesUnaryListenerSignature = (registrationCall, releaseCall) => getCalleeName$1(registrationCall) === "addListener" && registrationCall.arguments?.length === 1 && releaseCall.arguments?.length === 1;
|
|
15300
|
+
const hasSafeForEachProjectionCleanup = (registrationCall, releaseCall, context) => {
|
|
15301
|
+
const registrationCallee = stripParenExpression(registrationCall.callee);
|
|
15302
|
+
const releaseCallee = stripParenExpression(releaseCall.callee);
|
|
15303
|
+
if (!isNodeOfType(registrationCallee, "MemberExpression") || !isNodeOfType(releaseCallee, "MemberExpression")) return true;
|
|
15304
|
+
const registrationVerbName = getCalleeName$1(registrationCall);
|
|
15305
|
+
const releaseVerbName = getCalleeName$1(releaseCall);
|
|
15306
|
+
const releaseHandler = releaseCall.arguments?.[usesUnaryListenerSignature(registrationCall, releaseCall) ? 0 : 1];
|
|
15307
|
+
const releaseFunction = findEnclosingFunction$1(releaseCall);
|
|
15308
|
+
const registrationEventKey = resolveResourceIdentityKey(registrationCall.arguments?.[0], context);
|
|
15309
|
+
const releaseEventKey = resolveResourceIdentityKey(releaseCall.arguments?.[0], context);
|
|
15310
|
+
const doesHandlerlessOffReleaseEveryRegistration = releaseVerbName === "off" && !releaseHandler && (releaseCall.arguments?.length === 0 || registrationEventKey !== null && registrationEventKey === releaseEventKey);
|
|
15311
|
+
if (Boolean(releaseFunction && isFunctionLike$1(releaseFunction) && isReturnedEffectCleanupFunction(releaseFunction, context) && doMatchingNodesCoverEveryPathFromFunctionEntry(releaseFunction, [releaseCall], context)) && (releaseVerbName !== null && UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName) || doesHandlerlessOffReleaseEveryRegistration)) return true;
|
|
15312
|
+
const projections = [
|
|
15313
|
+
registrationCallee.object,
|
|
15314
|
+
registrationCall.arguments?.[0],
|
|
15315
|
+
registrationCall.arguments?.[1],
|
|
15316
|
+
releaseCallee.object,
|
|
15317
|
+
releaseCall.arguments?.[0],
|
|
15318
|
+
releaseCall.arguments?.[1]
|
|
15319
|
+
].flatMap((expression) => {
|
|
15320
|
+
const projection = resolveForEachProjection(expression, context);
|
|
15321
|
+
return projection ? [projection] : [];
|
|
15322
|
+
});
|
|
15323
|
+
if (registrationVerbName === "addEventListener" && releaseVerbName === "removeEventListener") for (const optionsNode of [registrationCall.arguments?.[2], releaseCall.arguments?.[2]]) {
|
|
15324
|
+
const captureProjection = resolveEventListenerCaptureProjection(optionsNode, context);
|
|
15325
|
+
if (captureProjection) projections.push(captureProjection);
|
|
15326
|
+
}
|
|
15327
|
+
if (projections.length === 0) return true;
|
|
15328
|
+
const collectionKeys = new Set(projections.map((projection) => projection.collectionKey));
|
|
15329
|
+
if (!findDirectExhaustiveForEachCleanupFunction(releaseCall, collectionKeys, context)) return false;
|
|
15330
|
+
return !hasCollectionMutationBeforeRelease(registrationCall, releaseCall, collectionKeys, context);
|
|
15331
|
+
};
|
|
15116
15332
|
const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
15117
15333
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
15118
15334
|
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
@@ -15128,8 +15344,8 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
15128
15344
|
const releaseVerbName = getReleaseVerbName(callNode);
|
|
15129
15345
|
if (!releaseVerbName) return false;
|
|
15130
15346
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
15131
|
-
const releaseReceiverKey =
|
|
15132
|
-
const releaseEventKey =
|
|
15347
|
+
const releaseReceiverKey = resolveResourceIdentityKey(callee.object, context);
|
|
15348
|
+
const releaseEventKey = resolveResourceIdentityKey(callNode.arguments?.[0], context);
|
|
15133
15349
|
const pairedReleaseVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
15134
15350
|
const pushedResourceCollectionKey = findPushedResourceCollectionKey(usage, context);
|
|
15135
15351
|
const releaseReceiverForOfStatement = findForOfStatementForIteratorExpression(callee.object, context);
|
|
@@ -15140,6 +15356,11 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
15140
15356
|
if (usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName))) return true;
|
|
15141
15357
|
if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
|
|
15142
15358
|
if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
|
|
15359
|
+
if (usage.registrationVerbName === "addEventListener" && releaseVerbName === "removeEventListener" && isNodeOfType(usage.node, "CallExpression")) {
|
|
15360
|
+
if (!isNodeOfType(stripParenExpression(usage.node.callee), "MemberExpression")) return false;
|
|
15361
|
+
if (!doEventListenerCapturesMatch(usage.node.arguments?.[2], callNode.arguments?.[2], context, true)) return false;
|
|
15362
|
+
}
|
|
15363
|
+
if (isNodeOfType(usage.node, "CallExpression") && !hasSafeForEachProjectionCleanup(usage.node, callNode, context)) return false;
|
|
15143
15364
|
if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
|
|
15144
15365
|
if (usage.registrationVerbName === "subscribe" && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub") && usage.handleKey !== null && resolveExpressionKey(callNode.arguments?.[0], context) === usage.handleKey) return true;
|
|
15145
15366
|
const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
@@ -15149,6 +15370,9 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
15149
15370
|
if (isAssignmentFormForOfIteratorReference(usageEventArgument, context) || isAssignmentFormForOfIteratorReference(releaseEventArgument, context)) return false;
|
|
15150
15371
|
if (usage.eventKey !== null && releaseEventKey !== null && usage.eventKey !== releaseEventKey) {
|
|
15151
15372
|
if (!isNodeOfType(usage.node, "CallExpression")) return false;
|
|
15373
|
+
const registrationEventProjectionKey = resolveForEachProjectionKey(usage.node.arguments?.[0], context);
|
|
15374
|
+
const releaseEventProjectionKey = resolveForEachProjectionKey(callNode.arguments?.[0], context);
|
|
15375
|
+
if ((registrationEventProjectionKey !== null || releaseEventProjectionKey !== null) && registrationEventProjectionKey !== releaseEventProjectionKey) return false;
|
|
15152
15376
|
const usageForOfStatement = findForOfStatementForIteratorExpression(usageEventArgument, context);
|
|
15153
15377
|
const releaseForOfStatement = findForOfStatementForIteratorExpression(releaseEventArgument, context);
|
|
15154
15378
|
if (usageForOfStatement === null !== (releaseForOfStatement === null)) return false;
|
|
@@ -15162,9 +15386,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
15162
15386
|
const registrationCallee = stripParenExpression(usage.node.callee);
|
|
15163
15387
|
if (!isNodeOfType(registrationCallee, "MemberExpression")) return false;
|
|
15164
15388
|
if (!isStableLoopReceiver(registrationCallee.object, context) || !isStableLoopReceiver(callee.object, context) || registrationHandlerSymbolId === null || registrationHandlerSymbolId !== releaseHandlerSymbolId) return false;
|
|
15165
|
-
|
|
15166
|
-
const releaseCapture = resolveEventListenerCapture(callNode.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15167
|
-
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
15389
|
+
if (!doEventListenerCapturesMatch(usage.node.arguments?.[2], callNode.arguments?.[2], context)) return false;
|
|
15168
15390
|
if (!isDirectExhaustiveForOfRelease(callNode, releaseForOfStatement)) return false;
|
|
15169
15391
|
}
|
|
15170
15392
|
}
|
|
@@ -15173,27 +15395,28 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
15173
15395
|
return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
|
|
15174
15396
|
}
|
|
15175
15397
|
if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
|
|
15176
|
-
const
|
|
15177
|
-
const releaseHandler =
|
|
15398
|
+
const usesUnaryListenerSignatureForCalls = isNodeOfType(usage.node, "CallExpression") && usesUnaryListenerSignature(usage.node, callNode);
|
|
15399
|
+
const releaseHandler = usesUnaryListenerSignatureForCalls ? callNode.arguments?.[0] : callNode.arguments?.[1];
|
|
15178
15400
|
if (!releaseHandler) return releaseVerbName === "off";
|
|
15179
|
-
const expectedHandlerKey =
|
|
15180
|
-
const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[
|
|
15181
|
-
return expectedHandlerKey !== null &&
|
|
15401
|
+
const expectedHandlerKey = usesUnaryListenerSignatureForCalls ? usage.eventKey : usage.handlerKey;
|
|
15402
|
+
const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[usesUnaryListenerSignatureForCalls ? 0 : 1] : null;
|
|
15403
|
+
return expectedHandlerKey !== null && resolveResourceIdentityKey(releaseHandler, context) === expectedHandlerKey || registrationHandler !== null && resolveStableValue(releaseHandler, context) === resolveStableValue(registrationHandler, context);
|
|
15182
15404
|
}
|
|
15183
15405
|
if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
|
|
15184
15406
|
return true;
|
|
15185
15407
|
};
|
|
15186
15408
|
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
15187
15409
|
const isReturnedEffectCleanupFunction = (functionNode, context) => {
|
|
15188
|
-
|
|
15189
|
-
|
|
15190
|
-
|
|
15191
|
-
|
|
15192
|
-
|
|
15193
|
-
|
|
15194
|
-
|
|
15195
|
-
|
|
15196
|
-
|
|
15410
|
+
const effectCallback = findEnclosingFunction$1(functionNode);
|
|
15411
|
+
if (!effectCallback || !isFunctionLike$1(effectCallback)) return false;
|
|
15412
|
+
const effectCall = effectCallback.parent;
|
|
15413
|
+
if (!isNodeOfType(effectCall, "CallExpression") || !isReactHookCall(effectCall, CLEANUP_EFFECT_HOOK_NAMES, context.scopes)) return false;
|
|
15414
|
+
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return resolveStableValue(effectCallback.body, context) === functionNode;
|
|
15415
|
+
let isReturned = false;
|
|
15416
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
15417
|
+
if (isNodeOfType(child, "ReturnStatement") && child.argument && resolveStableValue(child.argument, context) === functionNode) isReturned = true;
|
|
15418
|
+
});
|
|
15419
|
+
return isReturned;
|
|
15197
15420
|
};
|
|
15198
15421
|
const isPotentiallyReachableFunction = (functionNode, context) => {
|
|
15199
15422
|
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode, context)) return true;
|
|
@@ -15336,11 +15559,9 @@ const isRetainedDisposerRefRelease = (releaseNode, usage, context) => {
|
|
|
15336
15559
|
};
|
|
15337
15560
|
const isSelfReleasingListenerRelease = (releaseNode, releaseFunction, usage, context) => {
|
|
15338
15561
|
if (usage.kind !== "subscribe" || usage.registrationVerbName !== "addEventListener" || usage.receiverKey === null || usage.eventKey === null || !isNodeOfType(usage.node, "CallExpression") || !isFunctionLike$1(releaseFunction) || releaseFunction.async || releaseFunction.generator || !isNodeOfType(releaseFunction.body, "BlockStatement") || !doMatchingNodesCoverEveryPathFromFunctionEntry(releaseFunction, [releaseNode], context)) return false;
|
|
15339
|
-
const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15340
15562
|
const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
|
|
15341
15563
|
if (!isNodeOfType(releaseCall, "CallExpression")) return false;
|
|
15342
|
-
|
|
15343
|
-
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
15564
|
+
if (!doEventListenerCapturesMatch(usage.node.arguments?.[2], releaseCall.arguments?.[2], context)) return false;
|
|
15344
15565
|
const ownerFunction = findEnclosingFunction$1(releaseFunction);
|
|
15345
15566
|
if (!ownerFunction || !isFunctionLike$1(ownerFunction)) return false;
|
|
15346
15567
|
const triggerRegistrations = [];
|
|
@@ -18810,11 +19031,12 @@ const forbidDomProps = defineRule({
|
|
|
18810
19031
|
* carrying their own near-identical inlined implementation.
|
|
18811
19032
|
*/
|
|
18812
19033
|
const flattenCalleeName = (callee) => {
|
|
18813
|
-
|
|
18814
|
-
if (isNodeOfType(
|
|
18815
|
-
|
|
19034
|
+
const unwrappedCallee = stripParenExpression(callee);
|
|
19035
|
+
if (isNodeOfType(unwrappedCallee, "Identifier")) return unwrappedCallee.name;
|
|
19036
|
+
if (isNodeOfType(unwrappedCallee, "MemberExpression") && !unwrappedCallee.computed) {
|
|
19037
|
+
const objectName = flattenCalleeName(unwrappedCallee.object);
|
|
18816
19038
|
if (!objectName) return null;
|
|
18817
|
-
if (isNodeOfType(
|
|
19039
|
+
if (isNodeOfType(unwrappedCallee.property, "Identifier")) return `${objectName}.${unwrappedCallee.property.name}`;
|
|
18818
19040
|
}
|
|
18819
19041
|
return null;
|
|
18820
19042
|
};
|
|
@@ -24587,9 +24809,14 @@ const collectContextBindings = (programRoot) => {
|
|
|
24587
24809
|
}
|
|
24588
24810
|
return bindings;
|
|
24589
24811
|
};
|
|
24590
|
-
const isCreateContextBindingJsxName = (node, contextBindings) => {
|
|
24812
|
+
const isCreateContextBindingJsxName = (node, contextBindings, scopes) => {
|
|
24591
24813
|
if (!isNodeOfType(node, "JSXIdentifier")) return false;
|
|
24592
|
-
if (!contextBindings.has(node.name))
|
|
24814
|
+
if (!contextBindings.has(node.name)) {
|
|
24815
|
+
const symbol = scopes.symbolFor(node);
|
|
24816
|
+
if (!symbol || symbol.kind !== "import") return false;
|
|
24817
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
24818
|
+
return getImportSourceForName(node, node.name)?.toLowerCase().includes("context") === true && (node.name.endsWith("Context") || importedName?.endsWith("Context") === true);
|
|
24819
|
+
}
|
|
24593
24820
|
const binding = findVariableInitializer(node, node.name);
|
|
24594
24821
|
if (!binding) return false;
|
|
24595
24822
|
return binding.scopeOwner.type === "Program";
|
|
@@ -24613,7 +24840,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
24613
24840
|
if (isTestlikeFile) return;
|
|
24614
24841
|
const nameNode = node.name;
|
|
24615
24842
|
const isLegacyProvider = isProviderMemberName(nameNode);
|
|
24616
|
-
const isReact19Shorthand = isCreateContextBindingJsxName(nameNode, contextBindings);
|
|
24843
|
+
const isReact19Shorthand = isCreateContextBindingJsxName(nameNode, contextBindings, context.scopes);
|
|
24617
24844
|
if (!isLegacyProvider && !isReact19Shorthand) return;
|
|
24618
24845
|
if (!isInsideFunctionScope(node)) return;
|
|
24619
24846
|
for (const attribute of node.attributes) {
|
|
@@ -24884,6 +25111,29 @@ const buildSameFileJsxSlotPropRegistry = (program, memoRegistry, scopes) => {
|
|
|
24884
25111
|
return registry;
|
|
24885
25112
|
};
|
|
24886
25113
|
//#endregion
|
|
25114
|
+
//#region src/plugin/utils/has-custom-memo-comparator.ts
|
|
25115
|
+
const MEMO_CALLEE_NAMES$1 = new Set(["memo", "React.memo"]);
|
|
25116
|
+
const isIdentitySensitiveMemoComparator = (comparatorNode, scopes) => {
|
|
25117
|
+
if (isNodeOfType(comparatorNode, "Identifier")) {
|
|
25118
|
+
if (comparatorNode.name === "undefined") return scopes.isGlobalReference(comparatorNode);
|
|
25119
|
+
return getImportedNameFromModule(comparatorNode, comparatorNode.name, "react-redux") === "shallowEqual";
|
|
25120
|
+
}
|
|
25121
|
+
if (!isNodeOfType(comparatorNode, "MemberExpression") || comparatorNode.computed || !isNodeOfType(comparatorNode.object, "Identifier") || !isNodeOfType(comparatorNode.property, "Identifier") || comparatorNode.property.name !== "shallowEqual") return false;
|
|
25122
|
+
return isNamespaceImportFromModule$1(comparatorNode, comparatorNode.object.name, "react-redux");
|
|
25123
|
+
};
|
|
25124
|
+
const hasCustomMemoComparator = (openingName, scopes) => {
|
|
25125
|
+
if (!openingName || !isNodeOfType(openingName, "JSXIdentifier")) return false;
|
|
25126
|
+
const binding = findVariableInitializer(openingName, openingName.name);
|
|
25127
|
+
if (!binding || !binding.initializer) return false;
|
|
25128
|
+
const initializer = binding.initializer;
|
|
25129
|
+
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
25130
|
+
const calleeName = flattenCalleeName(initializer.callee);
|
|
25131
|
+
if (calleeName === null || !MEMO_CALLEE_NAMES$1.has(calleeName)) return false;
|
|
25132
|
+
const comparatorNode = (initializer.arguments ?? [])[1];
|
|
25133
|
+
if (!comparatorNode) return false;
|
|
25134
|
+
return !isIdentitySensitiveMemoComparator(comparatorNode, scopes);
|
|
25135
|
+
};
|
|
25136
|
+
//#endregion
|
|
24887
25137
|
//#region src/plugin/utils/is-on-intrinsic-html-element.ts
|
|
24888
25138
|
const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
24889
25139
|
const openingElement = attribute.parent;
|
|
@@ -25169,6 +25419,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
25169
25419
|
const parentJsxOpening = node.parent;
|
|
25170
25420
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
25171
25421
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
25422
|
+
if (hasCustomMemoComparator(openingName, context.scopes)) return;
|
|
25172
25423
|
const openingSymbol = openingName && isNodeOfType(openingName, "JSXIdentifier") ? context.scopes.symbolFor(openingName) : null;
|
|
25173
25424
|
if (openingSymbol && isNodeOfType(node.name, "JSXIdentifier") && jsxSlotPropRegistry?.get(openingSymbol.id)?.has(node.name.name)) return;
|
|
25174
25425
|
if (isNodeOfType(node.name, "JSXIdentifier") && isSlotPropName(node.name.name)) return;
|
|
@@ -25188,19 +25439,6 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
25188
25439
|
}
|
|
25189
25440
|
});
|
|
25190
25441
|
//#endregion
|
|
25191
|
-
//#region src/plugin/utils/has-custom-memo-comparator.ts
|
|
25192
|
-
const MEMO_CALLEE_NAMES = new Set(["memo", "React.memo"]);
|
|
25193
|
-
const hasCustomMemoComparator = (openingName) => {
|
|
25194
|
-
if (!openingName || !isNodeOfType(openingName, "JSXIdentifier")) return false;
|
|
25195
|
-
const binding = findVariableInitializer(openingName, openingName.name);
|
|
25196
|
-
if (!binding || !binding.initializer) return false;
|
|
25197
|
-
const initializer = binding.initializer;
|
|
25198
|
-
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
25199
|
-
const calleeName = flattenCalleeName(initializer.callee);
|
|
25200
|
-
if (calleeName === null || !MEMO_CALLEE_NAMES.has(calleeName)) return false;
|
|
25201
|
-
return (initializer.arguments ?? []).length >= 2;
|
|
25202
|
-
};
|
|
25203
|
-
//#endregion
|
|
25204
25442
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop-tables.ts
|
|
25205
25443
|
const DATA_ARRAY_PROP_NAMES = new Set([
|
|
25206
25444
|
"data",
|
|
@@ -25559,7 +25797,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
25559
25797
|
const parentJsxOpening = node.parent;
|
|
25560
25798
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
25561
25799
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
25562
|
-
if (hasCustomMemoComparator(openingName)) return;
|
|
25800
|
+
if (hasCustomMemoComparator(openingName, context.scopes)) return;
|
|
25563
25801
|
if (isNodeOfType(node.name, "JSXIdentifier") && isDataArrayPropName(node.name.name)) return;
|
|
25564
25802
|
if (!isInsideFunctionScope(node)) return;
|
|
25565
25803
|
const value = node.value;
|
|
@@ -26023,6 +26261,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
26023
26261
|
const parentJsxOpening = node.parent;
|
|
26024
26262
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
26025
26263
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
26264
|
+
if (hasCustomMemoComparator(openingName, context.scopes)) return;
|
|
26026
26265
|
if (isNodeOfType(node.name, "JSXIdentifier") && isOneShotHandlerName(node.name.name)) return;
|
|
26027
26266
|
if (!isInsideFunctionScope(node)) return;
|
|
26028
26267
|
const value = node.value;
|
|
@@ -26330,7 +26569,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
26330
26569
|
const parentJsxOpening = node.parent;
|
|
26331
26570
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
26332
26571
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
26333
|
-
if (hasCustomMemoComparator(openingName)) return;
|
|
26572
|
+
if (hasCustomMemoComparator(openingName, context.scopes)) return;
|
|
26334
26573
|
if (!isInsideFunctionScope(node)) return;
|
|
26335
26574
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
26336
26575
|
if (ALWAYS_FRESH_OBJECT_PROPS.has(node.name.name)) return;
|
|
@@ -29362,6 +29601,14 @@ const nextjsNoImgElement = defineRule({
|
|
|
29362
29601
|
});
|
|
29363
29602
|
//#endregion
|
|
29364
29603
|
//#region src/plugin/rules/nextjs/nextjs-no-native-script.ts
|
|
29604
|
+
const hasEnabledBooleanAttribute = (attributes, attributeName) => {
|
|
29605
|
+
const attribute = findJsxAttribute(attributes, attributeName);
|
|
29606
|
+
if (!attribute) return false;
|
|
29607
|
+
if (attribute.value === null) return true;
|
|
29608
|
+
if (isNodeOfType(attribute.value, "Literal")) return true;
|
|
29609
|
+
if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return false;
|
|
29610
|
+
return isNodeOfType(attribute.value.expression, "Literal") && Boolean(attribute.value.expression.value);
|
|
29611
|
+
};
|
|
29365
29612
|
const nextjsNoNativeScript = defineRule({
|
|
29366
29613
|
id: "nextjs-no-native-script",
|
|
29367
29614
|
title: "Plain script can block Next.js rendering",
|
|
@@ -29374,6 +29621,9 @@ const nextjsNoNativeScript = defineRule({
|
|
|
29374
29621
|
const typeAttribute = findJsxAttribute(node.attributes ?? [], "type");
|
|
29375
29622
|
const typeValue = isNodeOfType(typeAttribute?.value, "Literal") ? typeAttribute.value.value : null;
|
|
29376
29623
|
if (typeof typeValue === "string" && !EXECUTABLE_SCRIPT_TYPES.has(typeValue)) return;
|
|
29624
|
+
if (hasEnabledBooleanAttribute(node.attributes ?? [], "async")) return;
|
|
29625
|
+
if (hasEnabledBooleanAttribute(node.attributes ?? [], "defer")) return;
|
|
29626
|
+
if (typeValue === "module") return;
|
|
29377
29627
|
const hasSrcAttribute = Boolean(findJsxAttribute(node.attributes ?? [], "src"));
|
|
29378
29628
|
if (Boolean(findJsxAttribute(node.attributes ?? [], "dangerouslySetInnerHTML")) && !hasSrcAttribute) return;
|
|
29379
29629
|
context.report({
|
|
@@ -38911,15 +39161,16 @@ const callsOpaqueExternalSetter = (analysisFunctions, setterToStateName) => {
|
|
|
38911
39161
|
});
|
|
38912
39162
|
return didFindOpaqueSetterCall;
|
|
38913
39163
|
};
|
|
38914
|
-
const
|
|
39164
|
+
const isReactUseRefCall = (expression, scopes) => isNodeOfType(expression, "CallExpression") && isReactApiCall(expression, "useRef", scopes, {
|
|
38915
39165
|
allowGlobalReactNamespace: true,
|
|
38916
39166
|
allowUnboundBareCalls: true,
|
|
38917
39167
|
resolveNamedAliases: true
|
|
38918
|
-
})
|
|
39168
|
+
});
|
|
39169
|
+
const isReactRefCall = (expression, scopes) => isReactUseRefCall(expression, scopes) || isNodeOfType(expression, "CallExpression") && isReactApiCall(expression, "createRef", scopes, {
|
|
38919
39170
|
allowGlobalReactNamespace: true,
|
|
38920
39171
|
allowUnboundBareCalls: true,
|
|
38921
39172
|
resolveNamedAliases: true
|
|
38922
|
-
})
|
|
39173
|
+
});
|
|
38923
39174
|
const getDirectReactRefSymbol = (rawExpression, scopes) => {
|
|
38924
39175
|
const expression = stripParenExpression(rawExpression);
|
|
38925
39176
|
if (!isNodeOfType(expression, "Identifier")) return null;
|
|
@@ -38957,7 +39208,18 @@ const isIntrinsicRefCallbackParameter = (expression, scopes) => {
|
|
|
38957
39208
|
const rawFirstParameter = callback.params?.[0];
|
|
38958
39209
|
const firstParameter = isNodeOfType(rawFirstParameter, "AssignmentPattern") ? rawFirstParameter.left : rawFirstParameter;
|
|
38959
39210
|
const symbol = scopes.symbolFor(identifier);
|
|
38960
|
-
return Boolean(firstParameter && symbol?.bindingIdentifier === firstParameter)
|
|
39211
|
+
return Boolean(firstParameter && symbol?.bindingIdentifier === firstParameter && symbol.references.every((reference) => {
|
|
39212
|
+
if (reference.flag !== "read") return false;
|
|
39213
|
+
let referenceRoot = reference.identifier;
|
|
39214
|
+
while (referenceRoot.parent) {
|
|
39215
|
+
const parent = referenceRoot.parent;
|
|
39216
|
+
if (isNodeOfType(parent, "AssignmentExpression")) return parent.left !== referenceRoot;
|
|
39217
|
+
if (isNodeOfType(parent, "UpdateExpression")) return parent.argument !== referenceRoot;
|
|
39218
|
+
if (isNodeOfType(parent, "ForInStatement") || isNodeOfType(parent, "ForOfStatement")) return parent.left !== referenceRoot;
|
|
39219
|
+
referenceRoot = parent;
|
|
39220
|
+
}
|
|
39221
|
+
return true;
|
|
39222
|
+
}));
|
|
38961
39223
|
};
|
|
38962
39224
|
const getDirectReactRefCall = (symbol, scopes) => {
|
|
38963
39225
|
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
@@ -38965,15 +39227,38 @@ const getDirectReactRefCall = (symbol, scopes) => {
|
|
|
38965
39227
|
const expression = stripParenExpression(initializer);
|
|
38966
39228
|
return isNodeOfType(expression, "CallExpression") && isReactRefCall(expression, scopes) ? expression : null;
|
|
38967
39229
|
};
|
|
39230
|
+
const isEmptyGlobalMapConstruction = (rawExpression, scopes) => {
|
|
39231
|
+
const expression = stripParenExpression(rawExpression);
|
|
39232
|
+
return isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "Map" && scopes.isGlobalReference(expression.callee) && expression.arguments.length === 0;
|
|
39233
|
+
};
|
|
39234
|
+
const hasEmptyRefSentinelInitializer = (refCall, scopes) => {
|
|
39235
|
+
if (!isReactUseRefCall(refCall, scopes) || refCall.arguments.length > 1) return false;
|
|
39236
|
+
const [initialValue] = refCall.arguments;
|
|
39237
|
+
return !initialValue || isNodeOfType(initialValue, "Literal") && initialValue.value === null || isNodeOfType(initialValue, "Identifier") && initialValue.name === "undefined" && scopes.isGlobalReference(initialValue);
|
|
39238
|
+
};
|
|
39239
|
+
const isDirectLazyEmptyMapInitialization = (currentExpression, symbol, scopes) => {
|
|
39240
|
+
const assignment = currentExpression.parent;
|
|
39241
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== currentExpression || assignment.operator !== "??=" || !isEmptyGlobalMapConstruction(assignment.right, scopes)) return false;
|
|
39242
|
+
const refOwner = findEnclosingFunction$1(symbol.bindingIdentifier);
|
|
39243
|
+
return refOwner !== null && findEnclosingFunction$1(assignment) === refOwner;
|
|
39244
|
+
};
|
|
38968
39245
|
const storesOnlyIntrinsicRefCallbackValues = (symbol, scopes) => {
|
|
38969
|
-
const
|
|
38970
|
-
|
|
39246
|
+
const refCall = getDirectReactRefCall(symbol, scopes);
|
|
39247
|
+
const initialValue = refCall?.arguments[0];
|
|
39248
|
+
const hasDirectEmptyMapInitializer = Boolean(initialValue && isEmptyGlobalMapConstruction(initialValue, scopes));
|
|
39249
|
+
const hasLazyEmptyMapInitializer = Boolean(refCall && hasEmptyRefSentinelInitializer(refCall, scopes));
|
|
39250
|
+
if (!hasDirectEmptyMapInitializer && !hasLazyEmptyMapInitializer) return false;
|
|
38971
39251
|
let intrinsicValueWriteCount = 0;
|
|
39252
|
+
let lazyEmptyMapInitializationCount = 0;
|
|
38972
39253
|
for (const reference of symbol.references) {
|
|
38973
39254
|
const identifier = findTransparentExpressionRoot(reference.identifier);
|
|
38974
39255
|
const currentMember = identifier.parent;
|
|
38975
39256
|
if (!isNodeOfType(currentMember, "MemberExpression") || currentMember.object !== identifier || getStaticPropertyName(currentMember) !== "current") return false;
|
|
38976
39257
|
const currentExpression = findTransparentExpressionRoot(currentMember);
|
|
39258
|
+
if (hasLazyEmptyMapInitializer && isDirectLazyEmptyMapInitialization(currentExpression, symbol, scopes)) {
|
|
39259
|
+
lazyEmptyMapInitializationCount += 1;
|
|
39260
|
+
continue;
|
|
39261
|
+
}
|
|
38977
39262
|
const methodMember = currentExpression.parent;
|
|
38978
39263
|
if (!isNodeOfType(methodMember, "MemberExpression") || methodMember.object !== currentExpression) return false;
|
|
38979
39264
|
const methodName = getStaticPropertyName(methodMember);
|
|
@@ -38986,7 +39271,7 @@ const storesOnlyIntrinsicRefCallbackValues = (symbol, scopes) => {
|
|
|
38986
39271
|
if (!storedValue || isNodeOfType(storedValue, "SpreadElement") || !isIntrinsicRefCallbackParameter(storedValue, scopes)) return false;
|
|
38987
39272
|
intrinsicValueWriteCount += 1;
|
|
38988
39273
|
}
|
|
38989
|
-
return intrinsicValueWriteCount > 0;
|
|
39274
|
+
return intrinsicValueWriteCount > 0 && (hasDirectEmptyMapInitializer || lazyEmptyMapInitializationCount === 1);
|
|
38990
39275
|
};
|
|
38991
39276
|
const isDerivedFromProvenDomRefCurrent = (rawExpression, scopes, didReadCollectionValue = false, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
38992
39277
|
const expression = stripParenExpression(rawExpression);
|
|
@@ -39490,7 +39775,7 @@ const noEffectEventInDeps = defineRule({
|
|
|
39490
39775
|
title: "Effect Event listed in effect deps",
|
|
39491
39776
|
tags: ["test-noise"],
|
|
39492
39777
|
severity: "error",
|
|
39493
|
-
recommendation: "Call the useEffectEvent function inside the effect body and don't list it in the deps. It
|
|
39778
|
+
recommendation: "Call the useEffectEvent function inside the effect body and don't list it in the deps. It's non-reactive on purpose, so it must be omitted.",
|
|
39494
39779
|
create: (context) => {
|
|
39495
39780
|
const componentBindings = createComponentBindingStackTracker({ onVariableDeclarator: (declaratorNode) => {
|
|
39496
39781
|
if (!isNodeOfType(declaratorNode, "VariableDeclarator")) return;
|
|
@@ -39512,7 +39797,7 @@ const noEffectEventInDeps = defineRule({
|
|
|
39512
39797
|
if (!isNodeOfType(element, "Identifier")) continue;
|
|
39513
39798
|
if (componentBindings.isBoundName(element.name)) context.report({
|
|
39514
39799
|
node: element,
|
|
39515
|
-
message: `Listing "${element.name}" in the deps
|
|
39800
|
+
message: `Listing "${element.name}" in the deps defeats useEffectEvent — Effect Events are non-reactive and must be omitted from deps.`
|
|
39516
39801
|
});
|
|
39517
39802
|
}
|
|
39518
39803
|
}
|
|
@@ -39555,6 +39840,7 @@ const noEffectWithFreshDeps = defineRule({
|
|
|
39555
39840
|
title: "Effect dependency recreated every render",
|
|
39556
39841
|
severity: "error",
|
|
39557
39842
|
category: "State & Effects",
|
|
39843
|
+
disabledWhen: ["react-compiler"],
|
|
39558
39844
|
recommendation: "Move the value inside the hook body and depend on its simple inputs instead, or wrap it in useMemo / useCallback so it stays the same between renders.",
|
|
39559
39845
|
create: (context) => ({ CallExpression(node) {
|
|
39560
39846
|
for (const finding of findForwardedFreshHookDependencies(node, context, EFFECT_HOOK_NAMES$1)) context.report({
|
|
@@ -42495,14 +42781,13 @@ const noInlineExhaustiveStyle = defineRule({
|
|
|
42495
42781
|
});
|
|
42496
42782
|
//#endregion
|
|
42497
42783
|
//#region src/plugin/rules/performance/no-inline-prop-on-memo-component.ts
|
|
42498
|
-
const
|
|
42784
|
+
const MEMO_CALLEE_NAMES = new Set(["memo", "React.memo"]);
|
|
42785
|
+
const isMemoCall = (node) => isNodeOfType(node, "CallExpression") && MEMO_CALLEE_NAMES.has(flattenCalleeName(node.callee) ?? "");
|
|
42786
|
+
const hasCustomComparator = (node, scopes) => {
|
|
42499
42787
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
42500
|
-
|
|
42501
|
-
|
|
42502
|
-
return false;
|
|
42788
|
+
const comparator = node.arguments?.[1];
|
|
42789
|
+
return comparator ? !isIdentitySensitiveMemoComparator(comparator, scopes) : false;
|
|
42503
42790
|
};
|
|
42504
|
-
const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
|
|
42505
|
-
const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
|
|
42506
42791
|
const isInlineReference = (node, scopes) => {
|
|
42507
42792
|
const referenceNode = unwrapObjectIntegrityExpression(node, scopes);
|
|
42508
42793
|
if (isNodeOfType(referenceNode, "ArrowFunctionExpression") || isNodeOfType(referenceNode, "FunctionExpression") || isNodeOfType(referenceNode, "CallExpression") && isNodeOfType(referenceNode.callee, "MemberExpression") && isNodeOfType(referenceNode.callee.property, "Identifier") && referenceNode.callee.property.name === "bind") return "functions";
|
|
@@ -42516,16 +42801,17 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
42516
42801
|
title: "Inline prop defeats memo()",
|
|
42517
42802
|
tags: ["test-noise"],
|
|
42518
42803
|
severity: "warn",
|
|
42804
|
+
disabledWhen: ["react-compiler"],
|
|
42519
42805
|
recommendation: "Move the inline `() => ...` / `[]` / `{}` to a stable value with useMemo, useCallback, or module scope, so the memoized child stops redrawing on every parent render",
|
|
42520
42806
|
create: (context) => {
|
|
42521
42807
|
const memoizedComponentNames = /* @__PURE__ */ new Set();
|
|
42522
42808
|
return {
|
|
42523
42809
|
VariableDeclarator(node) {
|
|
42524
42810
|
if (!isNodeOfType(node.id, "Identifier") || !node.init) return;
|
|
42525
|
-
if (isMemoCall(node.init) && !hasCustomComparator(node.init)) memoizedComponentNames.add(node.id.name);
|
|
42811
|
+
if (isMemoCall(node.init) && !hasCustomComparator(node.init, context.scopes)) memoizedComponentNames.add(node.id.name);
|
|
42526
42812
|
},
|
|
42527
42813
|
ExportDefaultDeclaration(node) {
|
|
42528
|
-
if (node.declaration && isNodeOfType(node.declaration, "CallExpression") && isMemoCall(node.declaration) && !hasCustomComparator(node.declaration)) {
|
|
42814
|
+
if (node.declaration && isNodeOfType(node.declaration, "CallExpression") && isMemoCall(node.declaration) && !hasCustomComparator(node.declaration, context.scopes)) {
|
|
42529
42815
|
const innerArgument = node.declaration.arguments?.[0];
|
|
42530
42816
|
if (isNodeOfType(innerArgument, "Identifier")) memoizedComponentNames.add(innerArgument.name);
|
|
42531
42817
|
}
|
|
@@ -55380,6 +55666,7 @@ const preferModuleScopePureFunction = defineRule({
|
|
|
55380
55666
|
tags: ["test-noise"],
|
|
55381
55667
|
severity: "warn",
|
|
55382
55668
|
category: "Architecture",
|
|
55669
|
+
disabledWhen: ["react-compiler"],
|
|
55383
55670
|
recommendation: "Move the function above the component, at the top of the file. It doesn't use local state, so rebuilding it each update is wasted work.",
|
|
55384
55671
|
create: (context) => {
|
|
55385
55672
|
const report = (functionNode, name, componentName) => {
|
|
@@ -56021,10 +56308,10 @@ const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, c
|
|
|
56021
56308
|
const preferUseEffectEvent = defineRule({
|
|
56022
56309
|
id: "prefer-use-effect-event",
|
|
56023
56310
|
title: "Effect re-subscribes on a changing callback",
|
|
56024
|
-
requires: ["react:19"],
|
|
56311
|
+
requires: ["react:19.2"],
|
|
56025
56312
|
tags: ["test-noise"],
|
|
56026
56313
|
severity: "warn",
|
|
56027
|
-
recommendation: "Wrap the callback with `useEffectEvent(callback)` (React 19+) and call it inside the sub-handler. An Effect Event always sees the latest props and state but isn't a dependency, so the effect won't re-subscribe every time the parent redraws. See https://react.dev/reference/react/useEffectEvent",
|
|
56314
|
+
recommendation: "Wrap the callback with `useEffectEvent(callback)` (React 19.2+) and call it inside the sub-handler. An Effect Event always sees the latest props and state but isn't a dependency, so the effect won't re-subscribe every time the parent redraws. See https://react.dev/reference/react/useEffectEvent",
|
|
56028
56315
|
create: (context) => {
|
|
56029
56316
|
const checkComponent = (componentBody) => {
|
|
56030
56317
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
@@ -57629,7 +57916,6 @@ const reduxUseselectorInlineDerivation = defineRule({
|
|
|
57629
57916
|
title: "useSelector derives data inline",
|
|
57630
57917
|
severity: "warn",
|
|
57631
57918
|
category: "Performance",
|
|
57632
|
-
disabledWhen: ["react-compiler"],
|
|
57633
57919
|
recommendation: "Select the raw slice and memoize derivation so Redux actions do not rebuild a collection and redraw this component.",
|
|
57634
57920
|
create: (context) => {
|
|
57635
57921
|
let aliases = /* @__PURE__ */ new Set();
|
|
@@ -57689,7 +57975,6 @@ const reduxUseselectorReturnsNewCollection = defineRule({
|
|
|
57689
57975
|
title: "useSelector returns a new collection",
|
|
57690
57976
|
severity: "warn",
|
|
57691
57977
|
category: "Performance",
|
|
57692
|
-
disabledWhen: ["react-compiler"],
|
|
57693
57978
|
recommendation: "Return a stable selected value, split selectors, or pass `shallowEqual` so every Redux action does not redraw this component.",
|
|
57694
57979
|
create: (context) => {
|
|
57695
57980
|
let aliases = /* @__PURE__ */ new Set();
|
|
@@ -57815,6 +58100,7 @@ const renderingHoistJsx = defineRule({
|
|
|
57815
58100
|
title: "Constant JSX rebuilt each render",
|
|
57816
58101
|
tags: ["test-noise"],
|
|
57817
58102
|
severity: "warn",
|
|
58103
|
+
disabledWhen: ["react-compiler"],
|
|
57818
58104
|
recommendation: "Move the static JSX out to the top of the file: `const ICON = <svg>...</svg>`, so it isn't rebuilt on every render",
|
|
57819
58105
|
create: (context) => {
|
|
57820
58106
|
let componentDepth = 0;
|
|
@@ -58261,8 +58547,9 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
58261
58547
|
id: "rendering-hydration-no-flicker",
|
|
58262
58548
|
title: "useEffect setState flashes on mount",
|
|
58263
58549
|
tags: ["test-noise"],
|
|
58550
|
+
requires: ["ssr"],
|
|
58264
58551
|
severity: "warn",
|
|
58265
|
-
recommendation: "
|
|
58552
|
+
recommendation: "Read the value with `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` inside a reusable hook, or add `suppressHydrationWarning` to the element",
|
|
58266
58553
|
create: (context) => ({ CallExpression(node) {
|
|
58267
58554
|
if (!isHookCall$2(node, USE_EFFECT_ONLY) || (node.arguments?.length ?? 0) < 2) return;
|
|
58268
58555
|
const depsNode = node.arguments[1];
|
|
@@ -58715,7 +59002,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
58715
59002
|
title: "Loading useState forces extra render",
|
|
58716
59003
|
tags: ["test-noise"],
|
|
58717
59004
|
severity: "warn",
|
|
58718
|
-
recommendation: "Replace with `const [isPending, startTransition] = useTransition()`, which
|
|
59005
|
+
recommendation: "Replace with `const [isPending, startTransition] = useTransition()`, which marks the update as non-urgent and interruptible so the input stays responsive",
|
|
58719
59006
|
create: (context) => ({ VariableDeclarator(node) {
|
|
58720
59007
|
if (!isNodeOfType(node.id, "ArrayPattern") || !node.id.elements?.length) return;
|
|
58721
59008
|
if (!node.init || !isHookCall$2(node.init, "useState")) return;
|
|
@@ -58747,7 +59034,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
58747
59034
|
}
|
|
58748
59035
|
context.report({
|
|
58749
59036
|
node: node.init,
|
|
58750
|
-
message: `This
|
|
59037
|
+
message: `This makes the "${stateVariableName}" update urgent and blocking because it's a plain useState flag, so if it's a state change & not a data fetch, use useTransition to keep the UI responsive while it runs`
|
|
58751
59038
|
});
|
|
58752
59039
|
} })
|
|
58753
59040
|
});
|
|
@@ -59062,6 +59349,7 @@ const rerenderDependencies = defineRule({
|
|
|
59062
59349
|
title: "Unstable value recreated every render",
|
|
59063
59350
|
tags: ["test-noise"],
|
|
59064
59351
|
severity: "error",
|
|
59352
|
+
disabledWhen: ["react-compiler"],
|
|
59065
59353
|
recommendation: "Move it into a useMemo, useRef, or a constant outside the component so it stays the same between renders.",
|
|
59066
59354
|
create: (context) => ({ CallExpression(node) {
|
|
59067
59355
|
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
|
|
@@ -59474,7 +59762,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
59474
59762
|
title: "useMemo before an early return",
|
|
59475
59763
|
tags: ["test-noise"],
|
|
59476
59764
|
severity: "warn",
|
|
59477
|
-
recommendation: "Move the JSX into a child component
|
|
59765
|
+
recommendation: "Move the JSX into a child component rendered after the early return, so renders that take the early return never build it",
|
|
59478
59766
|
create: (context) => {
|
|
59479
59767
|
const inspectFunctionBody = (statements) => {
|
|
59480
59768
|
let memoNode = null;
|
|
@@ -59500,7 +59788,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
59500
59788
|
if (callbackGuardTests.some((guardTest) => areConditionsStructurallyEqual(stmt.test, guardTest))) continue;
|
|
59501
59789
|
context.report({
|
|
59502
59790
|
node: memoNode,
|
|
59503
|
-
message: "This
|
|
59791
|
+
message: "This rebuilds the JSX whenever its dependencies change even on renders that take the early return, so move the JSX into a child component rendered after the early return to skip it"
|
|
59504
59792
|
});
|
|
59505
59793
|
return;
|
|
59506
59794
|
}
|
|
@@ -60003,6 +60291,7 @@ const rerenderMemoWithDefaultValue = defineRule({
|
|
|
60003
60291
|
title: "Empty default prop breaks memo",
|
|
60004
60292
|
tags: ["test-noise"],
|
|
60005
60293
|
severity: "warn",
|
|
60294
|
+
disabledWhen: ["react-compiler"],
|
|
60006
60295
|
recommendation: "Move it to the top of the file: `const EMPTY_ITEMS: Item[] = []`, then use that as the default value",
|
|
60007
60296
|
create: (context) => {
|
|
60008
60297
|
let memoRegistry = /* @__PURE__ */ new Map();
|
|
@@ -60828,6 +61117,7 @@ const rnListDataMapped = defineRule({
|
|
|
60828
61117
|
title: "List data rebuilt every render",
|
|
60829
61118
|
tags: ["test-noise"],
|
|
60830
61119
|
requires: ["react-native"],
|
|
61120
|
+
disabledWhen: ["react-compiler"],
|
|
60831
61121
|
severity: "warn",
|
|
60832
61122
|
recommendation: "This builds a new array each time the parent redraws, so every row redraws too. Wrap it in `useMemo(() => items.map(...), [items])` to keep the same array.",
|
|
60833
61123
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
@@ -63181,6 +63471,66 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
63181
63471
|
} })
|
|
63182
63472
|
});
|
|
63183
63473
|
//#endregion
|
|
63474
|
+
//#region src/plugin/constants/global-aria-properties.ts
|
|
63475
|
+
const GLOBAL_ARIA_PROPERTIES = new Set([
|
|
63476
|
+
"aria-atomic",
|
|
63477
|
+
"aria-braillelabel",
|
|
63478
|
+
"aria-brailleroledescription",
|
|
63479
|
+
"aria-busy",
|
|
63480
|
+
"aria-controls",
|
|
63481
|
+
"aria-current",
|
|
63482
|
+
"aria-describedby",
|
|
63483
|
+
"aria-description",
|
|
63484
|
+
"aria-details",
|
|
63485
|
+
"aria-disabled",
|
|
63486
|
+
"aria-dropeffect",
|
|
63487
|
+
"aria-errormessage",
|
|
63488
|
+
"aria-flowto",
|
|
63489
|
+
"aria-grabbed",
|
|
63490
|
+
"aria-haspopup",
|
|
63491
|
+
"aria-hidden",
|
|
63492
|
+
"aria-invalid",
|
|
63493
|
+
"aria-keyshortcuts",
|
|
63494
|
+
"aria-label",
|
|
63495
|
+
"aria-labelledby",
|
|
63496
|
+
"aria-live",
|
|
63497
|
+
"aria-owns",
|
|
63498
|
+
"aria-relevant",
|
|
63499
|
+
"aria-roledescription"
|
|
63500
|
+
]);
|
|
63501
|
+
//#endregion
|
|
63502
|
+
//#region src/plugin/constants/prohibited-aria-properties-by-role.ts
|
|
63503
|
+
const ACCESSIBLE_NAME_PROPERTIES = new Set([
|
|
63504
|
+
"aria-braillelabel",
|
|
63505
|
+
"aria-label",
|
|
63506
|
+
"aria-labelledby"
|
|
63507
|
+
]);
|
|
63508
|
+
const PROHIBITED_ARIA_PROPERTIES_BY_ROLE = {
|
|
63509
|
+
caption: ACCESSIBLE_NAME_PROPERTIES,
|
|
63510
|
+
code: ACCESSIBLE_NAME_PROPERTIES,
|
|
63511
|
+
definition: ACCESSIBLE_NAME_PROPERTIES,
|
|
63512
|
+
deletion: ACCESSIBLE_NAME_PROPERTIES,
|
|
63513
|
+
emphasis: ACCESSIBLE_NAME_PROPERTIES,
|
|
63514
|
+
generic: new Set([
|
|
63515
|
+
"aria-braillelabel",
|
|
63516
|
+
"aria-brailleroledescription",
|
|
63517
|
+
"aria-label",
|
|
63518
|
+
"aria-labelledby",
|
|
63519
|
+
"aria-roledescription"
|
|
63520
|
+
]),
|
|
63521
|
+
insertion: ACCESSIBLE_NAME_PROPERTIES,
|
|
63522
|
+
mark: ACCESSIBLE_NAME_PROPERTIES,
|
|
63523
|
+
none: ACCESSIBLE_NAME_PROPERTIES,
|
|
63524
|
+
paragraph: ACCESSIBLE_NAME_PROPERTIES,
|
|
63525
|
+
presentation: ACCESSIBLE_NAME_PROPERTIES,
|
|
63526
|
+
strong: ACCESSIBLE_NAME_PROPERTIES,
|
|
63527
|
+
subscript: ACCESSIBLE_NAME_PROPERTIES,
|
|
63528
|
+
superscript: ACCESSIBLE_NAME_PROPERTIES,
|
|
63529
|
+
term: ACCESSIBLE_NAME_PROPERTIES,
|
|
63530
|
+
time: ACCESSIBLE_NAME_PROPERTIES,
|
|
63531
|
+
tooltip: ACCESSIBLE_NAME_PROPERTIES
|
|
63532
|
+
};
|
|
63533
|
+
//#endregion
|
|
63184
63534
|
//#region src/plugin/constants/role-supports-aria-props.ts
|
|
63185
63535
|
const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
63186
63536
|
alert: new Set([
|
|
@@ -66281,6 +66631,12 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
66281
66631
|
])
|
|
66282
66632
|
};
|
|
66283
66633
|
//#endregion
|
|
66634
|
+
//#region src/plugin/utils/is-aria-property-supported-by-role.ts
|
|
66635
|
+
const isAriaPropertySupportedByRole = (role, property) => {
|
|
66636
|
+
if (PROHIBITED_ARIA_PROPERTIES_BY_ROLE[role]?.has(property)) return false;
|
|
66637
|
+
return ROLE_SUPPORTS_ARIA_PROPS[role]?.has(property) === true || GLOBAL_ARIA_PROPERTIES.has(property);
|
|
66638
|
+
};
|
|
66639
|
+
//#endregion
|
|
66284
66640
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
66285
66641
|
const buildMessageDefault = (roles, propName) => {
|
|
66286
66642
|
return `Screen reader users get no help from \`${propName}\` because role ${roles.map((role) => `\`${role}\``).join(" / ")} ignores it, so remove it or change the role.`;
|
|
@@ -66317,16 +66673,10 @@ const roleSupportsAriaProps = defineRule({
|
|
|
66317
66673
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
66318
66674
|
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType, context.scopes)].filter((role) => role !== null);
|
|
66319
66675
|
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
66320
|
-
const
|
|
66321
|
-
for (const role of roleCandidates) {
|
|
66322
|
-
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
66323
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
66324
|
-
if (!supported) return;
|
|
66325
|
-
supportedSets.push(supported);
|
|
66326
|
-
}
|
|
66676
|
+
for (const role of roleCandidates) if (!VALID_ARIA_ROLES.has(role)) return;
|
|
66327
66677
|
const isImplicit = !roleAttribute;
|
|
66328
66678
|
for (const { attribute, propName } of ariaAttributes) {
|
|
66329
|
-
if (
|
|
66679
|
+
if (roleCandidates.some((role) => isAriaPropertySupportedByRole(role, propName))) continue;
|
|
66330
66680
|
context.report({
|
|
66331
66681
|
node: attribute,
|
|
66332
66682
|
message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
|
|
@@ -75108,7 +75458,7 @@ const EXTERNAL_RULES = [
|
|
|
75108
75458
|
{
|
|
75109
75459
|
key: "react-hooks-js/set-state-in-effect",
|
|
75110
75460
|
source: "react-compiler",
|
|
75111
|
-
severity: "
|
|
75461
|
+
severity: "warn"
|
|
75112
75462
|
},
|
|
75113
75463
|
{
|
|
75114
75464
|
key: "react-hooks-js/globals",
|