oxlint-plugin-react-doctor 0.7.2-dev.08b768b → 0.7.2-dev.0eb5293
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.js +292 -602
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -532,12 +532,6 @@ const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
|
|
|
532
532
|
]);
|
|
533
533
|
const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
|
|
534
534
|
const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
|
|
535
|
-
const SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP = new Set([
|
|
536
|
-
"WebSocket",
|
|
537
|
-
"EventSource",
|
|
538
|
-
"BroadcastChannel",
|
|
539
|
-
"RTCPeerConnection"
|
|
540
|
-
]);
|
|
541
535
|
const MUTABLE_GLOBAL_ROOTS = new Set([
|
|
542
536
|
"location",
|
|
543
537
|
"window",
|
|
@@ -601,19 +595,6 @@ const TRIVIAL_INITIALIZER_NAMES = new Set([
|
|
|
601
595
|
"parseInt",
|
|
602
596
|
"parseFloat"
|
|
603
597
|
]);
|
|
604
|
-
const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
|
|
605
|
-
"Date",
|
|
606
|
-
"Map",
|
|
607
|
-
"Set",
|
|
608
|
-
"WeakMap",
|
|
609
|
-
"WeakSet",
|
|
610
|
-
"WeakRef",
|
|
611
|
-
"RegExp",
|
|
612
|
-
"Error",
|
|
613
|
-
"URL",
|
|
614
|
-
"URLSearchParams",
|
|
615
|
-
"AbortController"
|
|
616
|
-
]);
|
|
617
598
|
const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
|
|
618
599
|
"Boolean",
|
|
619
600
|
"String",
|
|
@@ -693,10 +674,7 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
|
|
|
693
674
|
"unwatch",
|
|
694
675
|
"unlisten",
|
|
695
676
|
"unsub",
|
|
696
|
-
"abort"
|
|
697
|
-
"disconnect",
|
|
698
|
-
"unobserve",
|
|
699
|
-
"close"
|
|
677
|
+
"abort"
|
|
700
678
|
]);
|
|
701
679
|
const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
|
|
702
680
|
"remove",
|
|
@@ -2257,38 +2235,6 @@ const anchorHasContent = defineRule({
|
|
|
2257
2235
|
}
|
|
2258
2236
|
});
|
|
2259
2237
|
//#endregion
|
|
2260
|
-
//#region src/plugin/utils/get-jsx-prop-static-string-values.ts
|
|
2261
|
-
const MAX_CONST_RESOLUTION_HOPS = 4;
|
|
2262
|
-
const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
|
|
2263
|
-
const expression = stripParenExpression(rawExpression);
|
|
2264
|
-
if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
|
|
2265
|
-
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2266
|
-
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2267
|
-
return staticValue === null ? null : [staticValue];
|
|
2268
|
-
}
|
|
2269
|
-
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
2270
|
-
const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
|
|
2271
|
-
if (consequentValues === null) return null;
|
|
2272
|
-
const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
|
|
2273
|
-
if (alternateValues === null) return null;
|
|
2274
|
-
return [...consequentValues, ...alternateValues];
|
|
2275
|
-
}
|
|
2276
|
-
if (isNodeOfType(expression, "Identifier")) {
|
|
2277
|
-
if (remainingHops === 0) return null;
|
|
2278
|
-
const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
|
|
2279
|
-
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
|
|
2280
|
-
return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
|
|
2281
|
-
}
|
|
2282
|
-
return null;
|
|
2283
|
-
};
|
|
2284
|
-
const getJsxPropStaticStringValues = (attribute, scopes) => {
|
|
2285
|
-
const value = attribute.value;
|
|
2286
|
-
if (!value) return null;
|
|
2287
|
-
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? [value.value] : null;
|
|
2288
|
-
if (isNodeOfType(value, "JSXExpressionContainer")) return resolveStaticStringValues(value.expression, scopes, MAX_CONST_RESOLUTION_HOPS);
|
|
2289
|
-
return null;
|
|
2290
|
-
};
|
|
2291
|
-
//#endregion
|
|
2292
2238
|
//#region src/plugin/rules/a11y/anchor-is-valid.ts
|
|
2293
2239
|
const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
|
|
2294
2240
|
const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
|
|
@@ -2318,11 +2264,28 @@ const isInvalidHref = (value, validHrefs) => {
|
|
|
2318
2264
|
if (validHrefs.has(value)) return false;
|
|
2319
2265
|
return value === "" || value === "#" || value === "javascript:void(0)";
|
|
2320
2266
|
};
|
|
2321
|
-
const
|
|
2267
|
+
const getStaticHrefValue = (value) => {
|
|
2268
|
+
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value : null;
|
|
2269
|
+
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2270
|
+
const expression = value.expression;
|
|
2271
|
+
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
|
|
2272
|
+
if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression);
|
|
2273
|
+
}
|
|
2274
|
+
return null;
|
|
2275
|
+
};
|
|
2276
|
+
const checkValueIsEmptyOrInvalid = (value, validHrefs) => {
|
|
2277
|
+
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
|
|
2322
2278
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2323
2279
|
const expression = value.expression;
|
|
2324
2280
|
if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
|
|
2325
|
-
if (isNodeOfType(expression, "Literal")
|
|
2281
|
+
if (isNodeOfType(expression, "Literal")) {
|
|
2282
|
+
if (expression.value === null) return true;
|
|
2283
|
+
if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
|
|
2284
|
+
}
|
|
2285
|
+
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2286
|
+
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2287
|
+
return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
|
|
2288
|
+
}
|
|
2326
2289
|
}
|
|
2327
2290
|
if (isNodeOfType(value, "JSXFragment")) return true;
|
|
2328
2291
|
return false;
|
|
@@ -2355,10 +2318,9 @@ const anchorIsValid = defineRule({
|
|
|
2355
2318
|
});
|
|
2356
2319
|
return;
|
|
2357
2320
|
}
|
|
2358
|
-
|
|
2359
|
-
if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
|
|
2321
|
+
if (checkValueIsEmptyOrInvalid(hrefAttribute.value, settings.validHrefs)) {
|
|
2360
2322
|
const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
|
|
2361
|
-
if (!hasOnClick &&
|
|
2323
|
+
if (!hasOnClick && getStaticHrefValue(hrefAttribute.value) === "#") return;
|
|
2362
2324
|
context.report({
|
|
2363
2325
|
node: node.name,
|
|
2364
2326
|
message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
|
|
@@ -3414,25 +3376,6 @@ const ariaRole = defineRule({
|
|
|
3414
3376
|
if (!roleAttribute) return;
|
|
3415
3377
|
const elementType = getElementType(node, context.settings);
|
|
3416
3378
|
if (settings.ignoreNonDOM && !HTML_TAGS.has(elementType)) return;
|
|
3417
|
-
const reportFirstInvalidCandidate = (candidates) => {
|
|
3418
|
-
for (const candidate of candidates) {
|
|
3419
|
-
if (candidate.trim().length === 0) {
|
|
3420
|
-
context.report({
|
|
3421
|
-
node: roleAttribute,
|
|
3422
|
-
message: buildBaseMessage("")
|
|
3423
|
-
});
|
|
3424
|
-
return;
|
|
3425
|
-
}
|
|
3426
|
-
const tokens = candidate.split(/\s+/).filter((token) => token.length > 0);
|
|
3427
|
-
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3428
|
-
context.report({
|
|
3429
|
-
node: roleAttribute,
|
|
3430
|
-
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3431
|
-
});
|
|
3432
|
-
return;
|
|
3433
|
-
}
|
|
3434
|
-
}
|
|
3435
|
-
};
|
|
3436
3379
|
const value = roleAttribute.value;
|
|
3437
3380
|
if (!value) {
|
|
3438
3381
|
context.report({
|
|
@@ -3449,7 +3392,22 @@ const ariaRole = defineRule({
|
|
|
3449
3392
|
});
|
|
3450
3393
|
return;
|
|
3451
3394
|
}
|
|
3452
|
-
|
|
3395
|
+
const stringValue = value.value;
|
|
3396
|
+
if (stringValue.trim().length === 0) {
|
|
3397
|
+
context.report({
|
|
3398
|
+
node: roleAttribute,
|
|
3399
|
+
message: buildBaseMessage("")
|
|
3400
|
+
});
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
const tokens = stringValue.split(/\s+/).filter((token) => token.length > 0);
|
|
3404
|
+
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3405
|
+
context.report({
|
|
3406
|
+
node: roleAttribute,
|
|
3407
|
+
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3408
|
+
});
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3453
3411
|
return;
|
|
3454
3412
|
}
|
|
3455
3413
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
@@ -3470,11 +3428,6 @@ const ariaRole = defineRule({
|
|
|
3470
3428
|
});
|
|
3471
3429
|
return;
|
|
3472
3430
|
}
|
|
3473
|
-
const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
3474
|
-
if (resolvedCandidates !== null) {
|
|
3475
|
-
reportFirstInvalidCandidate(resolvedCandidates);
|
|
3476
|
-
return;
|
|
3477
|
-
}
|
|
3478
3431
|
return;
|
|
3479
3432
|
}
|
|
3480
3433
|
context.report({
|
|
@@ -7657,95 +7610,6 @@ const displayName = defineRule({
|
|
|
7657
7610
|
}
|
|
7658
7611
|
});
|
|
7659
7612
|
//#endregion
|
|
7660
|
-
//#region src/plugin/utils/is-react-hook-name.ts
|
|
7661
|
-
const isReactHookName = (name) => {
|
|
7662
|
-
if (!name.startsWith("use")) return false;
|
|
7663
|
-
if (name.length === 3) return true;
|
|
7664
|
-
const fourthCharacter = name.charCodeAt(3);
|
|
7665
|
-
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
7666
|
-
};
|
|
7667
|
-
//#endregion
|
|
7668
|
-
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
7669
|
-
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
7670
|
-
//#endregion
|
|
7671
|
-
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
7672
|
-
const hocWrapperCalleeName = (callee) => {
|
|
7673
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
7674
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
7675
|
-
return null;
|
|
7676
|
-
};
|
|
7677
|
-
const displayNameFromFunctionBinding = (functionNode) => {
|
|
7678
|
-
let current = functionNode;
|
|
7679
|
-
for (;;) {
|
|
7680
|
-
const parent = current.parent;
|
|
7681
|
-
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
7682
|
-
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
7683
|
-
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
7684
|
-
current = parent;
|
|
7685
|
-
continue;
|
|
7686
|
-
}
|
|
7687
|
-
}
|
|
7688
|
-
break;
|
|
7689
|
-
}
|
|
7690
|
-
const binding = current.parent;
|
|
7691
|
-
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
7692
|
-
return null;
|
|
7693
|
-
};
|
|
7694
|
-
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
7695
|
-
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
7696
|
-
return displayNameFromFunctionBinding(functionNode);
|
|
7697
|
-
};
|
|
7698
|
-
//#endregion
|
|
7699
|
-
//#region src/plugin/utils/find-enclosing-function.ts
|
|
7700
|
-
const findEnclosingFunction = (node) => {
|
|
7701
|
-
let cursor = node.parent;
|
|
7702
|
-
while (cursor) {
|
|
7703
|
-
if (isFunctionLike$1(cursor)) return cursor;
|
|
7704
|
-
cursor = cursor.parent ?? null;
|
|
7705
|
-
}
|
|
7706
|
-
return null;
|
|
7707
|
-
};
|
|
7708
|
-
//#endregion
|
|
7709
|
-
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
7710
|
-
const enclosingComponentOrHookName = (node) => {
|
|
7711
|
-
const functionNode = findEnclosingFunction(node);
|
|
7712
|
-
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
7713
|
-
};
|
|
7714
|
-
//#endregion
|
|
7715
|
-
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
7716
|
-
const isResultDiscardedCall = (callExpression) => {
|
|
7717
|
-
let node = callExpression;
|
|
7718
|
-
let parent = node.parent;
|
|
7719
|
-
while (parent) {
|
|
7720
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
7721
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
7722
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
7723
|
-
node = parent;
|
|
7724
|
-
parent = node.parent;
|
|
7725
|
-
continue;
|
|
7726
|
-
}
|
|
7727
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
7728
|
-
node = parent;
|
|
7729
|
-
parent = node.parent;
|
|
7730
|
-
continue;
|
|
7731
|
-
}
|
|
7732
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
7733
|
-
node = parent;
|
|
7734
|
-
parent = node.parent;
|
|
7735
|
-
continue;
|
|
7736
|
-
}
|
|
7737
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
7738
|
-
const expressions = parent.expressions ?? [];
|
|
7739
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
7740
|
-
node = parent;
|
|
7741
|
-
parent = node.parent;
|
|
7742
|
-
continue;
|
|
7743
|
-
}
|
|
7744
|
-
return false;
|
|
7745
|
-
}
|
|
7746
|
-
return false;
|
|
7747
|
-
};
|
|
7748
|
-
//#endregion
|
|
7749
7613
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
7750
7614
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
7751
7615
|
if (!node || typeof node !== "object") return;
|
|
@@ -7897,17 +7761,6 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
7897
7761
|
};
|
|
7898
7762
|
//#endregion
|
|
7899
7763
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
7900
|
-
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
7901
|
-
const RESOURCE_NOUN_BY_KIND = {
|
|
7902
|
-
subscribe: "subscription",
|
|
7903
|
-
timer: "timer",
|
|
7904
|
-
socket: "connection"
|
|
7905
|
-
};
|
|
7906
|
-
const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
|
|
7907
|
-
const isSubscribeOrObserveCall = (node) => {
|
|
7908
|
-
if (isSubscribeLikeCallExpression(node)) return true;
|
|
7909
|
-
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
7910
|
-
};
|
|
7911
7764
|
const findSubscribeLikeUsages = (callback) => {
|
|
7912
7765
|
const usages = [];
|
|
7913
7766
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
@@ -7919,14 +7772,6 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7919
7772
|
}
|
|
7920
7773
|
walkAst(callback, (child) => {
|
|
7921
7774
|
if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
|
|
7922
|
-
if (isSocketConstruction(child)) {
|
|
7923
|
-
usages.push({
|
|
7924
|
-
kind: "socket",
|
|
7925
|
-
node: child,
|
|
7926
|
-
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
7927
|
-
});
|
|
7928
|
-
return;
|
|
7929
|
-
}
|
|
7930
7775
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7931
7776
|
if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
|
|
7932
7777
|
usages.push({
|
|
@@ -7936,7 +7781,7 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7936
7781
|
});
|
|
7937
7782
|
return;
|
|
7938
7783
|
}
|
|
7939
|
-
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") &&
|
|
7784
|
+
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
|
|
7940
7785
|
kind: "subscribe",
|
|
7941
7786
|
node: child,
|
|
7942
7787
|
resourceName: child.callee.property.name
|
|
@@ -7959,12 +7804,7 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7959
7804
|
const bindingName = declarator.id.name;
|
|
7960
7805
|
bindings.effectScopeVariableNames.add(bindingName);
|
|
7961
7806
|
const init = declarator.init;
|
|
7962
|
-
if (!init) continue;
|
|
7963
|
-
if (isSocketConstruction(init)) {
|
|
7964
|
-
bindings.subscriptionNames.add(bindingName);
|
|
7965
|
-
continue;
|
|
7966
|
-
}
|
|
7967
|
-
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
7807
|
+
if (!init || !isNodeOfType(init, "CallExpression")) continue;
|
|
7968
7808
|
if (isSubscribeLikeCallExpression(init)) {
|
|
7969
7809
|
bindings.subscriptionNames.add(bindingName);
|
|
7970
7810
|
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
@@ -8016,168 +7856,26 @@ const effectHasCleanupReturn = (callback, usages) => {
|
|
|
8016
7856
|
});
|
|
8017
7857
|
return didFindCleanupReturn;
|
|
8018
7858
|
};
|
|
8019
|
-
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
8020
|
-
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some((property) => isNodeOfType(property, "Property") && isNodeOfType(property.key, "Identifier") && (property.key.name === "signal" || property.key.name === "once" && isNodeOfType(property.value, "Literal") && property.value.value === true)));
|
|
8021
|
-
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
8022
|
-
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
8023
|
-
["addListener", new Set([
|
|
8024
|
-
"removeListener",
|
|
8025
|
-
"off",
|
|
8026
|
-
"abort"
|
|
8027
|
-
])],
|
|
8028
|
-
["on", new Set([
|
|
8029
|
-
"off",
|
|
8030
|
-
"removeListener",
|
|
8031
|
-
"on"
|
|
8032
|
-
])],
|
|
8033
|
-
["subscribe", new Set(["unsubscribe", "unsub"])],
|
|
8034
|
-
["sub", new Set(["unsub", "unsubscribe"])],
|
|
8035
|
-
["watch", new Set(["unwatch", "close"])],
|
|
8036
|
-
["listen", new Set(["unlisten", "close"])],
|
|
8037
|
-
[OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
|
|
8038
|
-
]);
|
|
8039
|
-
const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
8040
|
-
"cleanup",
|
|
8041
|
-
"dispose",
|
|
8042
|
-
"destroy",
|
|
8043
|
-
"teardown"
|
|
8044
|
-
]);
|
|
8045
|
-
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8046
|
-
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
8047
|
-
const getReleaseVerbName = (node) => {
|
|
8048
|
-
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
8049
|
-
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8050
|
-
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8051
|
-
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
8052
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8053
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
8054
|
-
return null;
|
|
8055
|
-
};
|
|
8056
|
-
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
8057
|
-
const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
|
|
8058
|
-
let didFindPairedRelease = false;
|
|
8059
|
-
walkAst(body, (child) => {
|
|
8060
|
-
if (didFindPairedRelease) return false;
|
|
8061
|
-
if (child !== body && isFunctionLike$1(child)) return false;
|
|
8062
|
-
const releaseVerbName = getReleaseVerbName(child);
|
|
8063
|
-
if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
|
|
8064
|
-
didFindPairedRelease = true;
|
|
8065
|
-
return false;
|
|
8066
|
-
}
|
|
8067
|
-
});
|
|
8068
|
-
return didFindPairedRelease;
|
|
8069
|
-
};
|
|
8070
|
-
const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
|
|
8071
|
-
const collectFileReleaseVerbNames = (anyNode) => {
|
|
8072
|
-
let programNode = anyNode;
|
|
8073
|
-
while (programNode.parent) programNode = programNode.parent;
|
|
8074
|
-
const cached = fileReleaseVerbNamesCache.get(programNode);
|
|
8075
|
-
if (cached) return cached;
|
|
8076
|
-
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
8077
|
-
walkAst(programNode, (child) => {
|
|
8078
|
-
const releaseVerbName = getReleaseVerbName(child);
|
|
8079
|
-
if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
|
|
8080
|
-
});
|
|
8081
|
-
fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
|
|
8082
|
-
return releaseVerbNames;
|
|
8083
|
-
};
|
|
8084
|
-
const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
|
|
8085
|
-
const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
|
|
8086
|
-
const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
|
|
8087
|
-
if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
|
|
8088
|
-
for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
|
|
8089
|
-
return false;
|
|
8090
|
-
};
|
|
8091
|
-
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
8092
|
-
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
8093
|
-
const body = retainedFunction.body;
|
|
8094
|
-
if (!body) return null;
|
|
8095
|
-
let leak = null;
|
|
8096
|
-
walkAst(body, (child) => {
|
|
8097
|
-
if (leak !== null) return false;
|
|
8098
|
-
if (isFunctionLike$1(child)) return false;
|
|
8099
|
-
if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
|
|
8100
|
-
leak = {
|
|
8101
|
-
kind: "socket",
|
|
8102
|
-
node: child,
|
|
8103
|
-
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8104
|
-
};
|
|
8105
|
-
return false;
|
|
8106
|
-
}
|
|
8107
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8108
|
-
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
|
|
8109
|
-
leak = {
|
|
8110
|
-
kind: "timer",
|
|
8111
|
-
node: child,
|
|
8112
|
-
resourceName: "setInterval"
|
|
8113
|
-
};
|
|
8114
|
-
return false;
|
|
8115
|
-
}
|
|
8116
|
-
if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
|
|
8117
|
-
const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
|
|
8118
|
-
if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
|
|
8119
|
-
kind: "subscribe",
|
|
8120
|
-
node: child,
|
|
8121
|
-
resourceName: registrationVerbName
|
|
8122
|
-
};
|
|
8123
|
-
return false;
|
|
8124
|
-
}
|
|
8125
|
-
});
|
|
8126
|
-
return leak;
|
|
8127
|
-
};
|
|
8128
|
-
const isRetainedComponentScopeFunction = (functionNode) => {
|
|
8129
|
-
if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
|
|
8130
|
-
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
8131
|
-
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
8132
|
-
return enclosingComponentOrHookName(functionNode) !== null;
|
|
8133
|
-
};
|
|
8134
7859
|
const effectNeedsCleanup = defineRule({
|
|
8135
7860
|
id: "effect-needs-cleanup",
|
|
8136
7861
|
title: "Effect subscription or timer never cleaned up",
|
|
8137
7862
|
severity: "error",
|
|
8138
7863
|
tags: ["test-noise"],
|
|
8139
|
-
recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers,
|
|
8140
|
-
create: (context) => {
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
8155
|
-
return;
|
|
8156
|
-
}
|
|
8157
|
-
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
8158
|
-
const callback = getEffectCallback(node);
|
|
8159
|
-
if (!callback) return;
|
|
8160
|
-
const usages = findSubscribeLikeUsages(callback);
|
|
8161
|
-
if (usages.length === 0) return;
|
|
8162
|
-
if (effectHasCleanupReturn(callback, usages)) return;
|
|
8163
|
-
const firstUsage = usages[0];
|
|
8164
|
-
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
8165
|
-
context.report({
|
|
8166
|
-
node,
|
|
8167
|
-
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
8168
|
-
});
|
|
8169
|
-
},
|
|
8170
|
-
FunctionDeclaration(node) {
|
|
8171
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8172
|
-
},
|
|
8173
|
-
ArrowFunctionExpression(node) {
|
|
8174
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8175
|
-
},
|
|
8176
|
-
FunctionExpression(node) {
|
|
8177
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8178
|
-
}
|
|
8179
|
-
};
|
|
8180
|
-
}
|
|
7864
|
+
recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, or `return unsubscribe` if the subscribe call already gave you one.",
|
|
7865
|
+
create: (context) => ({ CallExpression(node) {
|
|
7866
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
7867
|
+
const callback = getEffectCallback(node);
|
|
7868
|
+
if (!callback) return;
|
|
7869
|
+
const usages = findSubscribeLikeUsages(callback);
|
|
7870
|
+
if (usages.length === 0) return;
|
|
7871
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
7872
|
+
const firstUsage = usages[0];
|
|
7873
|
+
const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
|
|
7874
|
+
context.report({
|
|
7875
|
+
node,
|
|
7876
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
7877
|
+
});
|
|
7878
|
+
} })
|
|
8181
7879
|
});
|
|
8182
7880
|
//#endregion
|
|
8183
7881
|
//#region src/plugin/semantic/scope-analysis.ts
|
|
@@ -8736,6 +8434,17 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
8736
8434
|
return computedCaptures;
|
|
8737
8435
|
};
|
|
8738
8436
|
//#endregion
|
|
8437
|
+
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8438
|
+
const isReactHookName = (name) => {
|
|
8439
|
+
if (!name.startsWith("use")) return false;
|
|
8440
|
+
if (name.length === 3) return true;
|
|
8441
|
+
const fourthCharacter = name.charCodeAt(3);
|
|
8442
|
+
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
8443
|
+
};
|
|
8444
|
+
//#endregion
|
|
8445
|
+
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
8446
|
+
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
8447
|
+
//#endregion
|
|
8739
8448
|
//#region src/plugin/utils/is-react-hoc-callback-argument.ts
|
|
8740
8449
|
const reactHocCalleeName = (callee) => {
|
|
8741
8450
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
@@ -9907,7 +9616,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9907
9616
|
});
|
|
9908
9617
|
//#endregion
|
|
9909
9618
|
//#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
|
|
9910
|
-
const EMPTY_VISITORS$
|
|
9619
|
+
const EMPTY_VISITORS$5 = {};
|
|
9911
9620
|
const NODE_OR_BUILD_FILE = /(\.config\.[cm]?[jt]sx?$)|((^|\/)(scripts|tools|tooling|cli|bin)\/)|(\+(api|html)\.[cm]?[jt]sx?$)|(\.server\.[cm]?[jt]sx?$)|(\.(test|spec)\.[cm]?[jt]sx?$)|((^|\/)__tests__\/)|(\.e2e\.[cm]?[jt]sx?$)/;
|
|
9912
9621
|
const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
|
|
9913
9622
|
const isProcessEnv = (node) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "process" && isNodeOfType(node.property, "Identifier") && node.property.name === "env";
|
|
@@ -9919,7 +9628,7 @@ const expoNoNonInlinedEnv = defineRule({
|
|
|
9919
9628
|
recommendation: "Read env vars with static dotted access (`process.env.EXPO_PUBLIC_NAME`). Computed access and destructuring aren't inlined by babel-preset-expo and resolve to `undefined` at runtime.",
|
|
9920
9629
|
create: (context) => {
|
|
9921
9630
|
const filename = normalizeFilename(context.filename ?? "");
|
|
9922
|
-
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$
|
|
9631
|
+
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$5;
|
|
9923
9632
|
return {
|
|
9924
9633
|
MemberExpression(node) {
|
|
9925
9634
|
if (!node.computed) return;
|
|
@@ -11337,8 +11046,8 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11337
11046
|
if (node.attributes.length === 0) return;
|
|
11338
11047
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
11339
11048
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
11340
|
-
const
|
|
11341
|
-
if (
|
|
11049
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
11050
|
+
if (!role) return;
|
|
11342
11051
|
let hasInteractiveHandler = false;
|
|
11343
11052
|
for (const attribute of node.attributes) {
|
|
11344
11053
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11352,16 +11061,11 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11352
11061
|
const elementType = getElementType(node, context.settings);
|
|
11353
11062
|
if (!HTML_TAGS.has(elementType)) return;
|
|
11354
11063
|
if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
11064
|
+
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11065
|
+
if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
|
|
11355
11066
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
11356
|
-
|
|
11357
|
-
|
|
11358
|
-
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11359
|
-
if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
|
|
11360
|
-
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
11361
|
-
}
|
|
11362
|
-
const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
|
|
11363
|
-
const roleDisplay = roleCandidates.join("' / '");
|
|
11364
|
-
const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
|
|
11067
|
+
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
11068
|
+
const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
|
|
11365
11069
|
context.report({
|
|
11366
11070
|
node,
|
|
11367
11071
|
message
|
|
@@ -11550,6 +11254,16 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
11550
11254
|
return names;
|
|
11551
11255
|
};
|
|
11552
11256
|
//#endregion
|
|
11257
|
+
//#region src/plugin/utils/find-enclosing-function.ts
|
|
11258
|
+
const findEnclosingFunction = (node) => {
|
|
11259
|
+
let cursor = node.parent;
|
|
11260
|
+
while (cursor) {
|
|
11261
|
+
if (isFunctionLike$1(cursor)) return cursor;
|
|
11262
|
+
cursor = cursor.parent ?? null;
|
|
11263
|
+
}
|
|
11264
|
+
return null;
|
|
11265
|
+
};
|
|
11266
|
+
//#endregion
|
|
11553
11267
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
11554
11268
|
const getFunctionBindingIdentifier = (functionNode) => {
|
|
11555
11269
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -21354,7 +21068,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
|
21354
21068
|
const noAdjustStateOnPropChange = defineRule({
|
|
21355
21069
|
id: "no-adjust-state-on-prop-change",
|
|
21356
21070
|
title: "State synced to a prop inside an effect",
|
|
21357
|
-
severity: "
|
|
21071
|
+
severity: "error",
|
|
21358
21072
|
tags: ["test-noise"],
|
|
21359
21073
|
recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
21360
21074
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -22763,10 +22477,6 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
|
|
|
22763
22477
|
const section = manifest[sectionName];
|
|
22764
22478
|
return typeof section === "object" && section !== null && Object.keys(section).length > 0;
|
|
22765
22479
|
});
|
|
22766
|
-
const declaresDependency = (manifest, dependencyName) => {
|
|
22767
|
-
for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
|
|
22768
|
-
return false;
|
|
22769
|
-
};
|
|
22770
22480
|
const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
|
|
22771
22481
|
const classifyPackagePlatform = (filename) => {
|
|
22772
22482
|
const manifest = readNearestPackageManifest(filename);
|
|
@@ -22783,7 +22493,9 @@ const classifyPackagePlatform = (filename) => {
|
|
|
22783
22493
|
return result;
|
|
22784
22494
|
};
|
|
22785
22495
|
//#endregion
|
|
22786
|
-
//#region src/plugin/utils/is-
|
|
22496
|
+
//#region src/plugin/utils/is-react-native-file.ts
|
|
22497
|
+
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22498
|
+
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22787
22499
|
const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
|
|
22788
22500
|
const resolveRealDirectory = (directory) => {
|
|
22789
22501
|
const cached = cachedRealDirectoryByDirectory.get(directory);
|
|
@@ -22804,10 +22516,6 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
|
|
|
22804
22516
|
const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
|
|
22805
22517
|
return realPackageDirectory.startsWith(rootPrefix);
|
|
22806
22518
|
};
|
|
22807
|
-
//#endregion
|
|
22808
|
-
//#region src/plugin/utils/is-react-native-file.ts
|
|
22809
|
-
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22810
|
-
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22811
22519
|
const classifyReactNativeFileTarget = (context) => {
|
|
22812
22520
|
const rawFilename = context.filename;
|
|
22813
22521
|
if (!rawFilename) return "unknown";
|
|
@@ -23224,6 +22932,7 @@ const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
|
23224
22932
|
let fallThroughCount = 0;
|
|
23225
22933
|
let maxTerminatingPathCount = 0;
|
|
23226
22934
|
for (const statement of statements) {
|
|
22935
|
+
if (isFunctionLike$1(statement)) continue;
|
|
23227
22936
|
const guardBranch = isGuardWithTerminatingBranch(statement);
|
|
23228
22937
|
if (guardBranch) {
|
|
23229
22938
|
maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
|
|
@@ -23259,23 +22968,14 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
|
|
|
23259
22968
|
if (!isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
23260
22969
|
return parent.parent === effectCallback.body;
|
|
23261
22970
|
};
|
|
23262
|
-
const countFunctionBodySetStateCalls = (functionNode, context) => {
|
|
23263
|
-
if (isAsyncFunctionLike(functionNode)) return 0;
|
|
23264
|
-
const body = functionNode.body;
|
|
23265
|
-
if (!body) return 0;
|
|
23266
|
-
return countMaxPathSetStateCalls(body, context);
|
|
23267
|
-
};
|
|
23268
22971
|
const countMaxPathSetStateCalls = (node, context) => {
|
|
23269
22972
|
if (!node || typeof node !== "object") return 0;
|
|
23270
|
-
if (
|
|
22973
|
+
if (isAsyncFunctionLike(node)) return 0;
|
|
23271
22974
|
if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
|
|
23272
22975
|
if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
|
|
23273
22976
|
const consequent = node.consequent;
|
|
23274
22977
|
const alternate = node.alternate;
|
|
23275
|
-
|
|
23276
|
-
const thenCount = countMaxPathSetStateCalls(consequent, context);
|
|
23277
|
-
const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
|
|
23278
|
-
return testCount + Math.max(thenCount, elseCount);
|
|
22978
|
+
return countMaxPathSetStateCalls(consequent, context) + (alternate ? countMaxPathSetStateCalls(alternate, context) : 0);
|
|
23279
22979
|
}
|
|
23280
22980
|
if (isNodeOfType(node, "SwitchStatement")) {
|
|
23281
22981
|
let maxRunSetters = 0;
|
|
@@ -23305,31 +23005,28 @@ const countMaxPathSetStateCalls = (node, context) => {
|
|
|
23305
23005
|
}
|
|
23306
23006
|
if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
|
|
23307
23007
|
let nestedSettersInArgs = 0;
|
|
23308
|
-
for (const argument of node.arguments ?? []) nestedSettersInArgs +=
|
|
23008
|
+
for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
|
|
23309
23009
|
return 1 + nestedSettersInArgs;
|
|
23310
23010
|
}
|
|
23311
23011
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
|
|
23312
23012
|
const helperFunction = context.helpersByName.get(node.callee.name);
|
|
23313
23013
|
if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
|
|
23314
23014
|
context.activeHelpers.add(helperFunction);
|
|
23315
|
-
let helperCount =
|
|
23015
|
+
let helperCount = countMaxPathSetStateCalls(helperFunction, context);
|
|
23316
23016
|
context.activeHelpers.delete(helperFunction);
|
|
23317
23017
|
for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
|
|
23318
23018
|
return helperCount;
|
|
23319
23019
|
}
|
|
23320
23020
|
}
|
|
23321
|
-
const
|
|
23322
|
-
if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
|
|
23323
|
-
return countMaxPathSetStateCalls(child, context);
|
|
23324
|
-
};
|
|
23021
|
+
const shouldWalkChild = (child) => !isFunctionLike$1(child) || runsOnEffectDispatch(child);
|
|
23325
23022
|
let total = 0;
|
|
23326
23023
|
const nodeRecord = node;
|
|
23327
23024
|
for (const key of Object.keys(nodeRecord)) {
|
|
23328
23025
|
if (key === "parent") continue;
|
|
23329
23026
|
const child = nodeRecord[key];
|
|
23330
23027
|
if (Array.isArray(child)) {
|
|
23331
|
-
for (const item of child) if (item && typeof item === "object" && "type" in item) total +=
|
|
23332
|
-
} else if (child && typeof child === "object" && "type" in child) total +=
|
|
23028
|
+
for (const item of child) if (item && typeof item === "object" && "type" in item && shouldWalkChild(item)) total += countMaxPathSetStateCalls(item, context);
|
|
23029
|
+
} else if (child && typeof child === "object" && "type" in child && shouldWalkChild(child)) total += countMaxPathSetStateCalls(child, context);
|
|
23333
23030
|
}
|
|
23334
23031
|
return total;
|
|
23335
23032
|
};
|
|
@@ -23378,7 +23075,7 @@ const noCascadingSetState = defineRule({
|
|
|
23378
23075
|
const callback = getEffectCallback(node);
|
|
23379
23076
|
if (!callback) return;
|
|
23380
23077
|
if (isDevOnlyGuardedEffect(callback)) return;
|
|
23381
|
-
const setStateCallCount =
|
|
23078
|
+
const setStateCallCount = countMaxPathSetStateCalls(callback, {
|
|
23382
23079
|
helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
|
|
23383
23080
|
activeHelpers: /* @__PURE__ */ new Set(),
|
|
23384
23081
|
effectCallback: callback
|
|
@@ -23722,6 +23419,40 @@ const noCloneElement = defineRule({
|
|
|
23722
23419
|
} })
|
|
23723
23420
|
});
|
|
23724
23421
|
//#endregion
|
|
23422
|
+
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
23423
|
+
const hocWrapperCalleeName = (callee) => {
|
|
23424
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
23425
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
23426
|
+
return null;
|
|
23427
|
+
};
|
|
23428
|
+
const displayNameFromFunctionBinding = (functionNode) => {
|
|
23429
|
+
let current = functionNode;
|
|
23430
|
+
for (;;) {
|
|
23431
|
+
const parent = current.parent;
|
|
23432
|
+
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
23433
|
+
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
23434
|
+
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
23435
|
+
current = parent;
|
|
23436
|
+
continue;
|
|
23437
|
+
}
|
|
23438
|
+
}
|
|
23439
|
+
break;
|
|
23440
|
+
}
|
|
23441
|
+
const binding = current.parent;
|
|
23442
|
+
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
23443
|
+
return null;
|
|
23444
|
+
};
|
|
23445
|
+
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
23446
|
+
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
23447
|
+
return displayNameFromFunctionBinding(functionNode);
|
|
23448
|
+
};
|
|
23449
|
+
//#endregion
|
|
23450
|
+
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
23451
|
+
const enclosingComponentOrHookName = (node) => {
|
|
23452
|
+
const functionNode = findEnclosingFunction(node);
|
|
23453
|
+
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
23454
|
+
};
|
|
23455
|
+
//#endregion
|
|
23725
23456
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
23726
23457
|
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
23727
23458
|
const CONTEXT_MODULES = [
|
|
@@ -24735,26 +24466,6 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24735
24466
|
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
24736
24467
|
}
|
|
24737
24468
|
};
|
|
24738
|
-
const flattenGuardedStatements = (statements) => {
|
|
24739
|
-
const flattened = [];
|
|
24740
|
-
for (const statement of statements) {
|
|
24741
|
-
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
24742
|
-
flattened.push(statement);
|
|
24743
|
-
continue;
|
|
24744
|
-
}
|
|
24745
|
-
if (isNodeOfType(statement, "IfStatement")) {
|
|
24746
|
-
for (const branch of [statement.consequent, statement.alternate]) {
|
|
24747
|
-
if (!branch) continue;
|
|
24748
|
-
const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
|
|
24749
|
-
if (flattenedBranch === null) return null;
|
|
24750
|
-
flattened.push(...flattenedBranch);
|
|
24751
|
-
}
|
|
24752
|
-
continue;
|
|
24753
|
-
}
|
|
24754
|
-
return null;
|
|
24755
|
-
}
|
|
24756
|
-
return flattened;
|
|
24757
|
-
};
|
|
24758
24469
|
const noDerivedStateEffect = defineRule({
|
|
24759
24470
|
id: "no-derived-state-effect",
|
|
24760
24471
|
title: "Derived state stored in an effect",
|
|
@@ -24786,8 +24497,8 @@ const noDerivedStateEffect = defineRule({
|
|
|
24786
24497
|
}
|
|
24787
24498
|
}
|
|
24788
24499
|
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
24789
|
-
const statements =
|
|
24790
|
-
if (statements
|
|
24500
|
+
const statements = getCallbackStatements(callback);
|
|
24501
|
+
if (statements.length === 0) return;
|
|
24791
24502
|
if (!statements.every((statement) => {
|
|
24792
24503
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24793
24504
|
const expression = statement.expression;
|
|
@@ -26582,11 +26293,7 @@ const noEffectEventInDeps = defineRule({
|
|
|
26582
26293
|
const initializer = declaratorNode.init;
|
|
26583
26294
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
26584
26295
|
if (!isHookCall$2(initializer, "useEffectEvent")) return;
|
|
26585
|
-
if (isNodeOfType(initializer.callee, "Identifier"))
|
|
26586
|
-
if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
|
|
26587
|
-
const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
|
|
26588
|
-
if (calleeSymbol && calleeSymbol.kind !== "import") return;
|
|
26589
|
-
}
|
|
26296
|
+
if (isNodeOfType(initializer.callee, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
|
|
26590
26297
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26591
26298
|
} });
|
|
26592
26299
|
return {
|
|
@@ -29266,7 +28973,7 @@ const extractReturnTypeAnnotation = (returnType) => {
|
|
|
29266
28973
|
const noJsxElementType = defineRule({
|
|
29267
28974
|
id: "no-jsx-element-type",
|
|
29268
28975
|
title: "No JSX.Element",
|
|
29269
|
-
severity: "
|
|
28976
|
+
severity: "error",
|
|
29270
28977
|
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
29271
28978
|
create: (context) => {
|
|
29272
28979
|
let isJsxImported = false;
|
|
@@ -30405,6 +30112,7 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
30405
30112
|
"reverse",
|
|
30406
30113
|
"sort"
|
|
30407
30114
|
]);
|
|
30115
|
+
const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
|
|
30408
30116
|
const OBJECT_MUTATION_METHODS = new Set([
|
|
30409
30117
|
"assign",
|
|
30410
30118
|
"defineProperties",
|
|
@@ -30478,6 +30186,7 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
30478
30186
|
const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
|
|
30479
30187
|
if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
|
|
30480
30188
|
if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30189
|
+
if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30481
30190
|
}
|
|
30482
30191
|
}
|
|
30483
30192
|
if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
|
|
@@ -30516,7 +30225,6 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
30516
30225
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
30517
30226
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
30518
30227
|
if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
|
|
30519
|
-
if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
|
|
30520
30228
|
if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
|
|
30521
30229
|
});
|
|
30522
30230
|
return mutations;
|
|
@@ -30749,7 +30457,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
30749
30457
|
id: "no-nested-component-definition",
|
|
30750
30458
|
title: "Component defined inside another component",
|
|
30751
30459
|
tags: ["test-noise", "react-jsx-only"],
|
|
30752
|
-
severity: "
|
|
30460
|
+
severity: "error",
|
|
30753
30461
|
category: "Correctness",
|
|
30754
30462
|
recommendation: "Move it to module scope or a separate file so React does not recreate the component and erase its state on every parent render.",
|
|
30755
30463
|
create: (context) => {
|
|
@@ -32228,6 +31936,40 @@ const noPreventDefault = defineRule({
|
|
|
32228
31936
|
}
|
|
32229
31937
|
});
|
|
32230
31938
|
//#endregion
|
|
31939
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
31940
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
31941
|
+
let node = callExpression;
|
|
31942
|
+
let parent = node.parent;
|
|
31943
|
+
while (parent) {
|
|
31944
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
31945
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
31946
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
31947
|
+
node = parent;
|
|
31948
|
+
parent = node.parent;
|
|
31949
|
+
continue;
|
|
31950
|
+
}
|
|
31951
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
31952
|
+
node = parent;
|
|
31953
|
+
parent = node.parent;
|
|
31954
|
+
continue;
|
|
31955
|
+
}
|
|
31956
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
31957
|
+
node = parent;
|
|
31958
|
+
parent = node.parent;
|
|
31959
|
+
continue;
|
|
31960
|
+
}
|
|
31961
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
31962
|
+
const expressions = parent.expressions ?? [];
|
|
31963
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
31964
|
+
node = parent;
|
|
31965
|
+
parent = node.parent;
|
|
31966
|
+
continue;
|
|
31967
|
+
}
|
|
31968
|
+
return false;
|
|
31969
|
+
}
|
|
31970
|
+
return false;
|
|
31971
|
+
};
|
|
31972
|
+
//#endregion
|
|
32231
31973
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
|
|
32232
31974
|
const isRefLatchGuardedEffect = (callbackBody) => {
|
|
32233
31975
|
const refNamesReadInGuards = /* @__PURE__ */ new Set();
|
|
@@ -32641,7 +32383,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
32641
32383
|
});
|
|
32642
32384
|
//#endregion
|
|
32643
32385
|
//#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
|
|
32644
|
-
const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]]);
|
|
32386
|
+
const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."], ["useContext", "useContext is replaced by `use()` in React 19, which reads context inside ifs & loops too, so switch to `import { use } from 'react'`."]]);
|
|
32645
32387
|
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
32646
32388
|
if (!rawFilename) return false;
|
|
32647
32389
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
@@ -32679,7 +32421,7 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
32679
32421
|
requires: ["react:19"],
|
|
32680
32422
|
tags: ["test-noise", "migration-hint"],
|
|
32681
32423
|
severity: "warn",
|
|
32682
|
-
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
|
|
32424
|
+
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Replace `useContext(X)` with `use(X)`. Only runs on React 19+ projects.",
|
|
32683
32425
|
create: (context) => {
|
|
32684
32426
|
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
32685
32427
|
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
@@ -33003,11 +32745,34 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
33003
32745
|
});
|
|
33004
32746
|
//#endregion
|
|
33005
32747
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
32748
|
+
const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32749
|
+
if (!symbol || visitedSymbols.has(symbol)) return false;
|
|
32750
|
+
visitedSymbols.add(symbol);
|
|
32751
|
+
if (isComponentParameterSymbol(symbol)) return true;
|
|
32752
|
+
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
32753
|
+
const source = symbol.initializer;
|
|
32754
|
+
if (!source) return false;
|
|
32755
|
+
return initializerRootsInProps(source, scopes, visitedSymbols);
|
|
32756
|
+
};
|
|
32757
|
+
const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32758
|
+
if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
|
|
32759
|
+
if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
|
|
32760
|
+
return rootsInProps(node, scopes, visitedSymbols);
|
|
32761
|
+
};
|
|
32762
|
+
const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32763
|
+
let current = node;
|
|
32764
|
+
while (isNodeOfType(current, "MemberExpression")) {
|
|
32765
|
+
if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
|
|
32766
|
+
current = current.object;
|
|
32767
|
+
}
|
|
32768
|
+
if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
|
|
32769
|
+
return false;
|
|
32770
|
+
};
|
|
33006
32771
|
const isInsideComponentContext = (node) => {
|
|
33007
32772
|
let cursor = node.parent;
|
|
33008
32773
|
while (cursor) {
|
|
32774
|
+
if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
|
|
33009
32775
|
if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
|
|
33010
|
-
if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
|
|
33011
32776
|
cursor = cursor.parent ?? null;
|
|
33012
32777
|
}
|
|
33013
32778
|
return false;
|
|
@@ -33020,21 +32785,21 @@ const functionBodyOf = (node) => {
|
|
|
33020
32785
|
const containsHookCall = (body) => {
|
|
33021
32786
|
let found = false;
|
|
33022
32787
|
walkAst(body, (child) => {
|
|
33023
|
-
if (found) return
|
|
33024
|
-
if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
|
|
32788
|
+
if (found) return;
|
|
33025
32789
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
33026
32790
|
const name = getCalleeName$2(child);
|
|
33027
32791
|
if (name && isReactHookName(name)) found = true;
|
|
33028
32792
|
});
|
|
33029
32793
|
return found;
|
|
33030
32794
|
};
|
|
33031
|
-
const
|
|
32795
|
+
const isModuleScopeHookFreeHelper = (symbol) => {
|
|
33032
32796
|
if (!symbol) return false;
|
|
33033
32797
|
const declaration = symbol.declarationNode;
|
|
33034
32798
|
if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
|
|
33035
32799
|
const body = functionBodyOf(declaration);
|
|
33036
32800
|
if (!body) return false;
|
|
33037
|
-
|
|
32801
|
+
if (findEnclosingFunction(declaration) !== null) return false;
|
|
32802
|
+
return !containsHookCall(body);
|
|
33038
32803
|
};
|
|
33039
32804
|
const noRenderInRender = defineRule({
|
|
33040
32805
|
id: "no-render-in-render",
|
|
@@ -33045,11 +32810,18 @@ const noRenderInRender = defineRule({
|
|
|
33045
32810
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
33046
32811
|
const expression = node.expression;
|
|
33047
32812
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
33048
|
-
|
|
33049
|
-
|
|
33050
|
-
if (
|
|
32813
|
+
let calleeName = null;
|
|
32814
|
+
if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
|
|
32815
|
+
else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
|
|
32816
|
+
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
33051
32817
|
if (!isInsideComponentContext(node)) return;
|
|
33052
|
-
if (
|
|
32818
|
+
if (isNodeOfType(expression.callee, "Identifier")) {
|
|
32819
|
+
const calleeSymbol = context.scopes.symbolFor(expression.callee);
|
|
32820
|
+
if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
|
|
32821
|
+
if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
|
|
32822
|
+
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
32823
|
+
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
32824
|
+
}
|
|
33053
32825
|
context.report({
|
|
33054
32826
|
node: expression,
|
|
33055
32827
|
message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
|
|
@@ -36398,28 +36170,6 @@ const isTriviallyCheapExpression = (node) => {
|
|
|
36398
36170
|
if (isNodeOfType(innerExpression, "MemberExpression")) return false;
|
|
36399
36171
|
return true;
|
|
36400
36172
|
};
|
|
36401
|
-
const isTrivialContainerLiteral = (node) => {
|
|
36402
|
-
if (!node) return false;
|
|
36403
|
-
const innerExpression = stripParenExpression(node);
|
|
36404
|
-
if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
|
|
36405
|
-
if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
|
|
36406
|
-
return false;
|
|
36407
|
-
};
|
|
36408
|
-
const isNonEscapingRead = (identifier) => {
|
|
36409
|
-
const parentNode = identifier.parent;
|
|
36410
|
-
return isNodeOfType(parentNode, "MemberExpression") && parentNode.object === identifier;
|
|
36411
|
-
};
|
|
36412
|
-
const isMemoIdentityUnused = (memoCallNode, scopes) => {
|
|
36413
|
-
const parentNode = memoCallNode.parent;
|
|
36414
|
-
if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
|
|
36415
|
-
if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoCallNode) return false;
|
|
36416
|
-
const bindingTarget = parentNode.id;
|
|
36417
|
-
if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
|
|
36418
|
-
if (!isNodeOfType(bindingTarget, "Identifier")) return false;
|
|
36419
|
-
const symbol = scopes.symbolFor(bindingTarget);
|
|
36420
|
-
if (!symbol) return false;
|
|
36421
|
-
return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
|
|
36422
|
-
};
|
|
36423
36173
|
const noUsememoSimpleExpression = defineRule({
|
|
36424
36174
|
id: "no-usememo-simple-expression",
|
|
36425
36175
|
title: "useMemo on a cheap value",
|
|
@@ -36441,17 +36191,9 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
36441
36191
|
let returnExpression = null;
|
|
36442
36192
|
if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
|
|
36443
36193
|
else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
|
|
36444
|
-
if (
|
|
36445
|
-
if (isTriviallyCheapExpression(returnExpression)) {
|
|
36446
|
-
context.report({
|
|
36447
|
-
node,
|
|
36448
|
-
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
36449
|
-
});
|
|
36450
|
-
return;
|
|
36451
|
-
}
|
|
36452
|
-
if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
|
|
36194
|
+
if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
|
|
36453
36195
|
node,
|
|
36454
|
-
message: "This
|
|
36196
|
+
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
36455
36197
|
});
|
|
36456
36198
|
} })
|
|
36457
36199
|
});
|
|
@@ -36833,7 +36575,8 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
|
|
|
36833
36575
|
const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
|
|
36834
36576
|
const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
|
|
36835
36577
|
const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
|
|
36836
|
-
const
|
|
36578
|
+
const LOCAL_COMPONENT_MESSAGE = "This component is not exported, so Fast Refresh skips it and local edits can full-reload.";
|
|
36579
|
+
const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
|
|
36837
36580
|
const DEFAULT_REACT_HOCS = [
|
|
36838
36581
|
"memo",
|
|
36839
36582
|
"forwardRef",
|
|
@@ -36904,20 +36647,6 @@ const isReactComponentInitializer = (expression, state) => {
|
|
|
36904
36647
|
if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
|
|
36905
36648
|
return false;
|
|
36906
36649
|
};
|
|
36907
|
-
const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
36908
|
-
for (const property of objectExpression.properties ?? []) {
|
|
36909
|
-
if (!isNodeOfType(property, "Property")) continue;
|
|
36910
|
-
const value = skipTsExpression(property.value);
|
|
36911
|
-
if (isNodeOfType(value, "Identifier")) {
|
|
36912
|
-
if (state.localComponentNames.has(value.name)) return true;
|
|
36913
|
-
continue;
|
|
36914
|
-
}
|
|
36915
|
-
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
36916
|
-
if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
|
|
36917
|
-
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
36918
|
-
}
|
|
36919
|
-
return false;
|
|
36920
|
-
};
|
|
36921
36650
|
const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
36922
36651
|
if (initializer) {
|
|
36923
36652
|
const expression = skipTsExpression(initializer);
|
|
@@ -36948,10 +36677,6 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
|
36948
36677
|
reportNode
|
|
36949
36678
|
};
|
|
36950
36679
|
}
|
|
36951
|
-
if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
|
|
36952
|
-
kind: "namespace-object",
|
|
36953
|
-
reportNode
|
|
36954
|
-
};
|
|
36955
36680
|
if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
|
|
36956
36681
|
kind: "non-component",
|
|
36957
36682
|
reportNode
|
|
@@ -36968,7 +36693,7 @@ const collectRelevantNodes = (programRoot) => {
|
|
|
36968
36693
|
walkAst(programRoot, (child) => {
|
|
36969
36694
|
const childType = child.type;
|
|
36970
36695
|
if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
|
|
36971
|
-
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator"
|
|
36696
|
+
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
|
|
36972
36697
|
});
|
|
36973
36698
|
return {
|
|
36974
36699
|
exportNodes,
|
|
@@ -37013,31 +36738,18 @@ const onlyExportComponents = defineRule({
|
|
|
37013
36738
|
category: "Architecture",
|
|
37014
36739
|
create: (context) => {
|
|
37015
36740
|
const settings = resolveSettings$6(context.settings);
|
|
36741
|
+
const state = {
|
|
36742
|
+
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
36743
|
+
allowExportNames: new Set(settings.allowExportNames),
|
|
36744
|
+
allowConstantExport: settings.allowConstantExport
|
|
36745
|
+
};
|
|
37016
36746
|
return { Program(node) {
|
|
37017
36747
|
if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
|
|
37018
36748
|
const { exportNodes, componentCandidates } = collectRelevantNodes(node);
|
|
37019
|
-
const localComponentNames = /* @__PURE__ */ new Set();
|
|
37020
|
-
const state = {
|
|
37021
|
-
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
37022
|
-
allowExportNames: new Set(settings.allowExportNames),
|
|
37023
|
-
allowConstantExport: settings.allowConstantExport,
|
|
37024
|
-
localComponentNames
|
|
37025
|
-
};
|
|
37026
|
-
for (const child of componentCandidates) {
|
|
37027
|
-
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
37028
|
-
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37029
|
-
}
|
|
37030
|
-
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
37031
|
-
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37032
|
-
}
|
|
37033
|
-
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
37034
|
-
const initializer = child.init;
|
|
37035
|
-
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37036
|
-
}
|
|
37037
|
-
}
|
|
37038
36749
|
const exports = [];
|
|
37039
36750
|
let hasReactExport = false;
|
|
37040
36751
|
let hasAnyExports = false;
|
|
36752
|
+
const localComponents = [];
|
|
37041
36753
|
const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
|
|
37042
36754
|
for (const child of exportNodes) {
|
|
37043
36755
|
if (isNodeOfType(child, "ExportAllDeclaration")) {
|
|
@@ -37108,14 +36820,7 @@ const onlyExportComponents = defineRule({
|
|
|
37108
36820
|
});
|
|
37109
36821
|
continue;
|
|
37110
36822
|
}
|
|
37111
|
-
if (isNodeOfType(stripped, "ObjectExpression")) {
|
|
37112
|
-
context.report({
|
|
37113
|
-
node: stripped,
|
|
37114
|
-
message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
|
|
37115
|
-
});
|
|
37116
|
-
continue;
|
|
37117
|
-
}
|
|
37118
|
-
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
|
|
36823
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "Literal")) {
|
|
37119
36824
|
context.report({
|
|
37120
36825
|
node: stripped,
|
|
37121
36826
|
message: ANONYMOUS_MESSAGE
|
|
@@ -37168,23 +36873,32 @@ const onlyExportComponents = defineRule({
|
|
|
37168
36873
|
let entry;
|
|
37169
36874
|
if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
|
|
37170
36875
|
else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
|
|
37171
|
-
else {
|
|
37172
|
-
|
|
37173
|
-
|
|
37174
|
-
|
|
37175
|
-
};
|
|
37176
|
-
if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
|
|
37177
|
-
}
|
|
36876
|
+
else entry = {
|
|
36877
|
+
kind: "non-component",
|
|
36878
|
+
reportNode
|
|
36879
|
+
};
|
|
37178
36880
|
if (isReExportFromSource && entry.kind !== "react-component") continue;
|
|
37179
36881
|
exports.push(entry);
|
|
37180
36882
|
}
|
|
37181
36883
|
}
|
|
37182
36884
|
}
|
|
37183
36885
|
for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
|
|
37184
|
-
|
|
37185
|
-
node
|
|
37186
|
-
|
|
37187
|
-
|
|
36886
|
+
const isInsideExport = (node) => {
|
|
36887
|
+
let walker = node.parent;
|
|
36888
|
+
while (walker) {
|
|
36889
|
+
if (isNodeOfType(walker, "ExportNamedDeclaration") || isNodeOfType(walker, "ExportDefaultDeclaration") || isNodeOfType(walker, "ExportAllDeclaration")) return true;
|
|
36890
|
+
walker = walker.parent ?? null;
|
|
36891
|
+
}
|
|
36892
|
+
return false;
|
|
36893
|
+
};
|
|
36894
|
+
for (const child of componentCandidates) {
|
|
36895
|
+
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
36896
|
+
if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
36897
|
+
}
|
|
36898
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
36899
|
+
if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
36900
|
+
}
|
|
36901
|
+
}
|
|
37188
36902
|
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
37189
36903
|
if (entry.kind === "non-component") context.report({
|
|
37190
36904
|
node: entry.reportNode,
|
|
@@ -37195,6 +36909,14 @@ const onlyExportComponents = defineRule({
|
|
|
37195
36909
|
message: REACT_CONTEXT_MESSAGE
|
|
37196
36910
|
});
|
|
37197
36911
|
}
|
|
36912
|
+
else if (hasAnyExports && !hasReactExport && localComponents.length > 0) for (const local of localComponents) context.report({
|
|
36913
|
+
node: local,
|
|
36914
|
+
message: LOCAL_COMPONENT_MESSAGE
|
|
36915
|
+
});
|
|
36916
|
+
else if (!hasAnyExports && localComponents.length > 0) for (const local of localComponents) context.report({
|
|
36917
|
+
node: local,
|
|
36918
|
+
message: NO_EXPORT_MESSAGE
|
|
36919
|
+
});
|
|
37198
36920
|
} };
|
|
37199
36921
|
}
|
|
37200
36922
|
});
|
|
@@ -37992,8 +37714,8 @@ const preferHtmlDialog = defineRule({
|
|
|
37992
37714
|
if (!HTML_TAGS.has(tagName)) return;
|
|
37993
37715
|
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
37994
37716
|
if (roleAttribute) {
|
|
37995
|
-
const
|
|
37996
|
-
if (
|
|
37717
|
+
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
37718
|
+
if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
|
|
37997
37719
|
if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
|
|
37998
37720
|
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
37999
37721
|
const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
|
|
@@ -40982,7 +40704,6 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40982
40704
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40983
40705
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40984
40706
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40985
|
-
if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
|
|
40986
40707
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
40987
40708
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
40988
40709
|
context.report({
|
|
@@ -41015,6 +40736,18 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
41015
40736
|
"getUTCMilliseconds",
|
|
41016
40737
|
"valueOf"
|
|
41017
40738
|
]);
|
|
40739
|
+
const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
|
|
40740
|
+
"Date",
|
|
40741
|
+
"Map",
|
|
40742
|
+
"Set",
|
|
40743
|
+
"WeakMap",
|
|
40744
|
+
"WeakSet",
|
|
40745
|
+
"RegExp",
|
|
40746
|
+
"Error",
|
|
40747
|
+
"URL",
|
|
40748
|
+
"URLSearchParams",
|
|
40749
|
+
"AbortController"
|
|
40750
|
+
]);
|
|
41018
40751
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
41019
40752
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
41020
40753
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
@@ -41758,7 +41491,7 @@ const rnBottomSheetPreferNative = defineRule({
|
|
|
41758
41491
|
});
|
|
41759
41492
|
//#endregion
|
|
41760
41493
|
//#region src/plugin/rules/react-native/rn-detox-missing-await.ts
|
|
41761
|
-
const EMPTY_VISITORS$
|
|
41494
|
+
const EMPTY_VISITORS$4 = {};
|
|
41762
41495
|
const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
|
|
41763
41496
|
const DETOX_ELEMENT_ACTIONS = new Set([
|
|
41764
41497
|
"tap",
|
|
@@ -41818,7 +41551,7 @@ const rnDetoxMissingAwait = defineRule({
|
|
|
41818
41551
|
recommendation: "Prepend `await` to Detox actions, `waitFor(...)` chains, and `expect(element(...))` assertions. They return promises tied to Detox's synchronization, so a missing await runs steps out of order.",
|
|
41819
41552
|
create: (context) => {
|
|
41820
41553
|
const filename = normalizeFilename(context.filename ?? "");
|
|
41821
|
-
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$
|
|
41554
|
+
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$4;
|
|
41822
41555
|
return { ExpressionStatement(node) {
|
|
41823
41556
|
const expression = node.expression;
|
|
41824
41557
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
@@ -42793,7 +42526,7 @@ const isLegacyArchReactNativeFile = (filename) => {
|
|
|
42793
42526
|
};
|
|
42794
42527
|
//#endregion
|
|
42795
42528
|
//#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
|
|
42796
|
-
const EMPTY_VISITORS$
|
|
42529
|
+
const EMPTY_VISITORS$3 = {};
|
|
42797
42530
|
const reportLegacyShadowProperties = (objectExpression, context) => {
|
|
42798
42531
|
const legacyShadowPropertyNames = [];
|
|
42799
42532
|
for (const property of objectExpression.properties ?? []) {
|
|
@@ -42816,7 +42549,7 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42816
42549
|
severity: "warn",
|
|
42817
42550
|
recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
|
|
42818
42551
|
create: (context) => {
|
|
42819
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
42552
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$3;
|
|
42820
42553
|
return {
|
|
42821
42554
|
JSXAttribute(node) {
|
|
42822
42555
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -43737,7 +43470,7 @@ const isExpoManagedFileActive = (context) => {
|
|
|
43737
43470
|
};
|
|
43738
43471
|
//#endregion
|
|
43739
43472
|
//#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
|
|
43740
|
-
const EMPTY_VISITORS$
|
|
43473
|
+
const EMPTY_VISITORS$2 = {};
|
|
43741
43474
|
const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
|
|
43742
43475
|
const MEMBER_PATH_FAN_OUT_LIMIT = 32;
|
|
43743
43476
|
const isRequireOfBundledAsset = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "require" && node.arguments?.length === 1 && isNodeOfType(node.arguments[0], "Literal") && typeof node.arguments[0].value === "string" && BUNDLED_ASSET_SOURCE_PATTERN.test(node.arguments[0].value);
|
|
@@ -43771,7 +43504,7 @@ const rnPreferExpoImage = defineRule({
|
|
|
43771
43504
|
severity: "warn",
|
|
43772
43505
|
recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
|
|
43773
43506
|
create: (context) => {
|
|
43774
|
-
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$
|
|
43507
|
+
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$2;
|
|
43775
43508
|
const flaggedImports = [];
|
|
43776
43509
|
const assetBindingNames = /* @__PURE__ */ new Set();
|
|
43777
43510
|
const moduleObjectLiterals = /* @__PURE__ */ new Map();
|
|
@@ -44258,7 +43991,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
|
|
|
44258
43991
|
});
|
|
44259
43992
|
//#endregion
|
|
44260
43993
|
//#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
|
|
44261
|
-
const EMPTY_VISITORS$
|
|
43994
|
+
const EMPTY_VISITORS$1 = {};
|
|
44262
43995
|
const IOS_SHADOW_KEYS = new Set([
|
|
44263
43996
|
"shadowColor",
|
|
44264
43997
|
"shadowOffset",
|
|
@@ -44344,7 +44077,7 @@ const rnStylePreferBoxShadow = defineRule({
|
|
|
44344
44077
|
severity: "warn",
|
|
44345
44078
|
recommendation: "These shadow keys only work on one platform. On RN v7+, use the CSS `boxShadow` string instead, like `boxShadow: \"0 2px 8px rgba(0,0,0,0.1)\"`, which works on both.",
|
|
44346
44079
|
create: (context) => {
|
|
44347
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
44080
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$1;
|
|
44348
44081
|
return {
|
|
44349
44082
|
JSXAttribute(node) {
|
|
44350
44083
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -44441,9 +44174,9 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
44441
44174
|
if (!HTML_TAGS.has(elementType)) return;
|
|
44442
44175
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
44443
44176
|
if (!roleAttribute) return;
|
|
44444
|
-
const
|
|
44445
|
-
if (
|
|
44446
|
-
const roles =
|
|
44177
|
+
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
44178
|
+
if (roleValue === null) return;
|
|
44179
|
+
const roles = roleValue.split(/\s+/).filter((token) => token.length > 0);
|
|
44447
44180
|
for (const role of roles) {
|
|
44448
44181
|
const required = ROLE_REQUIRED_PROPS.get(role);
|
|
44449
44182
|
if (!required) continue;
|
|
@@ -47560,9 +47293,7 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
47560
47293
|
};
|
|
47561
47294
|
//#endregion
|
|
47562
47295
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
47563
|
-
const buildMessageDefault = (
|
|
47564
|
-
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.`;
|
|
47565
|
-
};
|
|
47296
|
+
const buildMessageDefault = (role, propName) => `Screen reader users get no help from \`${propName}\` because role \`${role}\` ignores it, so remove it or change the role.`;
|
|
47566
47297
|
const buildMessageImplicit = (role, propName, elementType) => `Screen reader users get no help from \`${propName}\` because \`${elementType}\` has role \`${role}\`, which ignores it, so remove \`${propName}\` or change the element.`;
|
|
47567
47298
|
const roleSupportsAriaProps = defineRule({
|
|
47568
47299
|
id: "role-supports-aria-props",
|
|
@@ -47590,21 +47321,17 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47590
47321
|
if (!ariaAttributes) return;
|
|
47591
47322
|
const elementType = getElementType(node, context.settings);
|
|
47592
47323
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
47593
|
-
const
|
|
47594
|
-
if (
|
|
47595
|
-
|
|
47596
|
-
for (const role of roleCandidates) {
|
|
47597
|
-
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
47598
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47599
|
-
if (!supported) return;
|
|
47600
|
-
supportedSets.push(supported);
|
|
47601
|
-
}
|
|
47324
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
47325
|
+
if (!role) return;
|
|
47326
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
47602
47327
|
const isImplicit = !roleAttribute;
|
|
47328
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47329
|
+
if (!supported) return;
|
|
47603
47330
|
for (const { attribute, propName } of ariaAttributes) {
|
|
47604
|
-
if (
|
|
47331
|
+
if (supported.has(propName)) continue;
|
|
47605
47332
|
context.report({
|
|
47606
47333
|
node: attribute,
|
|
47607
|
-
message: isImplicit ? buildMessageImplicit(
|
|
47334
|
+
message: isImplicit ? buildMessageImplicit(role, propName, elementType) : buildMessageDefault(role, propName)
|
|
47608
47335
|
});
|
|
47609
47336
|
}
|
|
47610
47337
|
} })
|
|
@@ -47956,15 +47683,11 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
47956
47683
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47957
47684
|
return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
|
|
47958
47685
|
};
|
|
47959
|
-
const
|
|
47960
|
-
|
|
47961
|
-
return Boolean(symbol && symbol.kind !== "import");
|
|
47962
|
-
};
|
|
47963
|
-
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
|
|
47964
|
-
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
47686
|
+
const isNonReactEffectEventCallee = (callee, contextNode) => isNodeOfType(callee, "Identifier") && isImportedFromNonReactModule(contextNode, callee.name);
|
|
47687
|
+
const isNonReactEffectEventSymbol = (symbol, contextNode) => {
|
|
47965
47688
|
const initializer = symbol.initializer;
|
|
47966
47689
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47967
|
-
return isNonReactEffectEventCallee(initializer.callee, contextNode
|
|
47690
|
+
return isNonReactEffectEventCallee(initializer.callee, contextNode);
|
|
47968
47691
|
};
|
|
47969
47692
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
47970
47693
|
let current = node.parent;
|
|
@@ -48043,7 +47766,7 @@ const rulesOfHooks = defineRule({
|
|
|
48043
47766
|
const hookContext = isHookCall$1(node, context.scopes, settings);
|
|
48044
47767
|
if (!hookContext) return;
|
|
48045
47768
|
const { hookName } = hookContext;
|
|
48046
|
-
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node
|
|
47769
|
+
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
|
|
48047
47770
|
context.report({
|
|
48048
47771
|
node: node.callee,
|
|
48049
47772
|
message: buildEffectEventPassedDownMessage()
|
|
@@ -48156,7 +47879,7 @@ const rulesOfHooks = defineRule({
|
|
|
48156
47879
|
Identifier(node) {
|
|
48157
47880
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
48158
47881
|
if (!symbol || !isUseEffectEventSymbol(symbol)) return;
|
|
48159
|
-
if (isNonReactEffectEventSymbol(symbol, node
|
|
47882
|
+
if (isNonReactEffectEventSymbol(symbol, node)) return;
|
|
48160
47883
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
48161
47884
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
48162
47885
|
context.report({
|
|
@@ -55728,34 +55451,6 @@ const reactDoctorRules = [
|
|
|
55728
55451
|
];
|
|
55729
55452
|
const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
|
|
55730
55453
|
//#endregion
|
|
55731
|
-
//#region src/plugin/utils/is-next-file.ts
|
|
55732
|
-
const isNextFileActive = (context) => {
|
|
55733
|
-
const rawFilename = context.filename;
|
|
55734
|
-
if (!rawFilename) return true;
|
|
55735
|
-
const filename = normalizeFilename(rawFilename);
|
|
55736
|
-
const manifest = readNearestPackageManifest(filename);
|
|
55737
|
-
if (!manifest) return true;
|
|
55738
|
-
if (declaresDependency(manifest, "next")) return true;
|
|
55739
|
-
if (!declaresAnyDependency(manifest)) return true;
|
|
55740
|
-
const packageDirectory = findNearestPackageDirectory(filename);
|
|
55741
|
-
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
55742
|
-
if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
|
|
55743
|
-
return true;
|
|
55744
|
-
};
|
|
55745
|
-
//#endregion
|
|
55746
|
-
//#region src/plugin/utils/wrap-nextjs-rule.ts
|
|
55747
|
-
const EMPTY_VISITORS$1 = {};
|
|
55748
|
-
const wrapNextjsRule = (rule) => {
|
|
55749
|
-
const innerCreate = rule.create.bind(rule);
|
|
55750
|
-
return {
|
|
55751
|
-
...rule,
|
|
55752
|
-
create: (context) => {
|
|
55753
|
-
if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
|
|
55754
|
-
return innerCreate(context);
|
|
55755
|
-
}
|
|
55756
|
-
};
|
|
55757
|
-
};
|
|
55758
|
-
//#endregion
|
|
55759
55454
|
//#region src/plugin/utils/wrap-react-native-rule.ts
|
|
55760
55455
|
const EMPTY_VISITORS = {};
|
|
55761
55456
|
const wrapReactNativeRule = (rule) => {
|
|
@@ -56225,14 +55920,9 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
56225
55920
|
});
|
|
56226
55921
|
//#endregion
|
|
56227
55922
|
//#region src/plugin/react-doctor-plugin.ts
|
|
56228
|
-
const applyFrameworkGate = (rule) => {
|
|
56229
|
-
if (rule.framework === "react-native") return wrapReactNativeRule(rule);
|
|
56230
|
-
if (rule.framework === "nextjs") return wrapNextjsRule(rule);
|
|
56231
|
-
return rule;
|
|
56232
|
-
};
|
|
56233
55923
|
const applyFrameworkRuleWrappers = (registry) => {
|
|
56234
55924
|
const wrapped = {};
|
|
56235
|
-
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(
|
|
55925
|
+
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(rule.framework === "react-native" ? wrapReactNativeRule(rule) : rule);
|
|
56236
55926
|
return wrapped;
|
|
56237
55927
|
};
|
|
56238
55928
|
const plugin = {
|