oxlint-plugin-react-doctor 0.7.4-dev.b686594 → 0.7.4-dev.dfccac4
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 +1660 -1148
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -614,11 +614,24 @@ const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
|
|
|
614
614
|
"URLSearchParams",
|
|
615
615
|
"AbortController"
|
|
616
616
|
]);
|
|
617
|
+
const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
|
|
618
|
+
"Boolean",
|
|
619
|
+
"String",
|
|
620
|
+
"Number",
|
|
621
|
+
"Array",
|
|
622
|
+
"Object",
|
|
623
|
+
"parseInt",
|
|
624
|
+
"parseFloat",
|
|
625
|
+
"isNaN",
|
|
626
|
+
"isFinite",
|
|
627
|
+
"BigInt",
|
|
628
|
+
"Symbol"
|
|
629
|
+
]);
|
|
617
630
|
const SETTER_PATTERN = /^set[A-Z]/;
|
|
618
631
|
const RENDER_FUNCTION_PATTERN = /^render[A-Z]/;
|
|
619
632
|
const UPPERCASE_PATTERN = /^[A-Z]/;
|
|
620
633
|
const REACT_HANDLER_PROP_PATTERN = /^on[A-Z]/;
|
|
621
|
-
const HOOK_NAME_PATTERN$
|
|
634
|
+
const HOOK_NAME_PATTERN$4 = /^use[A-Z]/;
|
|
622
635
|
const HANDLER_FUNCTION_NAME_PATTERN = /^(?:on|handle)[A-Z]/;
|
|
623
636
|
const EFFECT_HOOK_NAMES$1 = new Set(["useEffect", "useLayoutEffect"]);
|
|
624
637
|
const HOOKS_WITH_DEPS = new Set([
|
|
@@ -7394,7 +7407,7 @@ const containsJsx$1 = (root) => {
|
|
|
7394
7407
|
containsJsxCache.set(root, found);
|
|
7395
7408
|
return found;
|
|
7396
7409
|
};
|
|
7397
|
-
const getStaticMemberName
|
|
7410
|
+
const getStaticMemberName = (node) => {
|
|
7398
7411
|
if (!isNodeOfType(node, "MemberExpression")) return null;
|
|
7399
7412
|
if (!node.computed && isNodeOfType(node.property, "Identifier")) return node.property.name;
|
|
7400
7413
|
if (node.computed && isNodeOfType(node.property, "Literal") && typeof node.property.value === "string") return node.property.value;
|
|
@@ -7408,7 +7421,7 @@ const getAssignedName = (node) => {
|
|
|
7408
7421
|
if (isNodeOfType(parent, "AssignmentExpression")) {
|
|
7409
7422
|
const left = parent.left;
|
|
7410
7423
|
if (isNodeOfType(left, "Identifier")) return left.name;
|
|
7411
|
-
if (isNodeOfType(left, "MemberExpression")) return getStaticMemberName
|
|
7424
|
+
if (isNodeOfType(left, "MemberExpression")) return getStaticMemberName(left);
|
|
7412
7425
|
}
|
|
7413
7426
|
return null;
|
|
7414
7427
|
};
|
|
@@ -7416,20 +7429,20 @@ const isModuleExportsAssignment = (node) => {
|
|
|
7416
7429
|
const parent = node.parent;
|
|
7417
7430
|
if (!parent || !isNodeOfType(parent, "AssignmentExpression")) return false;
|
|
7418
7431
|
const left = parent.left;
|
|
7419
|
-
return isNodeOfType(left, "MemberExpression") && isNodeOfType(left.object, "Identifier") && left.object.name === "module" && getStaticMemberName
|
|
7432
|
+
return isNodeOfType(left, "MemberExpression") && isNodeOfType(left.object, "Identifier") && left.object.name === "module" && getStaticMemberName(left) === "exports";
|
|
7420
7433
|
};
|
|
7421
7434
|
const isCreateClassLikeCall = (node) => {
|
|
7422
7435
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
7423
7436
|
if (isEs5Component(node)) return true;
|
|
7424
7437
|
const callee = node.callee;
|
|
7425
|
-
if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName
|
|
7438
|
+
if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName(callee) === "createClass";
|
|
7426
7439
|
return false;
|
|
7427
7440
|
};
|
|
7428
7441
|
const isCreateContextCall = (node) => {
|
|
7429
7442
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
7430
7443
|
const callee = node.callee;
|
|
7431
7444
|
if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
|
|
7432
|
-
return isNodeOfType(callee, "MemberExpression") && getStaticMemberName
|
|
7445
|
+
return isNodeOfType(callee, "MemberExpression") && getStaticMemberName(callee) === "createContext";
|
|
7433
7446
|
};
|
|
7434
7447
|
const isNamedFunctionLike = (node) => (isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) && Boolean(node.id?.name);
|
|
7435
7448
|
const firstCallArgument = (node) => {
|
|
@@ -7487,7 +7500,7 @@ const memberExpressionPath = (node) => {
|
|
|
7487
7500
|
if (isNodeOfType(node, "Identifier")) return [node.name];
|
|
7488
7501
|
if (!isNodeOfType(node, "MemberExpression")) return [];
|
|
7489
7502
|
const objectPath = memberExpressionPath(node.object);
|
|
7490
|
-
const propertyName = getStaticMemberName
|
|
7503
|
+
const propertyName = getStaticMemberName(node);
|
|
7491
7504
|
return propertyName ? [...objectPath, propertyName] : objectPath;
|
|
7492
7505
|
};
|
|
7493
7506
|
const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -7497,7 +7510,7 @@ const getDisplayNameAssignmentIndex = (programRoot) => {
|
|
|
7497
7510
|
const identifierTargets = /* @__PURE__ */ new Set();
|
|
7498
7511
|
const memberPathSegments = /* @__PURE__ */ new Set();
|
|
7499
7512
|
const visit = (node) => {
|
|
7500
|
-
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName
|
|
7513
|
+
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
|
|
7501
7514
|
if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
|
|
7502
7515
|
for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
|
|
7503
7516
|
}
|
|
@@ -12171,7 +12184,7 @@ const isAtomFromJotai = (callExpression) => {
|
|
|
12171
12184
|
if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
|
|
12172
12185
|
return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
|
|
12173
12186
|
};
|
|
12174
|
-
const isFunctionExpressionLike$
|
|
12187
|
+
const isFunctionExpressionLike$3 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
|
|
12175
12188
|
const getFirstParameterName = (fn) => {
|
|
12176
12189
|
const parameters = fn.params ?? [];
|
|
12177
12190
|
if (parameters.length !== 1) return null;
|
|
@@ -12304,7 +12317,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
|
|
|
12304
12317
|
const args = node.arguments ?? [];
|
|
12305
12318
|
if (args.length === 0) return;
|
|
12306
12319
|
const reader = args[0];
|
|
12307
|
-
if (!isFunctionExpressionLike$
|
|
12320
|
+
if (!isFunctionExpressionLike$3(reader)) return;
|
|
12308
12321
|
const getParameterName = getFirstParameterName(reader);
|
|
12309
12322
|
if (!getParameterName) return;
|
|
12310
12323
|
const freshReturn = getFreshReturnForFunction(reader);
|
|
@@ -12396,14 +12409,14 @@ const getHandlerNamedBindingName = (functionNode) => {
|
|
|
12396
12409
|
const containingFunctionIsComponentOrHook = (functionNode) => {
|
|
12397
12410
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) {
|
|
12398
12411
|
const declaredName = functionNode.id.name;
|
|
12399
|
-
return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN$
|
|
12412
|
+
return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN$4.test(declaredName);
|
|
12400
12413
|
}
|
|
12401
12414
|
let cursor = functionNode.parent ?? null;
|
|
12402
12415
|
while (cursor && isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
|
|
12403
12416
|
if (!cursor) return false;
|
|
12404
12417
|
if (!isNodeOfType(cursor, "VariableDeclarator")) return false;
|
|
12405
12418
|
if (!isNodeOfType(cursor.id, "Identifier")) return false;
|
|
12406
|
-
return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN$
|
|
12419
|
+
return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN$4.test(cursor.id.name);
|
|
12407
12420
|
};
|
|
12408
12421
|
const isBindingInvokedOnRenderPath = (root, bindingName) => {
|
|
12409
12422
|
let didFindRenderPathInvocation = false;
|
|
@@ -21325,6 +21338,19 @@ const noAccessKey = defineRule({
|
|
|
21325
21338
|
}
|
|
21326
21339
|
});
|
|
21327
21340
|
//#endregion
|
|
21341
|
+
//#region src/plugin/utils/get-call-method-name.ts
|
|
21342
|
+
/**
|
|
21343
|
+
* Returns the static method name of a call's callee when it's a
|
|
21344
|
+
* non-computed MemberExpression (`obj.method` → `"method"`), or
|
|
21345
|
+
* `null` otherwise. Used by `no-pass-data-to-parent` and
|
|
21346
|
+
* `no-pass-live-state-to-parent` to match the receiver-bound
|
|
21347
|
+
* method-call shape against an allow/block list.
|
|
21348
|
+
*/
|
|
21349
|
+
const getCallMethodName = (callee) => {
|
|
21350
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
21351
|
+
return null;
|
|
21352
|
+
};
|
|
21353
|
+
//#endregion
|
|
21328
21354
|
//#region src/plugin/utils/reads-post-mount-value.ts
|
|
21329
21355
|
const DOM_QUERY_MEMBER_NAMES = new Set([
|
|
21330
21356
|
"getBoundingClientRect",
|
|
@@ -21422,6 +21448,13 @@ const isPostMountMemberRead = (node) => {
|
|
|
21422
21448
|
if (!LAYOUT_MEASUREMENT_MEMBER_NAMES.has(memberName)) return false;
|
|
21423
21449
|
return isRefLikeReceiver(node.object);
|
|
21424
21450
|
};
|
|
21451
|
+
const isMeasurementMemberRead = (node) => {
|
|
21452
|
+
if (!isPostMountMemberRead(node)) return false;
|
|
21453
|
+
if (!isNodeOfType(node, "MemberExpression") || !isNodeOfType(node.property, "Identifier")) return false;
|
|
21454
|
+
if (!DOM_QUERY_MEMBER_NAMES.has(node.property.name)) return true;
|
|
21455
|
+
const parent = node.parent;
|
|
21456
|
+
return Boolean(parent && isNodeOfType(parent, "CallExpression") && parent.callee === node);
|
|
21457
|
+
};
|
|
21425
21458
|
const isPropertyNamePosition$1 = (identifier) => {
|
|
21426
21459
|
const parent = identifier.parent;
|
|
21427
21460
|
if (!parent) return false;
|
|
@@ -21539,10 +21572,10 @@ const getScopeForNode = (node, manager) => {
|
|
|
21539
21572
|
//#endregion
|
|
21540
21573
|
//#region src/plugin/rules/state-and-effects/utils/effect/ast.ts
|
|
21541
21574
|
const getChildKeys = (node) => VISITOR_KEYS[node.type] ?? Object.keys(node).filter((key) => key !== "parent");
|
|
21542
|
-
const HOOK_NAME_PATTERN$
|
|
21575
|
+
const HOOK_NAME_PATTERN$3 = /^use[A-Z0-9]/;
|
|
21543
21576
|
const isInsideCallbackArgumentOf = (identifier, initializer) => {
|
|
21544
21577
|
if (!isNodeOfType(initializer, "CallExpression") && !isNodeOfType(initializer, "NewExpression")) return false;
|
|
21545
|
-
if (isNodeOfType(initializer, "CallExpression") && isNodeOfType(initializer.callee, "Identifier") && HOOK_NAME_PATTERN$
|
|
21578
|
+
if (isNodeOfType(initializer, "CallExpression") && isNodeOfType(initializer.callee, "Identifier") && HOOK_NAME_PATTERN$3.test(initializer.callee.name)) return false;
|
|
21546
21579
|
const callbackArguments = (initializer.arguments ?? []).filter((argument) => isFunctionLike$1(argument));
|
|
21547
21580
|
if (callbackArguments.length === 0) return false;
|
|
21548
21581
|
let node = identifier;
|
|
@@ -21706,133 +21739,6 @@ const getEventualCallRefsTo = (analysis, ref, predicate) => {
|
|
|
21706
21739
|
return callExprRefs.filter(predicate);
|
|
21707
21740
|
};
|
|
21708
21741
|
//#endregion
|
|
21709
|
-
//#region src/plugin/rules/state-and-effects/utils/effect/external-state.ts
|
|
21710
|
-
const DEFERRING_CALLEE_NAMES$1 = new Set([
|
|
21711
|
-
"setTimeout",
|
|
21712
|
-
"setInterval",
|
|
21713
|
-
"setImmediate",
|
|
21714
|
-
"requestAnimationFrame",
|
|
21715
|
-
"requestIdleCallback",
|
|
21716
|
-
"queueMicrotask",
|
|
21717
|
-
"addEventListener",
|
|
21718
|
-
"addListener",
|
|
21719
|
-
"subscribe",
|
|
21720
|
-
"observe",
|
|
21721
|
-
"watch",
|
|
21722
|
-
"watchPosition",
|
|
21723
|
-
"then",
|
|
21724
|
-
"catch",
|
|
21725
|
-
"finally",
|
|
21726
|
-
"on",
|
|
21727
|
-
"once"
|
|
21728
|
-
]);
|
|
21729
|
-
const getCalleeName$1 = (callee) => {
|
|
21730
|
-
if (!callee) return null;
|
|
21731
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
21732
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
21733
|
-
return null;
|
|
21734
|
-
};
|
|
21735
|
-
const parentOf = (node) => node.parent ?? null;
|
|
21736
|
-
const argumentsInclude = (args, target) => (args ?? []).some((argument) => argument === target);
|
|
21737
|
-
const isDeferredCallbackPosition = (expression) => {
|
|
21738
|
-
const parent = parentOf(expression);
|
|
21739
|
-
if (!parent) return false;
|
|
21740
|
-
if (isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, expression)) {
|
|
21741
|
-
const name = getCalleeName$1(parent.callee);
|
|
21742
|
-
if (name && DEFERRING_CALLEE_NAMES$1.has(name)) return true;
|
|
21743
|
-
}
|
|
21744
|
-
if (isNodeOfType(parent, "NewExpression") && argumentsInclude(parent.arguments, expression)) {
|
|
21745
|
-
const name = getCalleeName$1(parent.callee);
|
|
21746
|
-
if (name && (name.endsWith("Observer") || name === "Promise")) return true;
|
|
21747
|
-
}
|
|
21748
|
-
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === expression && isNodeOfType(parent.left, "MemberExpression") && isNodeOfType(parent.left.property, "Identifier") && parent.left.property.name.startsWith("on")) return true;
|
|
21749
|
-
return false;
|
|
21750
|
-
};
|
|
21751
|
-
const getHandlerDeclarator = (fn) => {
|
|
21752
|
-
let current = fn;
|
|
21753
|
-
let parent = parentOf(current);
|
|
21754
|
-
while (parent && isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, current)) {
|
|
21755
|
-
current = parent;
|
|
21756
|
-
parent = parentOf(current);
|
|
21757
|
-
}
|
|
21758
|
-
if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent;
|
|
21759
|
-
return null;
|
|
21760
|
-
};
|
|
21761
|
-
const isNamedHandlerUsedAsDeferredCallback = (analysis, fn) => {
|
|
21762
|
-
const declarator = getHandlerDeclarator(fn);
|
|
21763
|
-
if (!declarator || !isNodeOfType(declarator.id, "Identifier")) return false;
|
|
21764
|
-
const name = declarator.id.name;
|
|
21765
|
-
for (const scope of analysis.scopeManager.scopes) {
|
|
21766
|
-
const variable = scope.variables.find((candidate) => candidate.name === name && candidate.defs.some((def) => def.node === declarator));
|
|
21767
|
-
if (!variable) continue;
|
|
21768
|
-
return variable.references.some((reference) => isDeferredCallbackPosition(reference.identifier));
|
|
21769
|
-
}
|
|
21770
|
-
return false;
|
|
21771
|
-
};
|
|
21772
|
-
const isDeferredCallbackFunction = (analysis, fn) => {
|
|
21773
|
-
if (isDeferredCallbackPosition(fn)) return true;
|
|
21774
|
-
return isNamedHandlerUsedAsDeferredCallback(analysis, fn);
|
|
21775
|
-
};
|
|
21776
|
-
const isInsideDeferredCallback$1 = (analysis, node, boundary) => {
|
|
21777
|
-
let current = parentOf(node);
|
|
21778
|
-
while (current && current !== boundary) {
|
|
21779
|
-
if (isFunctionLike$1(current) && isDeferredCallbackFunction(analysis, current)) return true;
|
|
21780
|
-
current = parentOf(current);
|
|
21781
|
-
}
|
|
21782
|
-
return false;
|
|
21783
|
-
};
|
|
21784
|
-
const findUseStateDeclarator = (ref) => {
|
|
21785
|
-
for (const def of ref.resolved?.defs ?? []) {
|
|
21786
|
-
const node = def.node;
|
|
21787
|
-
if (!isNodeOfType(node, "VariableDeclarator")) continue;
|
|
21788
|
-
if (!isNodeOfType(node.init, "CallExpression")) continue;
|
|
21789
|
-
if (!isNodeOfType(node.id, "ArrayPattern")) continue;
|
|
21790
|
-
return node;
|
|
21791
|
-
}
|
|
21792
|
-
return null;
|
|
21793
|
-
};
|
|
21794
|
-
const declaratorToExternallyDriven = /* @__PURE__ */ new WeakMap();
|
|
21795
|
-
const isExternallyDrivenState = (analysis, ref) => {
|
|
21796
|
-
const declarator = findUseStateDeclarator(ref);
|
|
21797
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
|
|
21798
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
21799
|
-
const cached = declaratorToExternallyDriven.get(declarator);
|
|
21800
|
-
if (cached !== void 0) return cached;
|
|
21801
|
-
const result = computeExternallyDriven(analysis, declarator);
|
|
21802
|
-
declaratorToExternallyDriven.set(declarator, result);
|
|
21803
|
-
return result;
|
|
21804
|
-
};
|
|
21805
|
-
const computeExternallyDriven = (analysis, declarator) => {
|
|
21806
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
21807
|
-
const setterElement = declarator.id.elements?.[1];
|
|
21808
|
-
if (!setterElement || !isNodeOfType(setterElement, "Identifier")) return false;
|
|
21809
|
-
const setterName = setterElement.name;
|
|
21810
|
-
let setterVariable = null;
|
|
21811
|
-
for (const scope of analysis.scopeManager.scopes) {
|
|
21812
|
-
const match = scope.variables.find((variable) => variable.name === setterName && variable.defs.some((def) => def.node === declarator));
|
|
21813
|
-
if (match) {
|
|
21814
|
-
setterVariable = match;
|
|
21815
|
-
break;
|
|
21816
|
-
}
|
|
21817
|
-
}
|
|
21818
|
-
if (!setterVariable) return false;
|
|
21819
|
-
let hasDeferredCallSite = false;
|
|
21820
|
-
for (const setterReference of setterVariable.references) {
|
|
21821
|
-
const identifier = setterReference.identifier;
|
|
21822
|
-
const parent = parentOf(identifier);
|
|
21823
|
-
if (!parent) continue;
|
|
21824
|
-
if (isDeferredCallbackPosition(identifier)) {
|
|
21825
|
-
hasDeferredCallSite = true;
|
|
21826
|
-
continue;
|
|
21827
|
-
}
|
|
21828
|
-
if (!isNodeOfType(parent, "CallExpression")) continue;
|
|
21829
|
-
if (parent.callee !== identifier) continue;
|
|
21830
|
-
if (!isInsideDeferredCallback$1(analysis, parent, declarator)) return false;
|
|
21831
|
-
hasDeferredCallSite = true;
|
|
21832
|
-
}
|
|
21833
|
-
return hasDeferredCallSite;
|
|
21834
|
-
};
|
|
21835
|
-
//#endregion
|
|
21836
21742
|
//#region src/plugin/rules/state-and-effects/utils/effect/react.ts
|
|
21837
21743
|
const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
21838
21744
|
const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
|
|
@@ -22096,736 +22002,167 @@ const findContainingNode = (analysis, node) => {
|
|
|
22096
22002
|
return findContainingNode(analysis, parent);
|
|
22097
22003
|
};
|
|
22098
22004
|
//#endregion
|
|
22099
|
-
//#region src/plugin/rules/state-and-effects/
|
|
22100
|
-
const
|
|
22101
|
-
|
|
22102
|
-
if (
|
|
22103
|
-
if (
|
|
22104
|
-
|
|
22105
|
-
|
|
22106
|
-
|
|
22107
|
-
|
|
22108
|
-
|
|
22109
|
-
|
|
22110
|
-
|
|
22111
|
-
return Boolean(memberReferenceName && eventHandlerReferenceNames.has(memberReferenceName));
|
|
22112
|
-
};
|
|
22113
|
-
const isIntrinsicJsxAttribute = (node) => {
|
|
22114
|
-
if (!isNodeOfType(node, "JSXAttribute")) return false;
|
|
22115
|
-
const openingElement = node.parent;
|
|
22116
|
-
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
22117
|
-
const elementName = openingElement.name;
|
|
22118
|
-
if (!isNodeOfType(elementName, "JSXIdentifier")) return false;
|
|
22119
|
-
return /^[a-z]/.test(elementName.name);
|
|
22120
|
-
};
|
|
22121
|
-
//#endregion
|
|
22122
|
-
//#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
|
|
22123
|
-
const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
22124
|
-
const getOwnScopeBoundNames = (functionNode) => {
|
|
22125
|
-
const cached = ownScopeBoundNamesCache.get(functionNode);
|
|
22126
|
-
if (cached) return cached;
|
|
22127
|
-
const boundNames = /* @__PURE__ */ new Set();
|
|
22128
|
-
if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
|
|
22129
|
-
walkAst(functionNode, (child) => {
|
|
22130
|
-
if (child !== functionNode && isFunctionLike$1(child)) return false;
|
|
22131
|
-
if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
|
|
22132
|
-
});
|
|
22133
|
-
ownScopeBoundNamesCache.set(functionNode, boundNames);
|
|
22134
|
-
return boundNames;
|
|
22135
|
-
};
|
|
22136
|
-
const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
|
|
22137
|
-
const isPropertyNamePosition = (identifier) => {
|
|
22138
|
-
const parent = identifier.parent;
|
|
22139
|
-
if (!parent) return false;
|
|
22140
|
-
if (isNodeOfType(parent, "MemberExpression")) return parent.property === identifier && !parent.computed;
|
|
22141
|
-
if (isNodeOfType(parent, "Property")) return parent.key === identifier && !parent.computed;
|
|
22142
|
-
return false;
|
|
22143
|
-
};
|
|
22144
|
-
const referencesIdentifierNamed = (root, identifierName) => {
|
|
22145
|
-
let isReferenced = false;
|
|
22146
|
-
walkAst(root, (child) => {
|
|
22147
|
-
if (isReferenced) return false;
|
|
22148
|
-
if (child !== root && isFunctionLike$1(child) && declaresBindingNamed(child, identifierName)) return false;
|
|
22149
|
-
if (isNodeOfType(child, "Identifier") && child.name === identifierName && !isPropertyNamePosition(child)) {
|
|
22150
|
-
isReferenced = true;
|
|
22151
|
-
return false;
|
|
22152
|
-
}
|
|
22153
|
-
});
|
|
22154
|
-
return isReferenced;
|
|
22155
|
-
};
|
|
22156
|
-
const isSetterWiredToJsxHandler = (componentFunction, setterName) => {
|
|
22157
|
-
let isWired = false;
|
|
22158
|
-
walkAst(componentFunction, (child) => {
|
|
22159
|
-
if (isWired) return false;
|
|
22160
|
-
if (child !== componentFunction && isFunctionLike$1(child) && declaresBindingNamed(child, setterName)) return false;
|
|
22161
|
-
if (!isNodeOfType(child, "JSXAttribute") || !child.value) return;
|
|
22162
|
-
const attributeName = getJsxAttributeName(child.name);
|
|
22163
|
-
if (!attributeName || !isEventHandlerName(attributeName)) return;
|
|
22164
|
-
if (referencesIdentifierNamed(child.value, setterName)) {
|
|
22165
|
-
isWired = true;
|
|
22166
|
-
return false;
|
|
22167
|
-
}
|
|
22168
|
-
});
|
|
22169
|
-
return isWired;
|
|
22170
|
-
};
|
|
22171
|
-
//#endregion
|
|
22172
|
-
//#region src/plugin/rules/state-and-effects/utils/has-user-input-setter-writer.ts
|
|
22173
|
-
const HANDLER_BINDING_NAME_PATTERN = /^(on|handle)[A-Z_]/;
|
|
22174
|
-
const isEventHandlerPropertyKey = (property) => isNodeOfType(property, "Property") && !property.computed && isNodeOfType(property.key, "Identifier") && isEventHandlerName(property.key.name);
|
|
22175
|
-
const DEFERRED_CALLBACK_MEMBER_NAMES = new Set([
|
|
22176
|
-
"then",
|
|
22177
|
-
"catch",
|
|
22178
|
-
"finally",
|
|
22179
|
-
"subscribe"
|
|
22180
|
-
]);
|
|
22181
|
-
const DEFERRED_CALLBACK_CALLEE_NAMES = new Set([
|
|
22182
|
-
"setTimeout",
|
|
22183
|
-
"setInterval",
|
|
22184
|
-
"requestAnimationFrame",
|
|
22185
|
-
"addEventListener",
|
|
22186
|
-
"addListener"
|
|
22187
|
-
]);
|
|
22188
|
-
const isDeferredCallbackArgumentOf = (callExpr, child) => {
|
|
22189
|
-
if (!isNodeOfType(callExpr, "CallExpression")) return false;
|
|
22190
|
-
if (!(callExpr.arguments ?? []).includes(child)) return false;
|
|
22191
|
-
const callee = callExpr.callee;
|
|
22192
|
-
if (isNodeOfType(callee, "Identifier")) return DEFERRED_CALLBACK_CALLEE_NAMES.has(callee.name);
|
|
22193
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return DEFERRED_CALLBACK_MEMBER_NAMES.has(callee.property.name) || DEFERRED_CALLBACK_CALLEE_NAMES.has(callee.property.name);
|
|
22005
|
+
//#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
|
|
22006
|
+
const isLiteralOnlyExpression$1 = (node) => {
|
|
22007
|
+
if (!node) return false;
|
|
22008
|
+
if (isNodeOfType(node, "Literal")) return true;
|
|
22009
|
+
if (isNodeOfType(node, "Identifier") && node.name === "undefined") return true;
|
|
22010
|
+
if (isNodeOfType(node, "UnaryExpression")) return isLiteralOnlyExpression$1(node.argument);
|
|
22011
|
+
if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isLiteralOnlyExpression$1(property.value));
|
|
22012
|
+
if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => isLiteralOnlyExpression$1(element));
|
|
22013
|
+
if (isNodeOfType(node, "NewExpression")) {
|
|
22014
|
+
const callee = node.callee;
|
|
22015
|
+
return isNodeOfType(callee, "Identifier") && (callee.name === "Map" || callee.name === "Set") && (node.arguments ?? []).length === 0;
|
|
22016
|
+
}
|
|
22194
22017
|
return false;
|
|
22195
22018
|
};
|
|
22196
|
-
const
|
|
22197
|
-
|
|
22198
|
-
|
|
22199
|
-
let
|
|
22200
|
-
|
|
22201
|
-
if (isNodeOfType(
|
|
22202
|
-
|
|
22203
|
-
|
|
22204
|
-
}
|
|
22205
|
-
if (isEventHandlerPropertyKey(cursor)) return true;
|
|
22206
|
-
if (includeDeferredWriters && isDeferredCallbackArgumentOf(cursor, previous)) return true;
|
|
22207
|
-
if (isFunctionLike$1(cursor)) {
|
|
22208
|
-
outermostFunctionBelowComponent = cursor;
|
|
22209
|
-
if (includeDeferredWriters && cursor.async === true) return true;
|
|
22019
|
+
const isSpreadMergeOfLiterals = (body) => {
|
|
22020
|
+
if (!body || !isNodeOfType(body, "ObjectExpression")) return false;
|
|
22021
|
+
const properties = body.properties ?? [];
|
|
22022
|
+
let sawSpread = false;
|
|
22023
|
+
for (const property of properties) {
|
|
22024
|
+
if (isNodeOfType(property, "SpreadElement")) {
|
|
22025
|
+
sawSpread = true;
|
|
22026
|
+
continue;
|
|
22210
22027
|
}
|
|
22211
|
-
|
|
22212
|
-
|
|
22028
|
+
if (!isNodeOfType(property, "Property")) return false;
|
|
22029
|
+
if (!isLiteralOnlyExpression$1(property.value)) return false;
|
|
22213
22030
|
}
|
|
22214
|
-
|
|
22215
|
-
const bindingName = getFunctionBindingName$1(outermostFunctionBelowComponent);
|
|
22216
|
-
if (!bindingName) return false;
|
|
22217
|
-
if (HANDLER_BINDING_NAME_PATTERN.test(bindingName)) return true;
|
|
22218
|
-
return isSetterWiredToJsxHandler(componentFunction, bindingName);
|
|
22031
|
+
return sawSpread;
|
|
22219
22032
|
};
|
|
22220
|
-
const
|
|
22221
|
-
|
|
22222
|
-
|
|
22223
|
-
if (
|
|
22224
|
-
|
|
22225
|
-
|
|
22226
|
-
|
|
22227
|
-
if (isAstDescendant(identifier, effectNode)) continue;
|
|
22228
|
-
if (isIndependentWriterIdentifier(componentFunction, identifier, includeDeferredWriters)) return true;
|
|
22033
|
+
const isConstantSetterArgument = (analysis, callExpr) => {
|
|
22034
|
+
const argument = callExpr.arguments?.[0];
|
|
22035
|
+
if (!argument) return true;
|
|
22036
|
+
if (isLiteralOnlyExpression$1(argument)) return true;
|
|
22037
|
+
if (isNodeOfType(argument, "Identifier")) {
|
|
22038
|
+
const argumentRef = getRef(analysis, argument);
|
|
22039
|
+
if (argumentRef && isConstant(argumentRef)) return true;
|
|
22229
22040
|
}
|
|
22041
|
+
if (isNodeOfType(argument, "ArrowFunctionExpression")) return isSpreadMergeOfLiterals(argument.body);
|
|
22230
22042
|
return false;
|
|
22231
22043
|
};
|
|
22232
|
-
|
|
22233
|
-
|
|
22234
|
-
|
|
22235
|
-
|
|
22236
|
-
|
|
22237
|
-
|
|
22238
|
-
|
|
22239
|
-
|
|
22240
|
-
|
|
22241
|
-
if (!isNodeOfType(pattern, "ObjectPattern")) return false;
|
|
22242
|
-
return (pattern.properties ?? []).some((property) => {
|
|
22243
|
-
if (!isNodeOfType(property, "Property")) return false;
|
|
22244
|
-
const bound = property.value;
|
|
22245
|
-
return Boolean(bound && isNodeOfType(bound, "Identifier") && bound.name === name);
|
|
22246
|
-
});
|
|
22247
|
-
};
|
|
22248
|
-
const findEffectLocalInitializer = (effectFn, name) => {
|
|
22249
|
-
let initializer = null;
|
|
22250
|
-
walkAst(effectFn, (child) => {
|
|
22251
|
-
if (initializer) return false;
|
|
22252
|
-
if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
|
|
22253
|
-
if (isNodeOfType(child.id, "Identifier") && child.id.name === name || objectPatternBindsName(child.id, name)) {
|
|
22254
|
-
initializer = child.init;
|
|
22255
|
-
return false;
|
|
22256
|
-
}
|
|
22257
|
-
});
|
|
22258
|
-
return initializer;
|
|
22259
|
-
};
|
|
22260
|
-
const readsPostMountValueThroughLocals = (root, effectFn, options = {}, visitedLocalNames = /* @__PURE__ */ new Set()) => {
|
|
22261
|
-
let found = false;
|
|
22262
|
-
walkAst(root, (child) => {
|
|
22263
|
-
if (found) return false;
|
|
22264
|
-
if (matchesPostMountRead(child, options)) {
|
|
22265
|
-
found = true;
|
|
22266
|
-
return false;
|
|
22267
|
-
}
|
|
22268
|
-
if (!isNodeOfType(child, "Identifier")) return;
|
|
22269
|
-
if (visitedLocalNames.has(child.name)) return;
|
|
22270
|
-
visitedLocalNames.add(child.name);
|
|
22271
|
-
const localInitializer = findEffectLocalInitializer(effectFn, child.name);
|
|
22272
|
-
if (localInitializer && readsPostMountValueThroughLocals(localInitializer, effectFn, options, visitedLocalNames)) {
|
|
22273
|
-
found = true;
|
|
22274
|
-
return false;
|
|
22275
|
-
}
|
|
22276
|
-
});
|
|
22277
|
-
return found;
|
|
22278
|
-
};
|
|
22279
|
-
//#endregion
|
|
22280
|
-
//#region src/plugin/rules/state-and-effects/utils/collect-effect-state-write-facts.ts
|
|
22281
|
-
const SYNCHRONOUS_ITERATOR_METHOD_NAMES = new Set([
|
|
22282
|
-
"every",
|
|
22283
|
-
"filter",
|
|
22284
|
-
"find",
|
|
22285
|
-
"findIndex",
|
|
22286
|
-
"flatMap",
|
|
22287
|
-
"forEach",
|
|
22288
|
-
"map",
|
|
22289
|
-
"reduce",
|
|
22290
|
-
"reduceRight",
|
|
22291
|
-
"some"
|
|
22292
|
-
]);
|
|
22293
|
-
const DEFERRING_CALLEE_NAMES = new Set([
|
|
22294
|
-
"queueMicrotask",
|
|
22295
|
-
"requestAnimationFrame",
|
|
22296
|
-
"requestIdleCallback",
|
|
22297
|
-
"setImmediate",
|
|
22298
|
-
"setInterval",
|
|
22299
|
-
"setTimeout"
|
|
22300
|
-
]);
|
|
22301
|
-
const DEFERRING_MEMBER_NAMES = new Set([
|
|
22302
|
-
"catch",
|
|
22303
|
-
"finally",
|
|
22304
|
-
"then"
|
|
22305
|
-
]);
|
|
22306
|
-
const PURE_GLOBAL_CALLEE_NAMES = new Set([
|
|
22307
|
-
"Array",
|
|
22308
|
-
"BigInt",
|
|
22309
|
-
"Boolean",
|
|
22310
|
-
"Number",
|
|
22311
|
-
"Object",
|
|
22312
|
-
"String",
|
|
22313
|
-
"parseFloat",
|
|
22314
|
-
"parseInt"
|
|
22315
|
-
]);
|
|
22316
|
-
const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
|
|
22317
|
-
const PURE_MEMBER_TRANSFORM_NAMES = new Set([
|
|
22318
|
-
"concat",
|
|
22319
|
-
"filter",
|
|
22320
|
-
"join",
|
|
22321
|
-
"map",
|
|
22322
|
-
"split",
|
|
22323
|
-
"toLowerCase",
|
|
22324
|
-
"toString",
|
|
22325
|
-
"toUpperCase",
|
|
22326
|
-
"trim"
|
|
22327
|
-
]);
|
|
22328
|
-
const STORAGE_GLOBAL_NAMES$1 = new Set([
|
|
22329
|
-
"indexedDB",
|
|
22330
|
-
"localStorage",
|
|
22331
|
-
"sessionStorage"
|
|
22044
|
+
const SUBSCRIPTION_REGISTRATION_METHOD_NAMES = new Set([
|
|
22045
|
+
"addEventListener",
|
|
22046
|
+
"addListener",
|
|
22047
|
+
"on",
|
|
22048
|
+
"once",
|
|
22049
|
+
"subscribe",
|
|
22050
|
+
"observe",
|
|
22051
|
+
"observeDeep",
|
|
22052
|
+
"watch"
|
|
22332
22053
|
]);
|
|
22333
|
-
const
|
|
22334
|
-
if (
|
|
22335
|
-
|
|
22336
|
-
|
|
22337
|
-
return null;
|
|
22338
|
-
};
|
|
22339
|
-
const getMemberRoot = (node) => {
|
|
22340
|
-
let current = stripParenExpression(node);
|
|
22341
|
-
while (isNodeOfType(current, "MemberExpression")) current = stripParenExpression(current.object);
|
|
22342
|
-
return current;
|
|
22343
|
-
};
|
|
22344
|
-
const getCallCalleeName$1 = (callExpression) => {
|
|
22345
|
-
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
22346
|
-
const callee = stripParenExpression(callExpression.callee);
|
|
22347
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
22348
|
-
return getStaticMemberName(callee);
|
|
22349
|
-
};
|
|
22350
|
-
const getFunctionParameters = (functionNode) => isFunctionLike$1(functionNode) ? functionNode.params ?? [] : [];
|
|
22351
|
-
const getIdentifierBindingIdentity = (analysis, identifier) => {
|
|
22352
|
-
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
22353
|
-
return getRef(analysis, identifier)?.resolved ?? null;
|
|
22354
|
-
};
|
|
22355
|
-
const getParameterBindingIdentity = (analysis, functionNode, parameter) => {
|
|
22356
|
-
if (!isNodeOfType(parameter, "Identifier")) return parameter;
|
|
22357
|
-
for (const scope of analysis.scopeManager.scopes) {
|
|
22358
|
-
const variable = scope.variables.find((candidate) => candidate.name === parameter.name && candidate.defs.some((definition) => definition.type === "Parameter" && definition.node === functionNode));
|
|
22359
|
-
if (variable) return variable;
|
|
22360
|
-
}
|
|
22361
|
-
return parameter;
|
|
22362
|
-
};
|
|
22363
|
-
const buildSubstitutions = (analysis, functionNode, argumentExpressions, parentFrame) => {
|
|
22364
|
-
const substitutions = /* @__PURE__ */ new Map();
|
|
22365
|
-
const parameters = getFunctionParameters(functionNode);
|
|
22366
|
-
for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
|
|
22367
|
-
const parameter = parameters[parameterIndex];
|
|
22368
|
-
const argument = argumentExpressions[parameterIndex];
|
|
22369
|
-
if (!parameter || !argument || !isNodeOfType(parameter, "Identifier")) continue;
|
|
22370
|
-
substitutions.set(getParameterBindingIdentity(analysis, functionNode, parameter), {
|
|
22371
|
-
expression: argument,
|
|
22372
|
-
frame: parentFrame
|
|
22373
|
-
});
|
|
22054
|
+
const isSubscriptionRegistrationCall = (node) => {
|
|
22055
|
+
if (isNodeOfType(node, "NewExpression")) {
|
|
22056
|
+
const callee = node.callee;
|
|
22057
|
+
return isNodeOfType(callee, "Identifier") && callee.name.endsWith("Observer");
|
|
22374
22058
|
}
|
|
22375
|
-
return
|
|
22376
|
-
|
|
22377
|
-
|
|
22378
|
-
|
|
22379
|
-
|
|
22380
|
-
|
|
22381
|
-
|
|
22382
|
-
|
|
22383
|
-
|
|
22384
|
-
|
|
22385
|
-
|
|
22386
|
-
|
|
22387
|
-
if (
|
|
22388
|
-
const
|
|
22389
|
-
|
|
22390
|
-
|
|
22391
|
-
|
|
22392
|
-
|
|
22393
|
-
|
|
22394
|
-
if (!/^useEvent(?:Callback)?$/.test(callee.name)) return false;
|
|
22395
|
-
const calleeReference = getRef(analysis, callee);
|
|
22396
|
-
if (!calleeReference?.resolved) return false;
|
|
22397
|
-
const implementation = resolveToFunction(calleeReference);
|
|
22398
|
-
if (!implementation) return false;
|
|
22399
|
-
const callbackParameter = getFunctionParameters(implementation)[0];
|
|
22400
|
-
if (!callbackParameter || !isNodeOfType(callbackParameter, "Identifier")) return false;
|
|
22401
|
-
const callbackBinding = getParameterBindingIdentity(analysis, implementation, callbackParameter);
|
|
22402
|
-
const callbackRefDeclarators = [];
|
|
22403
|
-
walkAst(implementation, (child) => {
|
|
22404
|
-
if (child !== implementation && isFunctionLike$1(child)) return false;
|
|
22405
|
-
if (!isNodeOfType(child, "VariableDeclarator")) return;
|
|
22406
|
-
if (!isNodeOfType(child.id, "Identifier")) return;
|
|
22407
|
-
if (!isNodeOfType(child.init, "CallExpression")) return;
|
|
22408
|
-
if (getCallCalleeName$1(child.init) !== "useRef") return;
|
|
22409
|
-
const refInitializer = child.init.arguments?.[0];
|
|
22410
|
-
if (!refInitializer || getIdentifierBindingIdentity(analysis, refInitializer) !== callbackBinding) return;
|
|
22411
|
-
callbackRefDeclarators.push(child);
|
|
22412
|
-
});
|
|
22413
|
-
if (callbackRefDeclarators.length === 0) return false;
|
|
22414
|
-
return getReturnedExpressions(implementation).some((returnedExpression) => {
|
|
22415
|
-
const returnedCall = stripParenExpression(returnedExpression);
|
|
22416
|
-
if (!isNodeOfType(returnedCall, "CallExpression")) return false;
|
|
22417
|
-
const returnedCalleeName = getCallCalleeName$1(returnedCall);
|
|
22418
|
-
if (returnedCalleeName !== "useCallback" && returnedCalleeName !== "useEffectEvent") return false;
|
|
22419
|
-
const stableCallback = returnedCall.arguments?.[0];
|
|
22420
|
-
if (!stableCallback || !isFunctionLike$1(stableCallback)) return false;
|
|
22421
|
-
let forwardsCallback = false;
|
|
22422
|
-
walkAst(stableCallback, (child) => {
|
|
22423
|
-
if (forwardsCallback) return false;
|
|
22424
|
-
if (child !== stableCallback && isFunctionLike$1(child)) return false;
|
|
22425
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22426
|
-
const forwardedCallee = stripParenExpression(child.callee);
|
|
22427
|
-
if (!isNodeOfType(forwardedCallee, "MemberExpression")) return;
|
|
22428
|
-
if (getStaticMemberName(forwardedCallee) !== "current") return;
|
|
22429
|
-
if (!isNodeOfType(forwardedCallee.object, "Identifier")) return;
|
|
22430
|
-
const refReference = getRef(analysis, forwardedCallee.object);
|
|
22431
|
-
if (!callbackRefDeclarators.find((declarator) => refReference?.resolved?.defs.some((definition) => definition.node === declarator)) || !refReference?.resolved) return;
|
|
22432
|
-
if (!refReference.resolved.references.some((candidateReference) => {
|
|
22433
|
-
const memberExpression = candidateReference.identifier.parent;
|
|
22434
|
-
const assignmentExpression = memberExpression?.parent;
|
|
22435
|
-
if (!memberExpression || !isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName(memberExpression) !== "current" || !assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression") || assignmentExpression.left !== memberExpression) return false;
|
|
22436
|
-
return getIdentifierBindingIdentity(analysis, assignmentExpression.right) !== callbackBinding;
|
|
22437
|
-
})) forwardsCallback = true;
|
|
22438
|
-
});
|
|
22439
|
-
return forwardsCallback;
|
|
22440
|
-
});
|
|
22441
|
-
};
|
|
22442
|
-
const resolveWrappedCallable = (analysis, node) => {
|
|
22443
|
-
const candidate = stripParenExpression(node);
|
|
22444
|
-
if (isFunctionLike$1(candidate)) return candidate;
|
|
22445
|
-
if (isNodeOfType(candidate, "Identifier")) {
|
|
22446
|
-
const reference = getRef(analysis, candidate);
|
|
22447
|
-
if (!reference) return null;
|
|
22448
|
-
if (reference.resolved?.defs.some((definition) => definition.type === "Parameter" || definition.type === "ImportBinding")) return null;
|
|
22449
|
-
const resolved = resolveToFunction(reference);
|
|
22450
|
-
if (resolved) return resolved;
|
|
22451
|
-
const definitionNode = reference.resolved?.defs[0]?.node;
|
|
22452
|
-
if (!definitionNode || !isNodeOfType(definitionNode, "VariableDeclarator")) return null;
|
|
22453
|
-
const initializer = definitionNode.init;
|
|
22454
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return null;
|
|
22455
|
-
if (!isReactUseEffectEventCallee(analysis, initializer.callee) && !localUseEventPreservesCallback(analysis, initializer.callee)) return null;
|
|
22456
|
-
const callback = initializer.arguments?.[0];
|
|
22457
|
-
return callback && isFunctionLike$1(callback) ? callback : null;
|
|
22458
|
-
}
|
|
22459
|
-
if (!isNodeOfType(candidate, "MemberExpression")) return null;
|
|
22460
|
-
if (getStaticMemberName(candidate) !== "current") return null;
|
|
22461
|
-
if (!isNodeOfType(candidate.object, "Identifier")) return null;
|
|
22462
|
-
const reference = getRef(analysis, candidate.object);
|
|
22463
|
-
const declarator = reference?.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
22464
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
22465
|
-
if (!isNodeOfType(declarator.init, "CallExpression")) return null;
|
|
22466
|
-
if (getCallCalleeName$1(declarator.init) !== "useRef") return null;
|
|
22467
|
-
const initializer = declarator.init.arguments?.[0];
|
|
22468
|
-
if (!initializer || !isFunctionLike$1(initializer)) return null;
|
|
22469
|
-
return Boolean(reference?.resolved?.references.some((candidateReference) => {
|
|
22470
|
-
const member = candidateReference.identifier.parent;
|
|
22471
|
-
const assignment = member?.parent;
|
|
22472
|
-
return Boolean(member && isNodeOfType(member, "MemberExpression") && getStaticMemberName(member) === "current" && assignment && isNodeOfType(assignment, "AssignmentExpression") && assignment.left === member);
|
|
22473
|
-
})) ? null : initializer;
|
|
22474
|
-
};
|
|
22475
|
-
const functionInvokesItself = (analysis, functionNode) => {
|
|
22476
|
-
let invokesItself = false;
|
|
22477
|
-
walkAst(functionNode, (child) => {
|
|
22478
|
-
if (invokesItself) return false;
|
|
22479
|
-
if (child !== functionNode && isFunctionLike$1(child)) return false;
|
|
22480
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22481
|
-
if (resolveWrappedCallable(analysis, child.callee) === functionNode) {
|
|
22482
|
-
invokesItself = true;
|
|
22483
|
-
return false;
|
|
22484
|
-
}
|
|
22485
|
-
});
|
|
22486
|
-
return invokesItself;
|
|
22487
|
-
};
|
|
22488
|
-
const collectIntroducedBindings = (analysis, functionNode) => {
|
|
22489
|
-
const introducedBindings = /* @__PURE__ */ new Set();
|
|
22490
|
-
for (const parameter of getFunctionParameters(functionNode)) if (isNodeOfType(parameter, "Identifier")) introducedBindings.add(getParameterBindingIdentity(analysis, functionNode, parameter));
|
|
22491
|
-
return introducedBindings;
|
|
22492
|
-
};
|
|
22493
|
-
const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
22494
|
-
const effectFunction = getEffectFn(analysis, effectNode);
|
|
22495
|
-
if (!effectFunction || !isFunctionLike$1(effectFunction) || effectFunction.async === true) return [];
|
|
22496
|
-
const invokedFunctionEvidence = collectEffectInvokedFunctions(effectFunction);
|
|
22497
|
-
const rootFrame = {
|
|
22498
|
-
functionNode: effectFunction,
|
|
22499
|
-
invocation: null,
|
|
22500
|
-
isDeferred: false,
|
|
22501
|
-
introducedBindings: /* @__PURE__ */ new Set(),
|
|
22502
|
-
substitutions: /* @__PURE__ */ new Map()
|
|
22503
|
-
};
|
|
22504
|
-
const frames = [rootFrame];
|
|
22505
|
-
walkAst(effectFunction, (child) => {
|
|
22506
|
-
if (child !== effectFunction && isFunctionLike$1(child)) return false;
|
|
22507
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22508
|
-
const callee = stripParenExpression(child.callee);
|
|
22509
|
-
const calleeName = getCallCalleeName$1(child);
|
|
22510
|
-
const memberName = getStaticMemberName(callee);
|
|
22511
|
-
const isDeferringCall = calleeName !== null && DEFERRING_CALLEE_NAMES.has(calleeName) || memberName !== null && DEFERRING_MEMBER_NAMES.has(memberName);
|
|
22512
|
-
const isIteratorCall = memberName !== null && SYNCHRONOUS_ITERATOR_METHOD_NAMES.has(memberName) && isNodeOfType(callee, "MemberExpression");
|
|
22513
|
-
const enqueueFrame = (callableNode, argumentsForCallable, isDeferred, introducedBindings, allowWithoutInvocationEvidence = false) => {
|
|
22514
|
-
const callable = resolveWrappedCallable(analysis, callableNode);
|
|
22515
|
-
if (!callable || callable.async === true || callable === effectFunction) return;
|
|
22516
|
-
if (functionInvokesItself(analysis, callable)) return;
|
|
22517
|
-
if (!allowWithoutInvocationEvidence && !invokedFunctionEvidence.has(callable) && !isNodeOfType(callableNode, "Identifier") && !isNodeOfType(callableNode, "MemberExpression")) return;
|
|
22518
|
-
frames.push({
|
|
22519
|
-
functionNode: callable,
|
|
22520
|
-
invocation: child,
|
|
22521
|
-
isDeferred,
|
|
22522
|
-
introducedBindings,
|
|
22523
|
-
substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame)
|
|
22524
|
-
});
|
|
22525
|
-
};
|
|
22526
|
-
if (isFunctionLike$1(callee)) {
|
|
22527
|
-
enqueueFrame(callee, child.arguments ?? [], false, /* @__PURE__ */ new Set());
|
|
22528
|
-
return;
|
|
22529
|
-
}
|
|
22530
|
-
if (isIteratorCall && isNodeOfType(callee, "MemberExpression")) {
|
|
22531
|
-
const collectionExpression = callee.object;
|
|
22532
|
-
for (const argument of child.arguments ?? []) {
|
|
22533
|
-
if (!resolveWrappedCallable(analysis, argument)) continue;
|
|
22534
|
-
enqueueFrame(argument, [collectionExpression], false, /* @__PURE__ */ new Set(), true);
|
|
22535
|
-
}
|
|
22536
|
-
return;
|
|
22059
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
22060
|
+
const callee = node.callee;
|
|
22061
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SUBSCRIPTION_REGISTRATION_METHOD_NAMES.has(callee.property.name);
|
|
22062
|
+
};
|
|
22063
|
+
const isCallArgumentOf = (parent, node) => {
|
|
22064
|
+
if (!isNodeOfType(parent, "CallExpression") && !isNodeOfType(parent, "NewExpression")) return false;
|
|
22065
|
+
return (parent.arguments ?? []).some((argument) => argument === node);
|
|
22066
|
+
};
|
|
22067
|
+
const EVENT_HANDLER_PROPERTY_PATTERN = /^on[a-z]/;
|
|
22068
|
+
const collectEventPropertyHandlerFunctions = (analysis, effectFn) => {
|
|
22069
|
+
const handlerFunctions = [];
|
|
22070
|
+
for (const assignment of findDownstreamNodes(effectFn, "AssignmentExpression")) {
|
|
22071
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=") continue;
|
|
22072
|
+
const target = assignment.left;
|
|
22073
|
+
if (!isNodeOfType(target, "MemberExpression") || !isNodeOfType(target.property, "Identifier") || !EVENT_HANDLER_PROPERTY_PATTERN.test(target.property.name)) continue;
|
|
22074
|
+
const assigned = assignment.right;
|
|
22075
|
+
if (isFunctionLike$1(assigned)) {
|
|
22076
|
+
handlerFunctions.push(assigned);
|
|
22077
|
+
continue;
|
|
22537
22078
|
}
|
|
22538
|
-
if (
|
|
22539
|
-
|
|
22540
|
-
|
|
22541
|
-
|
|
22542
|
-
enqueueFrame(argument, [], true, memberName !== null && DEFERRING_MEMBER_NAMES.has(memberName) ? collectIntroducedBindings(analysis, callable) : /* @__PURE__ */ new Set(), true);
|
|
22543
|
-
}
|
|
22544
|
-
return;
|
|
22079
|
+
if (isNodeOfType(assigned, "Identifier")) {
|
|
22080
|
+
const assignedRef = getRef(analysis, assigned);
|
|
22081
|
+
const resolvedFunction = assignedRef ? resolveToFunction(assignedRef) : null;
|
|
22082
|
+
if (resolvedFunction) handlerFunctions.push(resolvedFunction);
|
|
22545
22083
|
}
|
|
22546
|
-
enqueueFrame(callee, child.arguments ?? [], false, /* @__PURE__ */ new Set());
|
|
22547
|
-
});
|
|
22548
|
-
return frames;
|
|
22549
|
-
};
|
|
22550
|
-
const emptyEvidence = () => ({
|
|
22551
|
-
sourceReferences: /* @__PURE__ */ new Set(),
|
|
22552
|
-
hasUnknownSource: false,
|
|
22553
|
-
hasDeferredIntroducedValue: false,
|
|
22554
|
-
readsExternalValue: false
|
|
22555
|
-
});
|
|
22556
|
-
const mergeEvidence = (target, source) => {
|
|
22557
|
-
for (const reference of source.sourceReferences) target.sourceReferences.add(reference);
|
|
22558
|
-
target.hasUnknownSource ||= source.hasUnknownSource;
|
|
22559
|
-
target.hasDeferredIntroducedValue ||= source.hasDeferredIntroducedValue;
|
|
22560
|
-
target.readsExternalValue ||= source.readsExternalValue;
|
|
22561
|
-
};
|
|
22562
|
-
const getReturnedExpressions = (functionNode) => {
|
|
22563
|
-
if (!isFunctionLike$1(functionNode)) return [];
|
|
22564
|
-
if (!isNodeOfType(functionNode.body, "BlockStatement")) return [functionNode.body];
|
|
22565
|
-
const returnedExpressions = [];
|
|
22566
|
-
walkAst(functionNode.body, (child) => {
|
|
22567
|
-
if (child !== functionNode.body && isFunctionLike$1(child)) return false;
|
|
22568
|
-
if (isNodeOfType(child, "ReturnStatement") && child.argument) returnedExpressions.push(child.argument);
|
|
22569
|
-
});
|
|
22570
|
-
return returnedExpressions;
|
|
22571
|
-
};
|
|
22572
|
-
const isOpaqueHookCall = (callExpression) => {
|
|
22573
|
-
const calleeName = getCallCalleeName$1(callExpression);
|
|
22574
|
-
return Boolean(calleeName && /^use[A-Z0-9]/.test(calleeName) && calleeName !== "useMemo" && calleeName !== "useCallback" && calleeName !== "useEffectEvent");
|
|
22575
|
-
};
|
|
22576
|
-
const isLocallyConstructedObjectMember = (reference, memberExpression) => isNodeOfType(memberExpression, "MemberExpression") && reference.resolved?.defs.some((definition) => {
|
|
22577
|
-
const definitionNode = definition.node;
|
|
22578
|
-
return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
|
|
22579
|
-
}) === true;
|
|
22580
|
-
const collectValueEvidence = (analysis, expression, frame, remainingCallFrames, visitedBindings = /* @__PURE__ */ new Set()) => {
|
|
22581
|
-
const node = stripParenExpression(expression);
|
|
22582
|
-
const evidence = emptyEvidence();
|
|
22583
|
-
if (readsPostMountValue(node) || readsPostMountValueThroughLocals(node, frame.functionNode)) {
|
|
22584
|
-
evidence.readsExternalValue = true;
|
|
22585
|
-
return evidence;
|
|
22586
|
-
}
|
|
22587
|
-
const root = getMemberRoot(node);
|
|
22588
|
-
if (isNodeOfType(root, "Identifier") && STORAGE_GLOBAL_NAMES$1.has(root.name)) {
|
|
22589
|
-
evidence.readsExternalValue = true;
|
|
22590
|
-
return evidence;
|
|
22591
22084
|
}
|
|
22592
|
-
|
|
22593
|
-
|
|
22594
|
-
|
|
22595
|
-
|
|
22596
|
-
|
|
22597
|
-
|
|
22598
|
-
|
|
22599
|
-
|
|
22600
|
-
|
|
22601
|
-
|
|
22602
|
-
|
|
22603
|
-
|
|
22604
|
-
|
|
22605
|
-
|
|
22606
|
-
|
|
22607
|
-
|
|
22608
|
-
|
|
22609
|
-
if (substitution) return collectValueEvidence(analysis, substitution.expression, substitution.frame, remainingCallFrames, visitedBindings);
|
|
22610
|
-
if (isProp(analysis, reference) || isState(analysis, reference)) {
|
|
22611
|
-
evidence.sourceReferences.add(reference);
|
|
22612
|
-
if (isState(analysis, reference) && isExternallyDrivenState(analysis, reference)) evidence.readsExternalValue = true;
|
|
22613
|
-
return evidence;
|
|
22614
|
-
}
|
|
22615
|
-
if (!reference.resolved || visitedBindings.has(reference.resolved)) {
|
|
22616
|
-
evidence.hasUnknownSource = true;
|
|
22617
|
-
return evidence;
|
|
22618
|
-
}
|
|
22619
|
-
visitedBindings.add(reference.resolved);
|
|
22620
|
-
const definitions = reference.resolved.defs;
|
|
22621
|
-
if (definitions.some((definition) => definition.type === "ImportBinding")) {
|
|
22622
|
-
evidence.hasUnknownSource = true;
|
|
22623
|
-
return evidence;
|
|
22624
|
-
}
|
|
22625
|
-
if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) {
|
|
22626
|
-
evidence.hasUnknownSource = true;
|
|
22627
|
-
return evidence;
|
|
22628
|
-
}
|
|
22629
|
-
const initializer = definitions.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init));
|
|
22630
|
-
if (!initializer || !isNodeOfType(initializer, "VariableDeclarator") || !initializer.init) {
|
|
22631
|
-
evidence.hasUnknownSource = true;
|
|
22632
|
-
return evidence;
|
|
22633
|
-
}
|
|
22634
|
-
if (isNodeOfType(initializer.init, "CallExpression") && isOpaqueHookCall(initializer.init)) {
|
|
22635
|
-
evidence.readsExternalValue = true;
|
|
22636
|
-
return evidence;
|
|
22637
|
-
}
|
|
22638
|
-
return collectValueEvidence(analysis, initializer.init, frame, remainingCallFrames, visitedBindings);
|
|
22639
|
-
}
|
|
22640
|
-
if (isNodeOfType(node, "MemberExpression")) {
|
|
22641
|
-
if (isNodeOfType(node.object, "Identifier")) {
|
|
22642
|
-
const objectReference = getRef(analysis, node.object);
|
|
22643
|
-
if (objectReference && isLocallyConstructedObjectMember(objectReference, node)) {
|
|
22644
|
-
evidence.hasUnknownSource = true;
|
|
22645
|
-
return evidence;
|
|
22085
|
+
return handlerFunctions;
|
|
22086
|
+
};
|
|
22087
|
+
const collectSubscriptionCallbackFunctions = (analysis, effectFn) => {
|
|
22088
|
+
const registrationCalls = [...findDownstreamNodes(effectFn, "CallExpression"), ...findDownstreamNodes(effectFn, "NewExpression")].filter((call) => isSubscriptionRegistrationCall(call));
|
|
22089
|
+
const callbackFunctions = collectEventPropertyHandlerFunctions(analysis, effectFn);
|
|
22090
|
+
for (const registrationCall of registrationCalls) {
|
|
22091
|
+
if (!isNodeOfType(registrationCall, "CallExpression") && !isNodeOfType(registrationCall, "NewExpression")) continue;
|
|
22092
|
+
for (const argument of registrationCall.arguments ?? []) {
|
|
22093
|
+
const argumentNode = argument;
|
|
22094
|
+
if (isFunctionLike$1(argumentNode)) {
|
|
22095
|
+
callbackFunctions.push(argumentNode);
|
|
22096
|
+
continue;
|
|
22097
|
+
}
|
|
22098
|
+
if (isNodeOfType(argumentNode, "Identifier")) {
|
|
22099
|
+
const argumentRef = getRef(analysis, argumentNode);
|
|
22100
|
+
const resolvedFunction = argumentRef ? resolveToFunction(argumentRef) : null;
|
|
22101
|
+
if (resolvedFunction) callbackFunctions.push(resolvedFunction);
|
|
22646
22102
|
}
|
|
22647
22103
|
}
|
|
22648
|
-
mergeEvidence(evidence, collectValueEvidence(analysis, node.object, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
22649
|
-
if (node.computed) mergeEvidence(evidence, collectValueEvidence(analysis, node.property, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
22650
|
-
return evidence;
|
|
22651
|
-
}
|
|
22652
|
-
if (isNodeOfType(node, "CallExpression")) {
|
|
22653
|
-
if (isOpaqueHookCall(node)) {
|
|
22654
|
-
evidence.readsExternalValue = true;
|
|
22655
|
-
return evidence;
|
|
22656
|
-
}
|
|
22657
|
-
const callee = stripParenExpression(node.callee);
|
|
22658
|
-
const calleeRoot = getMemberRoot(callee);
|
|
22659
|
-
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(calleeRoot, "Identifier") && PURE_GLOBAL_NAMESPACE_NAMES.has(calleeRoot.name) && getIdentifierBindingIdentity(analysis, calleeRoot) === null;
|
|
22660
|
-
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
|
|
22661
|
-
if (isPureMemberTransform) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
22662
|
-
for (const argument of node.arguments ?? []) {
|
|
22663
|
-
if (isFunctionLike$1(argument)) continue;
|
|
22664
|
-
mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
22665
|
-
}
|
|
22666
|
-
if (isPureGlobalCall || isPureMemberTransform) return evidence;
|
|
22667
|
-
if (isNodeOfType(callee, "MemberExpression")) {
|
|
22668
|
-
evidence.hasUnknownSource = true;
|
|
22669
|
-
return evidence;
|
|
22670
|
-
}
|
|
22671
|
-
if (remainingCallFrames <= 0) {
|
|
22672
|
-
evidence.hasUnknownSource = true;
|
|
22673
|
-
return evidence;
|
|
22674
|
-
}
|
|
22675
|
-
const callable = resolveWrappedCallable(analysis, callee);
|
|
22676
|
-
if (!callable || callable.async === true || functionInvokesItself(analysis, callable)) {
|
|
22677
|
-
evidence.hasUnknownSource = true;
|
|
22678
|
-
return evidence;
|
|
22679
|
-
}
|
|
22680
|
-
const valueFrame = {
|
|
22681
|
-
functionNode: callable,
|
|
22682
|
-
invocation: node,
|
|
22683
|
-
isDeferred: false,
|
|
22684
|
-
introducedBindings: /* @__PURE__ */ new Set(),
|
|
22685
|
-
substitutions: buildSubstitutions(analysis, callable, node.arguments ?? [], frame)
|
|
22686
|
-
};
|
|
22687
|
-
const returnedExpressions = getReturnedExpressions(callable);
|
|
22688
|
-
if (returnedExpressions.length === 0) {
|
|
22689
|
-
evidence.hasUnknownSource = true;
|
|
22690
|
-
return evidence;
|
|
22691
|
-
}
|
|
22692
|
-
for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, valueFrame, remainingCallFrames - 1, new Set(visitedBindings)));
|
|
22693
|
-
return evidence;
|
|
22694
|
-
}
|
|
22695
|
-
if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
|
|
22696
|
-
evidence.hasUnknownSource = true;
|
|
22697
|
-
return evidence;
|
|
22698
|
-
}
|
|
22699
|
-
const nodeRecord = node;
|
|
22700
|
-
for (const [key, value] of Object.entries(nodeRecord)) {
|
|
22701
|
-
if (key === "parent" || key === "type" || key === "key") continue;
|
|
22702
|
-
if (Array.isArray(value)) for (const child of value) {
|
|
22703
|
-
if (!child || typeof child !== "object" || !("type" in child)) continue;
|
|
22704
|
-
mergeEvidence(evidence, collectValueEvidence(analysis, child, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
22705
|
-
}
|
|
22706
|
-
else if (value && typeof value === "object" && "type" in value) mergeEvidence(evidence, collectValueEvidence(analysis, value, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
22707
22104
|
}
|
|
22708
|
-
return
|
|
22105
|
+
return callbackFunctions;
|
|
22709
22106
|
};
|
|
22710
|
-
const
|
|
22711
|
-
|
|
22712
|
-
|
|
22713
|
-
|
|
22714
|
-
|
|
22715
|
-
if (!reference) return null;
|
|
22716
|
-
if (resolveToFunction(reference)) return null;
|
|
22717
|
-
if (isStateSetter(analysis, reference)) return reference;
|
|
22718
|
-
return getUpstreamRefs(analysis, reference).find((upstreamReference) => isStateSetter(analysis, upstreamReference)) ?? null;
|
|
22719
|
-
};
|
|
22720
|
-
const isSameSimpleValue = (analysis, leftExpression, rightExpression) => {
|
|
22721
|
-
const left = stripParenExpression(leftExpression);
|
|
22722
|
-
const right = stripParenExpression(rightExpression);
|
|
22723
|
-
if (left.type !== right.type) return false;
|
|
22724
|
-
if (isNodeOfType(left, "Identifier") && isNodeOfType(right, "Identifier")) {
|
|
22725
|
-
const leftBinding = getIdentifierBindingIdentity(analysis, left);
|
|
22726
|
-
const rightBinding = getIdentifierBindingIdentity(analysis, right);
|
|
22727
|
-
if (leftBinding || rightBinding) return leftBinding !== null && leftBinding === rightBinding;
|
|
22728
|
-
return left.name === right.name;
|
|
22107
|
+
const isInsideAnyFunction = (identifier, functions) => {
|
|
22108
|
+
let current = identifier;
|
|
22109
|
+
while (current) {
|
|
22110
|
+
if (functions.includes(current)) return true;
|
|
22111
|
+
current = current.parent;
|
|
22729
22112
|
}
|
|
22730
|
-
if (isNodeOfType(left, "Literal") && isNodeOfType(right, "Literal")) return left.value === right.value;
|
|
22731
|
-
if (isNodeOfType(left, "MemberExpression") && isNodeOfType(right, "MemberExpression")) return left.computed === right.computed && isSameSimpleValue(analysis, left.object, right.object) && isSameSimpleValue(analysis, left.property, right.property);
|
|
22732
22113
|
return false;
|
|
22733
22114
|
};
|
|
22734
|
-
const
|
|
22735
|
-
|
|
22736
|
-
if (!
|
|
22737
|
-
|
|
22738
|
-
|
|
22739
|
-
|
|
22740
|
-
|
|
22741
|
-
|
|
22742
|
-
|
|
22743
|
-
|
|
22744
|
-
|
|
22745
|
-
|
|
22746
|
-
|
|
22747
|
-
|
|
22748
|
-
if (child !== frame.functionNode && isFunctionLike$1(child)) return false;
|
|
22749
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22750
|
-
const setterReference = findStateSetterReference(analysis, child);
|
|
22751
|
-
if (setterReference) calls.push({
|
|
22752
|
-
callExpression: child,
|
|
22753
|
-
setterReference
|
|
22754
|
-
});
|
|
22115
|
+
const isSubscriptionRegistrationArgument = (identifier) => {
|
|
22116
|
+
const parent = identifier.parent;
|
|
22117
|
+
if (!parent || !isSubscriptionRegistrationCall(parent)) return false;
|
|
22118
|
+
return isCallArgumentOf(parent, identifier);
|
|
22119
|
+
};
|
|
22120
|
+
const hasSubscriptionCallbackCallToSameSetter = (analysis, setterRef, effectFnRefs, effectFn) => {
|
|
22121
|
+
const callbackFunctions = collectSubscriptionCallbackFunctions(analysis, effectFn);
|
|
22122
|
+
return effectFnRefs.some((otherRef) => {
|
|
22123
|
+
if (otherRef === setterRef) return false;
|
|
22124
|
+
if (otherRef.resolved !== setterRef.resolved) return false;
|
|
22125
|
+
const otherIdentifier = otherRef.identifier;
|
|
22126
|
+
if (isSubscriptionRegistrationArgument(otherIdentifier)) return true;
|
|
22127
|
+
if (!getCallExpr(otherRef)) return false;
|
|
22128
|
+
return isInsideAnyFunction(otherIdentifier, callbackFunctions);
|
|
22755
22129
|
});
|
|
22756
|
-
return calls;
|
|
22757
22130
|
};
|
|
22758
|
-
const
|
|
22759
|
-
|
|
22760
|
-
|
|
22761
|
-
|
|
22762
|
-
|
|
22763
|
-
|
|
22764
|
-
|
|
22765
|
-
|
|
22766
|
-
|
|
22131
|
+
const derivesFromPostMountRead = (ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
22132
|
+
const node = def.node;
|
|
22133
|
+
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
22134
|
+
return readsPostMountValue(node.init);
|
|
22135
|
+
}));
|
|
22136
|
+
const PROMISE_CONTINUATION_METHOD_NAMES = new Set([
|
|
22137
|
+
"then",
|
|
22138
|
+
"catch",
|
|
22139
|
+
"finally"
|
|
22140
|
+
]);
|
|
22141
|
+
const isPromiseFlowFunction = (fn) => {
|
|
22142
|
+
if (isFunctionLike$1(fn) && fn.async) return true;
|
|
22143
|
+
const parent = fn.parent;
|
|
22144
|
+
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
22145
|
+
if (!(parent.arguments ?? []).some((argument) => argument === fn)) return false;
|
|
22146
|
+
const callee = parent.callee;
|
|
22147
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && PROMISE_CONTINUATION_METHOD_NAMES.has(callee.property.name);
|
|
22148
|
+
};
|
|
22149
|
+
const isPromiseFlowStateSetterCall = (analysis, ref, effectFn) => {
|
|
22150
|
+
if (!isStateSetterCall(analysis, ref)) return false;
|
|
22151
|
+
let current = ref.identifier.parent;
|
|
22152
|
+
let sawPromiseFlowFunction = false;
|
|
22153
|
+
while (current && current !== effectFn) {
|
|
22154
|
+
if (isFunctionLike$1(current) && !sawPromiseFlowFunction) {
|
|
22155
|
+
if (!isPromiseFlowFunction(current)) return false;
|
|
22156
|
+
sawPromiseFlowFunction = true;
|
|
22767
22157
|
}
|
|
22768
|
-
|
|
22158
|
+
current = current.parent;
|
|
22769
22159
|
}
|
|
22770
|
-
return
|
|
22160
|
+
return sawPromiseFlowFunction;
|
|
22771
22161
|
};
|
|
22772
|
-
const
|
|
22773
|
-
const
|
|
22774
|
-
|
|
22775
|
-
const effectHasCleanup = hasCleanup(analysis, effectNode);
|
|
22776
|
-
const cleanupManagedStateDeclarators = /* @__PURE__ */ new Set();
|
|
22777
|
-
const facts = [];
|
|
22778
|
-
for (const frame of frames) for (const { callExpression, setterReference } of collectFrameSetterCalls(analysis, frame)) {
|
|
22779
|
-
if (!isNodeOfType(callExpression, "CallExpression")) continue;
|
|
22780
|
-
if ((callExpression.arguments ?? []).length !== 1) continue;
|
|
22781
|
-
const writtenValue = callExpression.arguments?.[0];
|
|
22782
|
-
if (!writtenValue) continue;
|
|
22783
|
-
const stateDeclarator = getUseStateDecl(analysis, setterReference);
|
|
22784
|
-
if (!stateDeclarator) continue;
|
|
22785
|
-
const remainingValueCallFrames = frame === frames[0] ? 1 : 0;
|
|
22786
|
-
let valueEvidence;
|
|
22787
|
-
if (isFunctionLike$1(writtenValue)) {
|
|
22788
|
-
const updaterFrame = {
|
|
22789
|
-
functionNode: writtenValue,
|
|
22790
|
-
invocation: callExpression,
|
|
22791
|
-
isDeferred: frame.isDeferred,
|
|
22792
|
-
introducedBindings: collectIntroducedBindings(analysis, writtenValue),
|
|
22793
|
-
substitutions: /* @__PURE__ */ new Map()
|
|
22794
|
-
};
|
|
22795
|
-
valueEvidence = emptyEvidence();
|
|
22796
|
-
const returnedExpressions = getReturnedExpressions(writtenValue);
|
|
22797
|
-
if (returnedExpressions.length === 0) valueEvidence.hasUnknownSource = true;
|
|
22798
|
-
for (const returnedExpression of returnedExpressions) mergeEvidence(valueEvidence, collectValueEvidence(analysis, returnedExpression, updaterFrame, remainingValueCallFrames));
|
|
22799
|
-
} else valueEvidence = collectValueEvidence(analysis, writtenValue, frame, remainingValueCallFrames);
|
|
22800
|
-
const sourceReferences = [...valueEvidence.sourceReferences].filter((sourceReference) => getUseStateDecl(analysis, sourceReference) !== stateDeclarator);
|
|
22801
|
-
const hasIndependentWriter = hasUserInputSetterWriter(setterReference, effectNode, true);
|
|
22802
|
-
const doesMatchStateInitializer = matchesStateInitializer(analysis, callExpression, stateDeclarator);
|
|
22803
|
-
if (effectHasCleanup && (frame.isDeferred || valueEvidence.hasUnknownSource || valueEvidence.hasDeferredIntroducedValue || valueEvidence.readsExternalValue)) cleanupManagedStateDeclarators.add(stateDeclarator);
|
|
22804
|
-
const isRenderKnownCopy = sourceReferences.length > 0 && !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue && !hasIndependentWriter;
|
|
22805
|
-
facts.push({
|
|
22806
|
-
callExpression,
|
|
22807
|
-
executionNode: frame.invocation ?? callExpression,
|
|
22808
|
-
setterReference,
|
|
22809
|
-
stateDeclarator,
|
|
22810
|
-
sourceReferences,
|
|
22811
|
-
isDeferred: frame.isDeferred,
|
|
22812
|
-
isRenderKnownCopy,
|
|
22813
|
-
matchesStateInitializer: doesMatchStateInitializer,
|
|
22814
|
-
resetsSourceState: false
|
|
22815
|
-
});
|
|
22816
|
-
}
|
|
22817
|
-
return facts.map((fact) => {
|
|
22818
|
-
const resetsSourceState = fact.sourceReferences.filter((sourceReference) => isState(analysis, sourceReference)).map((sourceReference) => getUseStateDecl(analysis, sourceReference)).filter((declarator) => Boolean(declarator)).some((sourceDeclarator) => facts.some((candidateFact) => !candidateFact.isDeferred && candidateFact.stateDeclarator === sourceDeclarator && !areInMutuallyExclusiveBranches(fact.executionNode, candidateFact.executionNode)));
|
|
22819
|
-
const { executionNode: _executionNode, ...publicFact } = fact;
|
|
22820
|
-
return {
|
|
22821
|
-
...publicFact,
|
|
22822
|
-
resetsSourceState,
|
|
22823
|
-
isRenderKnownCopy: fact.isRenderKnownCopy && !cleanupManagedStateDeclarators.has(fact.stateDeclarator) && !resetsSourceState
|
|
22824
|
-
};
|
|
22825
|
-
});
|
|
22162
|
+
const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
22163
|
+
const callMethodNames = findDownstreamNodes(effectFn, "CallExpression").map((call) => getCallMethodName(call.callee)).filter((name) => Boolean(name));
|
|
22164
|
+
return callMethodNames.includes("createObjectURL") && callMethodNames.includes("revokeObjectURL");
|
|
22826
22165
|
};
|
|
22827
|
-
//#endregion
|
|
22828
|
-
//#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
|
|
22829
22166
|
const noAdjustStateOnPropChange = defineRule({
|
|
22830
22167
|
id: "no-adjust-state-on-prop-change",
|
|
22831
22168
|
title: "State synced to a prop inside an effect",
|
|
@@ -22836,13 +22173,25 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
22836
22173
|
if (!isUseEffect(node)) return;
|
|
22837
22174
|
const analysis = getProgramAnalysis(node);
|
|
22838
22175
|
if (!analysis) return;
|
|
22839
|
-
const
|
|
22840
|
-
|
|
22841
|
-
if (!
|
|
22842
|
-
|
|
22843
|
-
|
|
22176
|
+
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
22177
|
+
const depsRefs = getEffectDepsRefs(analysis, node);
|
|
22178
|
+
if (!effectFnRefs || !depsRefs) return;
|
|
22179
|
+
const effectFn = getEffectFn(analysis, node);
|
|
22180
|
+
if (!effectFn) return;
|
|
22181
|
+
if (!depsRefs.flatMap((ref) => isState(analysis, ref) ? [] : getUpstreamRefs(analysis, ref)).some((ref) => isProp(analysis, ref))) return;
|
|
22182
|
+
if (isObjectUrlLifecycleEffect(effectFn)) return;
|
|
22183
|
+
const hasAsyncStateSetter = effectFnRefs.some((ref) => isPromiseFlowStateSetterCall(analysis, ref, effectFn));
|
|
22184
|
+
for (const ref of effectFnRefs) {
|
|
22185
|
+
if (!isSyncStateSetterCall(analysis, ref, effectFn)) continue;
|
|
22186
|
+
const callExpr = getCallExpr(ref);
|
|
22187
|
+
if (!callExpr) continue;
|
|
22188
|
+
if (readsPostMountValue(callExpr)) continue;
|
|
22189
|
+
if (getArgsUpstreamRefs(analysis, ref).some((argRef) => derivesFromPostMountRead(argRef))) continue;
|
|
22190
|
+
if (isNodeOfType(callExpr, "CallExpression") && isConstantSetterArgument(analysis, callExpr) && hasSubscriptionCallbackCallToSameSetter(analysis, ref, effectFnRefs, effectFn)) continue;
|
|
22191
|
+
if (hasAsyncStateSetter && isNodeOfType(callExpr, "CallExpression") && isConstantSetterArgument(analysis, callExpr)) continue;
|
|
22192
|
+
if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isProp(analysis, argRef))) continue;
|
|
22844
22193
|
context.report({
|
|
22845
|
-
node:
|
|
22194
|
+
node: callExpr,
|
|
22846
22195
|
message: "This effect adjusts state after a prop changes, so users briefly see the stale value."
|
|
22847
22196
|
});
|
|
22848
22197
|
}
|
|
@@ -24856,6 +24205,180 @@ const noCascadingSetState = defineRule({
|
|
|
24856
24205
|
} })
|
|
24857
24206
|
});
|
|
24858
24207
|
//#endregion
|
|
24208
|
+
//#region src/plugin/rules/state-and-effects/utils/reads-post-mount-through-locals.ts
|
|
24209
|
+
const isBareRefCurrentRead = (node) => isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && node.property.name === "current";
|
|
24210
|
+
const matchesPostMountRead = (node, options) => {
|
|
24211
|
+
if (isPostMountGlobalRead(node)) return true;
|
|
24212
|
+
if (!isPostMountMemberRead(node)) return false;
|
|
24213
|
+
return !(options.ignoreBareRefCurrent === true && isBareRefCurrentRead(node));
|
|
24214
|
+
};
|
|
24215
|
+
const objectPatternBindsName$1 = (pattern, name) => {
|
|
24216
|
+
if (!isNodeOfType(pattern, "ObjectPattern")) return false;
|
|
24217
|
+
return (pattern.properties ?? []).some((property) => {
|
|
24218
|
+
if (!isNodeOfType(property, "Property")) return false;
|
|
24219
|
+
const bound = property.value;
|
|
24220
|
+
return Boolean(bound && isNodeOfType(bound, "Identifier") && bound.name === name);
|
|
24221
|
+
});
|
|
24222
|
+
};
|
|
24223
|
+
const findEffectLocalInitializer$1 = (effectFn, name) => {
|
|
24224
|
+
let initializer = null;
|
|
24225
|
+
walkAst(effectFn, (child) => {
|
|
24226
|
+
if (initializer) return false;
|
|
24227
|
+
if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
|
|
24228
|
+
if (isNodeOfType(child.id, "Identifier") && child.id.name === name || objectPatternBindsName$1(child.id, name)) {
|
|
24229
|
+
initializer = child.init;
|
|
24230
|
+
return false;
|
|
24231
|
+
}
|
|
24232
|
+
});
|
|
24233
|
+
return initializer;
|
|
24234
|
+
};
|
|
24235
|
+
const readsPostMountValueThroughLocals = (root, effectFn, options = {}, visitedLocalNames = /* @__PURE__ */ new Set()) => {
|
|
24236
|
+
let found = false;
|
|
24237
|
+
walkAst(root, (child) => {
|
|
24238
|
+
if (found) return false;
|
|
24239
|
+
if (matchesPostMountRead(child, options)) {
|
|
24240
|
+
found = true;
|
|
24241
|
+
return false;
|
|
24242
|
+
}
|
|
24243
|
+
if (!isNodeOfType(child, "Identifier")) return;
|
|
24244
|
+
if (visitedLocalNames.has(child.name)) return;
|
|
24245
|
+
visitedLocalNames.add(child.name);
|
|
24246
|
+
const localInitializer = findEffectLocalInitializer$1(effectFn, child.name);
|
|
24247
|
+
if (localInitializer && readsPostMountValueThroughLocals(localInitializer, effectFn, options, visitedLocalNames)) {
|
|
24248
|
+
found = true;
|
|
24249
|
+
return false;
|
|
24250
|
+
}
|
|
24251
|
+
});
|
|
24252
|
+
return found;
|
|
24253
|
+
};
|
|
24254
|
+
//#endregion
|
|
24255
|
+
//#region src/plugin/rules/state-and-effects/utils/effect/external-state.ts
|
|
24256
|
+
const DEFERRING_CALLEE_NAMES = new Set([
|
|
24257
|
+
"setTimeout",
|
|
24258
|
+
"setInterval",
|
|
24259
|
+
"setImmediate",
|
|
24260
|
+
"requestAnimationFrame",
|
|
24261
|
+
"requestIdleCallback",
|
|
24262
|
+
"queueMicrotask",
|
|
24263
|
+
"addEventListener",
|
|
24264
|
+
"addListener",
|
|
24265
|
+
"subscribe",
|
|
24266
|
+
"observe",
|
|
24267
|
+
"watch",
|
|
24268
|
+
"watchPosition",
|
|
24269
|
+
"then",
|
|
24270
|
+
"catch",
|
|
24271
|
+
"finally",
|
|
24272
|
+
"on",
|
|
24273
|
+
"once"
|
|
24274
|
+
]);
|
|
24275
|
+
const getCalleeName$1 = (callee) => {
|
|
24276
|
+
if (!callee) return null;
|
|
24277
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
24278
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
24279
|
+
return null;
|
|
24280
|
+
};
|
|
24281
|
+
const parentOf = (node) => node.parent ?? null;
|
|
24282
|
+
const argumentsInclude = (args, target) => (args ?? []).some((argument) => argument === target);
|
|
24283
|
+
const isDeferredCallbackPosition = (expression) => {
|
|
24284
|
+
const parent = parentOf(expression);
|
|
24285
|
+
if (!parent) return false;
|
|
24286
|
+
if (isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, expression)) {
|
|
24287
|
+
const name = getCalleeName$1(parent.callee);
|
|
24288
|
+
if (name && DEFERRING_CALLEE_NAMES.has(name)) return true;
|
|
24289
|
+
}
|
|
24290
|
+
if (isNodeOfType(parent, "NewExpression") && argumentsInclude(parent.arguments, expression)) {
|
|
24291
|
+
const name = getCalleeName$1(parent.callee);
|
|
24292
|
+
if (name && (name.endsWith("Observer") || name === "Promise")) return true;
|
|
24293
|
+
}
|
|
24294
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === expression && isNodeOfType(parent.left, "MemberExpression") && isNodeOfType(parent.left.property, "Identifier") && parent.left.property.name.startsWith("on")) return true;
|
|
24295
|
+
return false;
|
|
24296
|
+
};
|
|
24297
|
+
const getHandlerDeclarator = (fn) => {
|
|
24298
|
+
let current = fn;
|
|
24299
|
+
let parent = parentOf(current);
|
|
24300
|
+
while (parent && isNodeOfType(parent, "CallExpression") && argumentsInclude(parent.arguments, current)) {
|
|
24301
|
+
current = parent;
|
|
24302
|
+
parent = parentOf(current);
|
|
24303
|
+
}
|
|
24304
|
+
if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent;
|
|
24305
|
+
return null;
|
|
24306
|
+
};
|
|
24307
|
+
const isNamedHandlerUsedAsDeferredCallback = (analysis, fn) => {
|
|
24308
|
+
const declarator = getHandlerDeclarator(fn);
|
|
24309
|
+
if (!declarator || !isNodeOfType(declarator.id, "Identifier")) return false;
|
|
24310
|
+
const name = declarator.id.name;
|
|
24311
|
+
for (const scope of analysis.scopeManager.scopes) {
|
|
24312
|
+
const variable = scope.variables.find((candidate) => candidate.name === name && candidate.defs.some((def) => def.node === declarator));
|
|
24313
|
+
if (!variable) continue;
|
|
24314
|
+
return variable.references.some((reference) => isDeferredCallbackPosition(reference.identifier));
|
|
24315
|
+
}
|
|
24316
|
+
return false;
|
|
24317
|
+
};
|
|
24318
|
+
const isDeferredCallbackFunction = (analysis, fn) => {
|
|
24319
|
+
if (isDeferredCallbackPosition(fn)) return true;
|
|
24320
|
+
return isNamedHandlerUsedAsDeferredCallback(analysis, fn);
|
|
24321
|
+
};
|
|
24322
|
+
const isInsideDeferredCallback$1 = (analysis, node, boundary) => {
|
|
24323
|
+
let current = parentOf(node);
|
|
24324
|
+
while (current && current !== boundary) {
|
|
24325
|
+
if (isFunctionLike$1(current) && isDeferredCallbackFunction(analysis, current)) return true;
|
|
24326
|
+
current = parentOf(current);
|
|
24327
|
+
}
|
|
24328
|
+
return false;
|
|
24329
|
+
};
|
|
24330
|
+
const findUseStateDeclarator = (ref) => {
|
|
24331
|
+
for (const def of ref.resolved?.defs ?? []) {
|
|
24332
|
+
const node = def.node;
|
|
24333
|
+
if (!isNodeOfType(node, "VariableDeclarator")) continue;
|
|
24334
|
+
if (!isNodeOfType(node.init, "CallExpression")) continue;
|
|
24335
|
+
if (!isNodeOfType(node.id, "ArrayPattern")) continue;
|
|
24336
|
+
return node;
|
|
24337
|
+
}
|
|
24338
|
+
return null;
|
|
24339
|
+
};
|
|
24340
|
+
const declaratorToExternallyDriven = /* @__PURE__ */ new WeakMap();
|
|
24341
|
+
const isExternallyDrivenState = (analysis, ref) => {
|
|
24342
|
+
const declarator = findUseStateDeclarator(ref);
|
|
24343
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
|
|
24344
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
24345
|
+
const cached = declaratorToExternallyDriven.get(declarator);
|
|
24346
|
+
if (cached !== void 0) return cached;
|
|
24347
|
+
const result = computeExternallyDriven(analysis, declarator);
|
|
24348
|
+
declaratorToExternallyDriven.set(declarator, result);
|
|
24349
|
+
return result;
|
|
24350
|
+
};
|
|
24351
|
+
const computeExternallyDriven = (analysis, declarator) => {
|
|
24352
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
24353
|
+
const setterElement = declarator.id.elements?.[1];
|
|
24354
|
+
if (!setterElement || !isNodeOfType(setterElement, "Identifier")) return false;
|
|
24355
|
+
const setterName = setterElement.name;
|
|
24356
|
+
let setterVariable = null;
|
|
24357
|
+
for (const scope of analysis.scopeManager.scopes) {
|
|
24358
|
+
const match = scope.variables.find((variable) => variable.name === setterName && variable.defs.some((def) => def.node === declarator));
|
|
24359
|
+
if (match) {
|
|
24360
|
+
setterVariable = match;
|
|
24361
|
+
break;
|
|
24362
|
+
}
|
|
24363
|
+
}
|
|
24364
|
+
if (!setterVariable) return false;
|
|
24365
|
+
let hasDeferredCallSite = false;
|
|
24366
|
+
for (const setterReference of setterVariable.references) {
|
|
24367
|
+
const identifier = setterReference.identifier;
|
|
24368
|
+
const parent = parentOf(identifier);
|
|
24369
|
+
if (!parent) continue;
|
|
24370
|
+
if (isDeferredCallbackPosition(identifier)) {
|
|
24371
|
+
hasDeferredCallSite = true;
|
|
24372
|
+
continue;
|
|
24373
|
+
}
|
|
24374
|
+
if (!isNodeOfType(parent, "CallExpression")) continue;
|
|
24375
|
+
if (parent.callee !== identifier) continue;
|
|
24376
|
+
if (!isInsideDeferredCallback$1(analysis, parent, declarator)) return false;
|
|
24377
|
+
hasDeferredCallSite = true;
|
|
24378
|
+
}
|
|
24379
|
+
return hasDeferredCallSite;
|
|
24380
|
+
};
|
|
24381
|
+
//#endregion
|
|
24859
24382
|
//#region src/plugin/rules/state-and-effects/no-chain-state-updates.ts
|
|
24860
24383
|
const getUseStateDeclarator = (ref) => (ref.resolved?.defs ?? []).map((def) => def.node).find((node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "ArrayPattern")) ?? null;
|
|
24861
24384
|
const isDeclaredWithin = (node, container) => {
|
|
@@ -25596,16 +25119,322 @@ const noDefaultProps = defineRule({
|
|
|
25596
25119
|
} })
|
|
25597
25120
|
});
|
|
25598
25121
|
//#endregion
|
|
25122
|
+
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
25123
|
+
const isInitialOnlyPropName = (propName) => {
|
|
25124
|
+
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
25125
|
+
return /^initial[A-Z]/.test(propName) || /^default[A-Z]/.test(propName) || /^seed[A-Z]/.test(propName) || /^starting[A-Z]/.test(propName) || /^baseline[A-Z]/.test(propName) || /^preset[A-Z]/.test(propName);
|
|
25126
|
+
};
|
|
25127
|
+
//#endregion
|
|
25128
|
+
//#region src/plugin/rules/state-and-effects/utils/event-handler-reference.ts
|
|
25129
|
+
const isEventHandlerName = (name) => /^on[A-Z]/.test(name);
|
|
25130
|
+
const getStaticMemberReferenceName = (node, resolveName = (name) => name) => {
|
|
25131
|
+
if (!isNodeOfType(node, "MemberExpression")) return null;
|
|
25132
|
+
if (!isNodeOfType(node.object, "Identifier")) return null;
|
|
25133
|
+
const propertyName = getStaticMemberPropertyName(node);
|
|
25134
|
+
return propertyName ? `${resolveName(node.object.name)}.${propertyName}` : null;
|
|
25135
|
+
};
|
|
25136
|
+
const isEventHandlerValue = (node, eventHandlerReferenceNames, resolveName = (name) => name) => {
|
|
25137
|
+
if (isInlineFunctionExpression(node)) return true;
|
|
25138
|
+
if (isNodeOfType(node, "Identifier")) return eventHandlerReferenceNames.has(resolveName(node.name));
|
|
25139
|
+
const memberReferenceName = getStaticMemberReferenceName(node, resolveName);
|
|
25140
|
+
return Boolean(memberReferenceName && eventHandlerReferenceNames.has(memberReferenceName));
|
|
25141
|
+
};
|
|
25142
|
+
const isIntrinsicJsxAttribute = (node) => {
|
|
25143
|
+
if (!isNodeOfType(node, "JSXAttribute")) return false;
|
|
25144
|
+
const openingElement = node.parent;
|
|
25145
|
+
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
25146
|
+
const elementName = openingElement.name;
|
|
25147
|
+
if (!isNodeOfType(elementName, "JSXIdentifier")) return false;
|
|
25148
|
+
return /^[a-z]/.test(elementName.name);
|
|
25149
|
+
};
|
|
25150
|
+
//#endregion
|
|
25151
|
+
//#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
|
|
25152
|
+
const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
|
|
25153
|
+
const collectComponentPropNames = (componentFunction) => {
|
|
25154
|
+
const cached = componentPropNamesCache.get(componentFunction);
|
|
25155
|
+
if (cached) return cached;
|
|
25156
|
+
const propNames = /* @__PURE__ */ new Set();
|
|
25157
|
+
if (!isFunctionLike$1(componentFunction)) return propNames;
|
|
25158
|
+
const propsObjectParamNames = /* @__PURE__ */ new Set();
|
|
25159
|
+
for (const param of componentFunction.params ?? []) {
|
|
25160
|
+
collectPatternNames(param, propNames);
|
|
25161
|
+
if (isNodeOfType(param, "Identifier")) propsObjectParamNames.add(param.name);
|
|
25162
|
+
}
|
|
25163
|
+
const componentBody = componentFunction.body;
|
|
25164
|
+
if (!componentBody) return propNames;
|
|
25165
|
+
walkAst(componentBody, (child) => {
|
|
25166
|
+
if (child !== componentBody && isFunctionLike$1(child)) return false;
|
|
25167
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
|
|
25168
|
+
});
|
|
25169
|
+
componentPropNamesCache.set(componentFunction, propNames);
|
|
25170
|
+
return propNames;
|
|
25171
|
+
};
|
|
25172
|
+
const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
25173
|
+
const getOwnScopeBoundNames = (functionNode) => {
|
|
25174
|
+
const cached = ownScopeBoundNamesCache.get(functionNode);
|
|
25175
|
+
if (cached) return cached;
|
|
25176
|
+
const boundNames = /* @__PURE__ */ new Set();
|
|
25177
|
+
if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
|
|
25178
|
+
walkAst(functionNode, (child) => {
|
|
25179
|
+
if (child !== functionNode && isFunctionLike$1(child)) return false;
|
|
25180
|
+
if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
|
|
25181
|
+
});
|
|
25182
|
+
ownScopeBoundNamesCache.set(functionNode, boundNames);
|
|
25183
|
+
return boundNames;
|
|
25184
|
+
};
|
|
25185
|
+
const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
|
|
25186
|
+
const isPropertyNamePosition = (identifier) => {
|
|
25187
|
+
const parent = identifier.parent;
|
|
25188
|
+
if (!parent) return false;
|
|
25189
|
+
if (isNodeOfType(parent, "MemberExpression")) return parent.property === identifier && !parent.computed;
|
|
25190
|
+
if (isNodeOfType(parent, "Property")) return parent.key === identifier && !parent.computed;
|
|
25191
|
+
return false;
|
|
25192
|
+
};
|
|
25193
|
+
const referencesIdentifierNamed = (root, identifierName) => {
|
|
25194
|
+
let isReferenced = false;
|
|
25195
|
+
walkAst(root, (child) => {
|
|
25196
|
+
if (isReferenced) return false;
|
|
25197
|
+
if (child !== root && isFunctionLike$1(child) && declaresBindingNamed(child, identifierName)) return false;
|
|
25198
|
+
if (isNodeOfType(child, "Identifier") && child.name === identifierName && !isPropertyNamePosition(child)) {
|
|
25199
|
+
isReferenced = true;
|
|
25200
|
+
return false;
|
|
25201
|
+
}
|
|
25202
|
+
});
|
|
25203
|
+
return isReferenced;
|
|
25204
|
+
};
|
|
25205
|
+
const isSetterWiredToJsxHandler = (componentFunction, setterName) => {
|
|
25206
|
+
let isWired = false;
|
|
25207
|
+
walkAst(componentFunction, (child) => {
|
|
25208
|
+
if (isWired) return false;
|
|
25209
|
+
if (child !== componentFunction && isFunctionLike$1(child) && declaresBindingNamed(child, setterName)) return false;
|
|
25210
|
+
if (!isNodeOfType(child, "JSXAttribute") || !child.value) return;
|
|
25211
|
+
const attributeName = getJsxAttributeName(child.name);
|
|
25212
|
+
if (!attributeName || !isEventHandlerName(attributeName)) return;
|
|
25213
|
+
if (referencesIdentifierNamed(child.value, setterName)) {
|
|
25214
|
+
isWired = true;
|
|
25215
|
+
return false;
|
|
25216
|
+
}
|
|
25217
|
+
});
|
|
25218
|
+
return isWired;
|
|
25219
|
+
};
|
|
25220
|
+
const isControlledPropMirror = (effectNode, setterCall) => {
|
|
25221
|
+
if (!isNodeOfType(setterCall, "CallExpression")) return false;
|
|
25222
|
+
if (!isNodeOfType(setterCall.callee, "Identifier")) return false;
|
|
25223
|
+
const setterArguments = setterCall.arguments ?? [];
|
|
25224
|
+
if (setterArguments.length !== 1) return false;
|
|
25225
|
+
const mirroredArgument = setterArguments[0];
|
|
25226
|
+
if (!isNodeOfType(mirroredArgument, "Identifier")) return false;
|
|
25227
|
+
const componentFunction = findEnclosingFunction(effectNode);
|
|
25228
|
+
if (!componentFunction) return false;
|
|
25229
|
+
if (!collectComponentPropNames(componentFunction).has(mirroredArgument.name)) return false;
|
|
25230
|
+
return isSetterWiredToJsxHandler(componentFunction, setterCall.callee.name);
|
|
25231
|
+
};
|
|
25232
|
+
//#endregion
|
|
25233
|
+
//#region src/plugin/rules/state-and-effects/utils/has-user-input-setter-writer.ts
|
|
25234
|
+
const HANDLER_BINDING_NAME_PATTERN = /^(on|handle)[A-Z_]/;
|
|
25235
|
+
const isEventHandlerPropertyKey = (property) => isNodeOfType(property, "Property") && !property.computed && isNodeOfType(property.key, "Identifier") && isEventHandlerName(property.key.name);
|
|
25236
|
+
const DEFERRED_CALLBACK_MEMBER_NAMES = new Set([
|
|
25237
|
+
"then",
|
|
25238
|
+
"catch",
|
|
25239
|
+
"finally",
|
|
25240
|
+
"subscribe"
|
|
25241
|
+
]);
|
|
25242
|
+
const DEFERRED_CALLBACK_CALLEE_NAMES = new Set([
|
|
25243
|
+
"setTimeout",
|
|
25244
|
+
"setInterval",
|
|
25245
|
+
"requestAnimationFrame",
|
|
25246
|
+
"addEventListener",
|
|
25247
|
+
"addListener"
|
|
25248
|
+
]);
|
|
25249
|
+
const isDeferredCallbackArgumentOf = (callExpr, child) => {
|
|
25250
|
+
if (!isNodeOfType(callExpr, "CallExpression")) return false;
|
|
25251
|
+
if (!(callExpr.arguments ?? []).includes(child)) return false;
|
|
25252
|
+
const callee = callExpr.callee;
|
|
25253
|
+
if (isNodeOfType(callee, "Identifier")) return DEFERRED_CALLBACK_CALLEE_NAMES.has(callee.name);
|
|
25254
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return DEFERRED_CALLBACK_MEMBER_NAMES.has(callee.property.name) || DEFERRED_CALLBACK_CALLEE_NAMES.has(callee.property.name);
|
|
25255
|
+
return false;
|
|
25256
|
+
};
|
|
25257
|
+
const isIndependentWriterIdentifier = (componentFunction, identifier, includeDeferredWriters) => {
|
|
25258
|
+
let outermostFunctionBelowComponent = null;
|
|
25259
|
+
let previous = identifier;
|
|
25260
|
+
let cursor = identifier.parent;
|
|
25261
|
+
while (cursor && cursor !== componentFunction) {
|
|
25262
|
+
if (isNodeOfType(cursor, "JSXAttribute")) {
|
|
25263
|
+
const attributeName = getJsxAttributeName(cursor.name);
|
|
25264
|
+
if (attributeName && isEventHandlerName(attributeName)) return true;
|
|
25265
|
+
}
|
|
25266
|
+
if (isEventHandlerPropertyKey(cursor)) return true;
|
|
25267
|
+
if (includeDeferredWriters && isDeferredCallbackArgumentOf(cursor, previous)) return true;
|
|
25268
|
+
if (isFunctionLike$1(cursor)) {
|
|
25269
|
+
outermostFunctionBelowComponent = cursor;
|
|
25270
|
+
if (includeDeferredWriters && cursor.async === true) return true;
|
|
25271
|
+
}
|
|
25272
|
+
previous = cursor;
|
|
25273
|
+
cursor = cursor.parent ?? null;
|
|
25274
|
+
}
|
|
25275
|
+
if (!outermostFunctionBelowComponent) return false;
|
|
25276
|
+
const bindingName = getFunctionBindingName$1(outermostFunctionBelowComponent);
|
|
25277
|
+
if (!bindingName) return false;
|
|
25278
|
+
if (HANDLER_BINDING_NAME_PATTERN.test(bindingName)) return true;
|
|
25279
|
+
return isSetterWiredToJsxHandler(componentFunction, bindingName);
|
|
25280
|
+
};
|
|
25281
|
+
const hasUserInputSetterWriter = (setterRef, effectNode, includeDeferredWriters = false) => {
|
|
25282
|
+
if (!setterRef.resolved) return false;
|
|
25283
|
+
const componentFunction = findEnclosingFunction(effectNode);
|
|
25284
|
+
if (!componentFunction) return false;
|
|
25285
|
+
for (const reference of setterRef.resolved.references) {
|
|
25286
|
+
if (reference.init) continue;
|
|
25287
|
+
const identifier = reference.identifier;
|
|
25288
|
+
if (isAstDescendant(identifier, effectNode)) continue;
|
|
25289
|
+
if (isIndependentWriterIdentifier(componentFunction, identifier, includeDeferredWriters)) return true;
|
|
25290
|
+
}
|
|
25291
|
+
return false;
|
|
25292
|
+
};
|
|
25293
|
+
//#endregion
|
|
25599
25294
|
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
25600
|
-
const
|
|
25601
|
-
if (!
|
|
25602
|
-
|
|
25603
|
-
const
|
|
25604
|
-
|
|
25605
|
-
|
|
25606
|
-
|
|
25607
|
-
|
|
25608
|
-
|
|
25295
|
+
const countSetterCallSites = (ref) => {
|
|
25296
|
+
if (!ref.resolved) return 0;
|
|
25297
|
+
let count = 0;
|
|
25298
|
+
for (const reference of ref.resolved.references) {
|
|
25299
|
+
const parent = reference.identifier.parent;
|
|
25300
|
+
if (parent && isNodeOfType(parent, "CallExpression")) count += 1;
|
|
25301
|
+
}
|
|
25302
|
+
return count;
|
|
25303
|
+
};
|
|
25304
|
+
const collectUpdaterParameterReads = (analysis, updaterFn) => {
|
|
25305
|
+
const reads = [];
|
|
25306
|
+
for (const ref of getDownstreamRefs(analysis, updaterFn.body)) if (ref.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updaterFn)) reads.push(ref.identifier);
|
|
25307
|
+
return reads;
|
|
25308
|
+
};
|
|
25309
|
+
const readsParameterOnlyViaObjectSpread = (parameterReads) => parameterReads.every((read) => {
|
|
25310
|
+
const spread = read.parent;
|
|
25311
|
+
if (!spread || !isNodeOfType(spread, "SpreadElement")) return false;
|
|
25312
|
+
const container = spread.parent;
|
|
25313
|
+
return Boolean(container && isNodeOfType(container, "ObjectExpression"));
|
|
25314
|
+
});
|
|
25315
|
+
const isAccumulatingFunctionalUpdater = (analysis, callExpr) => {
|
|
25316
|
+
if (!isNodeOfType(callExpr, "CallExpression")) return false;
|
|
25317
|
+
const firstArgument = callExpr.arguments?.[0];
|
|
25318
|
+
if (!firstArgument || !isFunctionLike$1(firstArgument)) return false;
|
|
25319
|
+
const parameterReads = collectUpdaterParameterReads(analysis, firstArgument);
|
|
25320
|
+
if (parameterReads.length === 0) return false;
|
|
25321
|
+
return !readsParameterOnlyViaObjectSpread(parameterReads);
|
|
25322
|
+
};
|
|
25323
|
+
const deriveStateNameFromSetterName = (setterName) => {
|
|
25324
|
+
if (setterName.length > 3 && setterName.startsWith("set")) return setterName[3].toLowerCase() + setterName.slice(4);
|
|
25325
|
+
return setterName;
|
|
25326
|
+
};
|
|
25327
|
+
const getStateNameForUseStateDecl = (useStateNode) => {
|
|
25328
|
+
if (!useStateNode || !isNodeOfType(useStateNode, "VariableDeclarator")) return null;
|
|
25329
|
+
if (!isNodeOfType(useStateNode.id, "ArrayPattern")) return null;
|
|
25330
|
+
const elements = useStateNode.id.elements ?? [];
|
|
25331
|
+
const stateSlot = elements[0];
|
|
25332
|
+
if (stateSlot && isNodeOfType(stateSlot, "Identifier")) return stateSlot.name;
|
|
25333
|
+
const setterSlot = elements[1];
|
|
25334
|
+
if (setterSlot && isNodeOfType(setterSlot, "Identifier")) return deriveStateNameFromSetterName(setterSlot.name);
|
|
25335
|
+
return null;
|
|
25336
|
+
};
|
|
25337
|
+
const isUseStateCallExpression = (callExpr) => {
|
|
25338
|
+
if (!isNodeOfType(callExpr, "CallExpression")) return false;
|
|
25339
|
+
const callee = callExpr.callee;
|
|
25340
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name === "useState";
|
|
25341
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useState";
|
|
25342
|
+
};
|
|
25343
|
+
const getSetterArgsUpstreamRefs = (analysis, ref) => {
|
|
25344
|
+
const result = [];
|
|
25345
|
+
for (const upRef of getUpstreamRefs(analysis, ref)) {
|
|
25346
|
+
const callExpr = getCallExpr(upRef);
|
|
25347
|
+
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
25348
|
+
if (isUseStateCallExpression(callExpr)) continue;
|
|
25349
|
+
for (const argument of callExpr.arguments ?? []) for (const argRef of getDownstreamRefs(analysis, argument)) for (const innerRef of getUpstreamRefs(analysis, argRef)) result.push(innerRef);
|
|
25350
|
+
}
|
|
25351
|
+
return result;
|
|
25352
|
+
};
|
|
25353
|
+
const refResolvesToDeclarator = (ref, declarator) => Boolean(declarator && ref.resolved?.defs.some((def) => def.node === declarator));
|
|
25354
|
+
const isUseContextLocal = (ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
25355
|
+
const node = def.node;
|
|
25356
|
+
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
25357
|
+
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
25358
|
+
const callee = node.init.callee;
|
|
25359
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name === "useContext";
|
|
25360
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useContext";
|
|
25361
|
+
}));
|
|
25362
|
+
const isPlainValueReadExpression = (node) => {
|
|
25363
|
+
if (isNodeOfType(node, "Identifier")) return true;
|
|
25364
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
25365
|
+
if (node.computed && !isNodeOfType(node.property, "Literal")) return false;
|
|
25366
|
+
return isPlainValueReadExpression(node.object);
|
|
25367
|
+
}
|
|
25368
|
+
return false;
|
|
25369
|
+
};
|
|
25370
|
+
const isWholeValueMirrorArgument = (callExpr) => {
|
|
25371
|
+
if (!isNodeOfType(callExpr, "CallExpression")) return false;
|
|
25372
|
+
const args = callExpr.arguments ?? [];
|
|
25373
|
+
if (args.length !== 1) return false;
|
|
25374
|
+
return isPlainValueReadExpression(args[0]);
|
|
25375
|
+
};
|
|
25376
|
+
const collectEarlyReturnGuardTests = (block, beforeStatement) => {
|
|
25377
|
+
const tests = [];
|
|
25378
|
+
if (!isNodeOfType(block, "BlockStatement")) return tests;
|
|
25379
|
+
for (const statement of block.body ?? []) {
|
|
25380
|
+
if (statement === beforeStatement) break;
|
|
25381
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
25382
|
+
const consequent = statement.consequent;
|
|
25383
|
+
if (isNodeOfType(consequent, "ReturnStatement") || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isNodeOfType(inner, "ReturnStatement"))) tests.push(statement.test);
|
|
25384
|
+
}
|
|
25385
|
+
return tests;
|
|
25386
|
+
};
|
|
25387
|
+
const collectGuardTestsOnPath = (callExpr, effectFn) => {
|
|
25388
|
+
const guardTests = [];
|
|
25389
|
+
let child = callExpr;
|
|
25390
|
+
let parent = callExpr.parent;
|
|
25391
|
+
while (parent && child !== effectFn) {
|
|
25392
|
+
if (isNodeOfType(parent, "IfStatement") && parent.test !== child) guardTests.push(parent.test);
|
|
25393
|
+
else if (isNodeOfType(parent, "ConditionalExpression") && parent.test !== child) guardTests.push(parent.test);
|
|
25394
|
+
else if (isNodeOfType(parent, "LogicalExpression") && parent.right === child) guardTests.push(parent.left);
|
|
25395
|
+
else if (isNodeOfType(parent, "BlockStatement")) guardTests.push(...collectEarlyReturnGuardTests(parent, child));
|
|
25396
|
+
child = parent;
|
|
25397
|
+
parent = parent.parent ?? null;
|
|
25398
|
+
}
|
|
25399
|
+
return guardTests;
|
|
25400
|
+
};
|
|
25401
|
+
const someGuardReadsWrittenState = (analysis, guardTests, writtenStateDecl) => guardTests.some((test) => getDownstreamRefs(analysis, test).some((testRef) => getUpstreamRefs(analysis, testRef).some((upRef) => isState(analysis, upRef) && refResolvesToDeclarator(upRef, writtenStateDecl))));
|
|
25402
|
+
const copiesOtherStateOrContextValue = (analysis, callExpr, writtenStateDecl) => {
|
|
25403
|
+
if (!isNodeOfType(callExpr, "CallExpression")) return false;
|
|
25404
|
+
const argument = callExpr.arguments?.[0];
|
|
25405
|
+
if (!argument) return false;
|
|
25406
|
+
for (const argRef of getDownstreamRefs(analysis, argument)) {
|
|
25407
|
+
if (isState(analysis, argRef) && !refResolvesToDeclarator(argRef, writtenStateDecl)) return true;
|
|
25408
|
+
for (const upRef of getUpstreamRefs(analysis, argRef)) if (isUseContextLocal(upRef)) return true;
|
|
25409
|
+
}
|
|
25410
|
+
return false;
|
|
25411
|
+
};
|
|
25412
|
+
const effectResetsArgumentSourceState = (analysis, effectFnRefs, effectFn, flaggedRef, argsUpstreamRefs) => {
|
|
25413
|
+
const sourceStateDecls = /* @__PURE__ */ new Set();
|
|
25414
|
+
for (const argRef of argsUpstreamRefs) {
|
|
25415
|
+
if (!isState(analysis, argRef)) continue;
|
|
25416
|
+
for (const def of argRef.resolved?.defs ?? []) sourceStateDecls.add(def.node);
|
|
25417
|
+
}
|
|
25418
|
+
if (sourceStateDecls.size === 0) return false;
|
|
25419
|
+
for (const otherRef of effectFnRefs) {
|
|
25420
|
+
if (otherRef === flaggedRef) continue;
|
|
25421
|
+
if (!isSyncStateSetterCall(analysis, otherRef, effectFn)) continue;
|
|
25422
|
+
const otherDecl = getUseStateDecl(analysis, otherRef);
|
|
25423
|
+
if (otherDecl && sourceStateDecls.has(otherDecl)) return true;
|
|
25424
|
+
}
|
|
25425
|
+
return false;
|
|
25426
|
+
};
|
|
25427
|
+
const hasReferenceOutsideEffect = (ref, effectNode) => {
|
|
25428
|
+
if (!ref.resolved) return false;
|
|
25429
|
+
const effectRange = effectNode.range;
|
|
25430
|
+
if (!effectRange) return false;
|
|
25431
|
+
for (const reference of ref.resolved.references) {
|
|
25432
|
+
if (reference.init) continue;
|
|
25433
|
+
const identifier = reference.identifier;
|
|
25434
|
+
if (!identifier.range) continue;
|
|
25435
|
+
if (!(effectRange[0] <= identifier.range[0] && identifier.range[1] <= effectRange[1])) return true;
|
|
25436
|
+
}
|
|
25437
|
+
return false;
|
|
25609
25438
|
};
|
|
25610
25439
|
const noDerivedState = defineRule({
|
|
25611
25440
|
id: "no-derived-state",
|
|
@@ -25617,18 +25446,124 @@ const noDerivedState = defineRule({
|
|
|
25617
25446
|
if (!isUseEffect(node)) return;
|
|
25618
25447
|
const analysis = getProgramAnalysis(node);
|
|
25619
25448
|
if (!analysis) return;
|
|
25620
|
-
|
|
25621
|
-
|
|
25622
|
-
|
|
25623
|
-
|
|
25624
|
-
|
|
25449
|
+
if (hasCleanup(analysis, node)) return;
|
|
25450
|
+
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
25451
|
+
const depsRefs = getEffectDepsRefs(analysis, node);
|
|
25452
|
+
if (!effectFnRefs || !depsRefs) return;
|
|
25453
|
+
const effectFn = getEffectFn(analysis, node);
|
|
25454
|
+
if (!effectFn) return;
|
|
25455
|
+
for (const ref of effectFnRefs) {
|
|
25456
|
+
if (!isSyncStateSetterCall(analysis, ref, effectFn)) continue;
|
|
25457
|
+
const callExpr = getCallExpr(ref);
|
|
25458
|
+
if (!callExpr) continue;
|
|
25459
|
+
if (readsPostMountValueThroughLocals(callExpr, effectFn)) continue;
|
|
25460
|
+
const useStateNode = getUseStateDecl(analysis, ref);
|
|
25461
|
+
const stateName = getStateNameForUseStateDecl(useStateNode) ?? "<state>";
|
|
25462
|
+
const argsUpstreamRefs = getSetterArgsUpstreamRefs(analysis, ref);
|
|
25463
|
+
const depsUpstreamRefs = depsRefs.flatMap((depRef) => getUpstreamRefs(analysis, depRef));
|
|
25464
|
+
if (isInitialOnlySetterCall(callExpr)) continue;
|
|
25465
|
+
if (isControlledPropMirror(node, callExpr)) continue;
|
|
25466
|
+
if (isAccumulatingFunctionalUpdater(analysis, callExpr)) continue;
|
|
25467
|
+
if (effectResetsArgumentSourceState(analysis, effectFnRefs, effectFn, ref, argsUpstreamRefs)) continue;
|
|
25468
|
+
const guardTestsOnPath = collectGuardTestsOnPath(callExpr, effectFn);
|
|
25469
|
+
const upstreamSetterRef = getUpstreamRefs(analysis, ref).find((upRef) => isStateSetter(analysis, upRef)) ?? null;
|
|
25470
|
+
if (guardTestsOnPath.length > 0 && upstreamSetterRef !== null && hasUserInputSetterWriter(upstreamSetterRef, node, true) && (!isWholeValueMirrorArgument(callExpr) || copiesOtherStateOrContextValue(analysis, callExpr, useStateNode)) && !someGuardReadsWrittenState(analysis, guardTestsOnPath, useStateNode)) continue;
|
|
25471
|
+
const isSomeArgsInternal = argsUpstreamRefs.some((argRef) => isState(analysis, argRef) || isProp(analysis, argRef));
|
|
25472
|
+
const isValueAlwaysInSync = argsUpstreamRefs.length > 0 && argsUpstreamRefs.every((argRef) => depsUpstreamRefs.some((depRef) => argRef.resolved === depRef.resolved)) && countSetterCallSites(ref) === 1 && !hasReferenceOutsideEffect(ref, node);
|
|
25473
|
+
if (isSomeArgsInternal) context.report({
|
|
25474
|
+
node: callExpr,
|
|
25625
25475
|
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
25626
25476
|
});
|
|
25477
|
+
else if (isValueAlwaysInSync) context.report({
|
|
25478
|
+
node: callExpr,
|
|
25479
|
+
message: `"${stateName}" is only set here from other values, so storing it costs an extra render.`
|
|
25480
|
+
});
|
|
25627
25481
|
}
|
|
25628
25482
|
} })
|
|
25629
25483
|
});
|
|
25484
|
+
const isInitialOnlySetterCall = (callExpr) => {
|
|
25485
|
+
if (!isNodeOfType(callExpr, "CallExpression")) return false;
|
|
25486
|
+
const args = callExpr.arguments ?? [];
|
|
25487
|
+
if (args.length !== 1) return false;
|
|
25488
|
+
const arg = args[0];
|
|
25489
|
+
if (!isNodeOfType(arg, "Identifier")) return false;
|
|
25490
|
+
return isInitialOnlyPropName(arg.name);
|
|
25491
|
+
};
|
|
25492
|
+
//#endregion
|
|
25493
|
+
//#region src/plugin/utils/is-no-op-statement.ts
|
|
25494
|
+
const isNoOpStatement = (statement) => {
|
|
25495
|
+
if (isNodeOfType(statement, "EmptyStatement")) return true;
|
|
25496
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
25497
|
+
const expression = stripParenExpression(statement.expression);
|
|
25498
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
25499
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
25500
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
|
|
25501
|
+
return false;
|
|
25502
|
+
};
|
|
25503
|
+
//#endregion
|
|
25504
|
+
//#region src/plugin/utils/get-callback-statements.ts
|
|
25505
|
+
const getCallbackStatements = (callback) => {
|
|
25506
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
|
|
25507
|
+
return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
|
|
25508
|
+
};
|
|
25630
25509
|
//#endregion
|
|
25631
25510
|
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
25511
|
+
const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE__ */ new Set()) => {
|
|
25512
|
+
if (!node || typeof node !== "object") return;
|
|
25513
|
+
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")) {
|
|
25514
|
+
const nestedBindingNames = new Set(localBindingNames);
|
|
25515
|
+
for (const parameter of node.params ?? []) if (isNodeOfType(parameter, "Identifier")) nestedBindingNames.add(parameter.name);
|
|
25516
|
+
collectValueIdentifierNames(node.body, into, nestedBindingNames);
|
|
25517
|
+
return;
|
|
25518
|
+
}
|
|
25519
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
25520
|
+
if (isNodeOfType(node.callee, "MemberExpression")) {
|
|
25521
|
+
const rootName = getRootIdentifierName(node.callee);
|
|
25522
|
+
if (!rootName || !BUILTIN_GLOBAL_NAMESPACE_NAMES.has(rootName)) collectValueIdentifierNames(node.callee.object, into, localBindingNames);
|
|
25523
|
+
}
|
|
25524
|
+
for (const argument of node.arguments ?? []) collectValueIdentifierNames(argument, into, localBindingNames);
|
|
25525
|
+
return;
|
|
25526
|
+
}
|
|
25527
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
25528
|
+
const rootName = getRootIdentifierName(node);
|
|
25529
|
+
if (!rootName || !BUILTIN_GLOBAL_NAMESPACE_NAMES.has(rootName)) collectValueIdentifierNames(node.object, into, localBindingNames);
|
|
25530
|
+
if (node.computed) collectValueIdentifierNames(node.property, into, localBindingNames);
|
|
25531
|
+
return;
|
|
25532
|
+
}
|
|
25533
|
+
if (isNodeOfType(node, "Identifier")) {
|
|
25534
|
+
if (!localBindingNames.has(node.name)) into.push(node.name);
|
|
25535
|
+
return;
|
|
25536
|
+
}
|
|
25537
|
+
const nodeRecord = node;
|
|
25538
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
25539
|
+
if (key === "parent" || key === "type") continue;
|
|
25540
|
+
const child = nodeRecord[key];
|
|
25541
|
+
if (Array.isArray(child)) {
|
|
25542
|
+
for (const item of child) if (item && typeof item === "object" && "type" in item) collectValueIdentifierNames(item, into, localBindingNames);
|
|
25543
|
+
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
25544
|
+
}
|
|
25545
|
+
};
|
|
25546
|
+
const flattenGuardedStatements = (statements) => {
|
|
25547
|
+
const flattened = [];
|
|
25548
|
+
for (const statement of statements) {
|
|
25549
|
+
if (isNoOpStatement(statement)) continue;
|
|
25550
|
+
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
25551
|
+
flattened.push(statement);
|
|
25552
|
+
continue;
|
|
25553
|
+
}
|
|
25554
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
25555
|
+
for (const branch of [statement.consequent, statement.alternate]) {
|
|
25556
|
+
if (!branch) continue;
|
|
25557
|
+
const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
|
|
25558
|
+
if (flattenedBranch === null) return null;
|
|
25559
|
+
flattened.push(...flattenedBranch);
|
|
25560
|
+
}
|
|
25561
|
+
continue;
|
|
25562
|
+
}
|
|
25563
|
+
return null;
|
|
25564
|
+
}
|
|
25565
|
+
return flattened;
|
|
25566
|
+
};
|
|
25632
25567
|
const noDerivedStateEffect = defineRule({
|
|
25633
25568
|
id: "no-derived-state-effect",
|
|
25634
25569
|
title: "Derived state stored in an effect",
|
|
@@ -25636,13 +25571,83 @@ const noDerivedStateEffect = defineRule({
|
|
|
25636
25571
|
tags: ["test-noise"],
|
|
25637
25572
|
recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
25638
25573
|
create: (context) => ({ CallExpression(node) {
|
|
25639
|
-
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
25640
|
-
const
|
|
25641
|
-
if (!
|
|
25642
|
-
|
|
25574
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1) || (node.arguments?.length ?? 0) < 2) return;
|
|
25575
|
+
const callback = getEffectCallback(node);
|
|
25576
|
+
if (!callback) return;
|
|
25577
|
+
const depsNode = node.arguments[1];
|
|
25578
|
+
if (!isNodeOfType(depsNode, "ArrayExpression") || !depsNode.elements?.length) return;
|
|
25579
|
+
const dependencyNames = /* @__PURE__ */ new Set();
|
|
25580
|
+
for (const element of depsNode.elements ?? []) {
|
|
25581
|
+
if (isNodeOfType(element, "Identifier")) dependencyNames.add(element.name);
|
|
25582
|
+
if (isNodeOfType(element, "MemberExpression")) {
|
|
25583
|
+
const rootName = getRootIdentifierName(element);
|
|
25584
|
+
if (rootName) dependencyNames.add(rootName);
|
|
25585
|
+
}
|
|
25586
|
+
}
|
|
25587
|
+
if (dependencyNames.size === 0) return;
|
|
25588
|
+
let allDepsAreInitialOnly = true;
|
|
25589
|
+
let sawAnyDep = false;
|
|
25590
|
+
for (const name of dependencyNames) {
|
|
25591
|
+
sawAnyDep = true;
|
|
25592
|
+
if (!isInitialOnlyPropName(name)) {
|
|
25593
|
+
allDepsAreInitialOnly = false;
|
|
25594
|
+
break;
|
|
25595
|
+
}
|
|
25596
|
+
}
|
|
25597
|
+
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
25598
|
+
const statements = flattenGuardedStatements(getCallbackStatements(callback));
|
|
25599
|
+
if (statements === null || statements.length === 0) return;
|
|
25600
|
+
if (!statements.every((statement) => {
|
|
25601
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
25602
|
+
const expression = statement.expression;
|
|
25603
|
+
if (!isSetterCall(expression)) return false;
|
|
25604
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
25605
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return false;
|
|
25606
|
+
return isUseStateSetterInScope(expression, expression.callee.name);
|
|
25607
|
+
})) return;
|
|
25608
|
+
if (statements.some((statement) => {
|
|
25609
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
25610
|
+
return isControlledPropMirror(node, statement.expression);
|
|
25611
|
+
})) return;
|
|
25612
|
+
let allArgumentsDeriveFromDeps = true;
|
|
25613
|
+
let hasAnyDependencyReference = false;
|
|
25614
|
+
let hasExpensiveDerivation = false;
|
|
25615
|
+
for (const statement of statements) {
|
|
25616
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
25617
|
+
if (!isNodeOfType(statement.expression, "CallExpression")) continue;
|
|
25618
|
+
const setStateArguments = statement.expression.arguments;
|
|
25619
|
+
if (!setStateArguments?.length) continue;
|
|
25620
|
+
const valueIdentifierNames = [];
|
|
25621
|
+
collectValueIdentifierNames(setStateArguments[0], valueIdentifierNames);
|
|
25622
|
+
walkAst(setStateArguments[0], (child) => {
|
|
25623
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
25624
|
+
if (isNodeOfType(child.callee, "MemberExpression")) {
|
|
25625
|
+
const rootName = getRootIdentifierName(child.callee);
|
|
25626
|
+
if (rootName && BUILTIN_GLOBAL_NAMESPACE_NAMES.has(rootName)) return;
|
|
25627
|
+
hasExpensiveDerivation = true;
|
|
25628
|
+
return;
|
|
25629
|
+
}
|
|
25630
|
+
if (isNodeOfType(child.callee, "Identifier")) {
|
|
25631
|
+
const calleeName = child.callee.name;
|
|
25632
|
+
if (!TRIVIAL_DERIVATION_CALLEE_NAMES.has(calleeName) && !isSetterIdentifier(calleeName)) hasExpensiveDerivation = true;
|
|
25633
|
+
}
|
|
25634
|
+
});
|
|
25635
|
+
const nonSetterIdentifiers = valueIdentifierNames.filter((name) => !isSetterIdentifier(name));
|
|
25636
|
+
if (nonSetterIdentifiers.some((name) => dependencyNames.has(name))) hasAnyDependencyReference = true;
|
|
25637
|
+
if (nonSetterIdentifiers.some((name) => !dependencyNames.has(name))) {
|
|
25638
|
+
allArgumentsDeriveFromDeps = false;
|
|
25639
|
+
break;
|
|
25640
|
+
}
|
|
25641
|
+
}
|
|
25642
|
+
if (!allArgumentsDeriveFromDeps) return;
|
|
25643
|
+
if (hasExpensiveDerivation) hasAnyDependencyReference = true;
|
|
25644
|
+
let message;
|
|
25645
|
+
if (!hasAnyDependencyReference) message = "Your users briefly see stale state on every prop change.";
|
|
25646
|
+
else if (hasExpensiveDerivation) message = "You pay an extra render for state derived from other values.";
|
|
25647
|
+
else message = "You pay an extra render for state you can derive from other values.";
|
|
25643
25648
|
context.report({
|
|
25644
25649
|
node,
|
|
25645
|
-
message
|
|
25650
|
+
message
|
|
25646
25651
|
});
|
|
25647
25652
|
} })
|
|
25648
25653
|
});
|
|
@@ -25780,12 +25785,6 @@ const createComponentPropStackTracker = (callbacks) => {
|
|
|
25780
25785
|
};
|
|
25781
25786
|
};
|
|
25782
25787
|
//#endregion
|
|
25783
|
-
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
25784
|
-
const isInitialOnlyPropName = (propName) => {
|
|
25785
|
-
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
25786
|
-
return /^initial[A-Z]/.test(propName) || /^default[A-Z]/.test(propName) || /^seed[A-Z]/.test(propName) || /^starting[A-Z]/.test(propName) || /^baseline[A-Z]/.test(propName) || /^preset[A-Z]/.test(propName);
|
|
25787
|
-
};
|
|
25788
|
-
//#endregion
|
|
25789
25788
|
//#region src/plugin/rules/state-and-effects/no-derived-use-state.ts
|
|
25790
25789
|
const isInitialOnlySeedName = (propName) => isInitialOnlyPropName(propName) || propName === "initial" || propName === "autoFocus" || propName === "autoPlay" || propName === "startOpen" || /^initially[A-Z]/.test(propName) || /Initial([A-Z]|$)/.test(propName);
|
|
25791
25790
|
const SNAPSHOT_STATE_NAME_PATTERN = /^(initial|previous|prev|preserved|saved|original|cached|snapshot|prior|debounced|deferred)([A-Z_]|$)/;
|
|
@@ -27219,23 +27218,6 @@ const noEffectChain = defineRule({
|
|
|
27219
27218
|
}
|
|
27220
27219
|
});
|
|
27221
27220
|
//#endregion
|
|
27222
|
-
//#region src/plugin/utils/is-no-op-statement.ts
|
|
27223
|
-
const isNoOpStatement = (statement) => {
|
|
27224
|
-
if (isNodeOfType(statement, "EmptyStatement")) return true;
|
|
27225
|
-
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
27226
|
-
const expression = stripParenExpression(statement.expression);
|
|
27227
|
-
if (isNodeOfType(expression, "Literal")) return true;
|
|
27228
|
-
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
27229
|
-
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
|
|
27230
|
-
return false;
|
|
27231
|
-
};
|
|
27232
|
-
//#endregion
|
|
27233
|
-
//#region src/plugin/utils/get-callback-statements.ts
|
|
27234
|
-
const getCallbackStatements = (callback) => {
|
|
27235
|
-
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
|
|
27236
|
-
return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
|
|
27237
|
-
};
|
|
27238
|
-
//#endregion
|
|
27239
27221
|
//#region src/plugin/rules/state-and-effects/utils/find-triggered-side-effect-callee-name.ts
|
|
27240
27222
|
const findTriggeredSideEffectCalleeName = (consequentNode) => {
|
|
27241
27223
|
let foundCalleeName = null;
|
|
@@ -27590,186 +27572,397 @@ const noEval = defineRule({
|
|
|
27590
27572
|
})
|
|
27591
27573
|
});
|
|
27592
27574
|
//#endregion
|
|
27593
|
-
//#region src/plugin/rules/state-and-effects/
|
|
27594
|
-
const
|
|
27595
|
-
const
|
|
27596
|
-
const
|
|
27597
|
-
|
|
27598
|
-
|
|
27599
|
-
|
|
27575
|
+
//#region src/plugin/rules/state-and-effects/no-event-handler.ts
|
|
27576
|
+
const SETTER_NAME_PATTERN = /^set[A-Z]/;
|
|
27577
|
+
const DISPATCH_NAME_PATTERN = /^dispatch$|Dispatch$/;
|
|
27578
|
+
const HOOK_NAME_PATTERN$2 = /^use[A-Z0-9]/;
|
|
27579
|
+
const isSetterLikeName = (name) => SETTER_NAME_PATTERN.test(name) || DISPATCH_NAME_PATTERN.test(name);
|
|
27580
|
+
const isSetterCallExpressionStatement = (node) => {
|
|
27581
|
+
if (!isNodeOfType(node, "ExpressionStatement")) return false;
|
|
27582
|
+
let expression = node.expression;
|
|
27583
|
+
if (expression && isNodeOfType(expression, "ChainExpression")) expression = expression.expression;
|
|
27584
|
+
if (!expression) return false;
|
|
27585
|
+
if (isNodeOfType(expression, "CallExpression")) {
|
|
27586
|
+
const callee = expression.callee;
|
|
27587
|
+
if (isNodeOfType(callee, "Identifier")) return isSetterLikeName(callee.name);
|
|
27588
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && isSetterLikeName(callee.property.name)) return true;
|
|
27589
|
+
return false;
|
|
27600
27590
|
}
|
|
27601
|
-
|
|
27602
|
-
|
|
27603
|
-
|
|
27604
|
-
if (isNodeOfType(functionNode, "FunctionDeclaration")) return functionNode;
|
|
27605
|
-
let current = functionNode;
|
|
27606
|
-
while (current) {
|
|
27607
|
-
if (isNodeOfType(current, "VariableDeclarator")) return current;
|
|
27608
|
-
if (isNodeOfType(current, "Program") || isFunctionLike$1(current) && current !== functionNode) return null;
|
|
27609
|
-
current = current.parent;
|
|
27591
|
+
if (isNodeOfType(expression, "AssignmentExpression")) {
|
|
27592
|
+
const left = expression.left;
|
|
27593
|
+
if (isNodeOfType(left, "MemberExpression") && !left.computed && isNodeOfType(left.property, "Identifier") && left.property.name === "current" && isNodeOfType(left.object, "Identifier")) return true;
|
|
27610
27594
|
}
|
|
27611
|
-
return
|
|
27595
|
+
return false;
|
|
27612
27596
|
};
|
|
27613
|
-
const
|
|
27614
|
-
|
|
27615
|
-
|
|
27616
|
-
|
|
27617
|
-
|
|
27597
|
+
const REF_GUARD_SCAN_BUDGET = 50;
|
|
27598
|
+
const containsRefGuard = (testNode) => {
|
|
27599
|
+
const stack = [testNode];
|
|
27600
|
+
let budget = REF_GUARD_SCAN_BUDGET;
|
|
27601
|
+
while (stack.length > 0 && budget-- > 0) {
|
|
27602
|
+
const node = stack.pop();
|
|
27603
|
+
if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.property, "Identifier") && node.property.name === "current" && isNodeOfType(node.object, "Identifier")) {
|
|
27604
|
+
const name = node.object.name;
|
|
27605
|
+
if (name === "ref" || name.endsWith("Ref") || name.endsWith("ref")) return true;
|
|
27606
|
+
}
|
|
27607
|
+
if (isNodeOfType(node, "LogicalExpression") || isNodeOfType(node, "BinaryExpression")) stack.push(node.left, node.right);
|
|
27608
|
+
else if (isNodeOfType(node, "UnaryExpression")) stack.push(node.argument);
|
|
27609
|
+
else if (isNodeOfType(node, "ConditionalExpression")) stack.push(node.test, node.consequent, node.alternate);
|
|
27610
|
+
else if (isNodeOfType(node, "MemberExpression")) stack.push(node.object);
|
|
27611
|
+
else if (isNodeOfType(node, "ChainExpression")) stack.push(node.expression);
|
|
27618
27612
|
}
|
|
27619
27613
|
return false;
|
|
27620
27614
|
};
|
|
27621
|
-
const
|
|
27622
|
-
|
|
27623
|
-
|
|
27624
|
-
|
|
27625
|
-
|
|
27626
|
-
|
|
27627
|
-
|
|
27628
|
-
|
|
27629
|
-
|
|
27630
|
-
|
|
27615
|
+
const COMPARISON_OPERATORS$1 = new Set([
|
|
27616
|
+
"==",
|
|
27617
|
+
"===",
|
|
27618
|
+
"!=",
|
|
27619
|
+
"!==",
|
|
27620
|
+
"<",
|
|
27621
|
+
"<=",
|
|
27622
|
+
">",
|
|
27623
|
+
">="
|
|
27624
|
+
]);
|
|
27625
|
+
const isLiteralLikeNode = (node) => isNodeOfType(node, "Literal") || isNodeOfType(node, "TemplateLiteral") || isNodeOfType(node, "UnaryExpression") && isNodeOfType(node.argument, "Literal");
|
|
27626
|
+
const isPreviousValueComparisonSide = (analysis, side) => {
|
|
27627
|
+
let current = side;
|
|
27628
|
+
for (;;) if (isNodeOfType(current, "ChainExpression")) current = current.expression;
|
|
27629
|
+
else if (isNodeOfType(current, "TSNonNullExpression")) current = current.expression;
|
|
27630
|
+
else if (isNodeOfType(current, "MemberExpression")) {
|
|
27631
|
+
if (!current.computed && isNodeOfType(current.property, "Identifier") && current.property.name === "current" && isNodeOfType(current.object, "Identifier")) {
|
|
27632
|
+
const objectRef = getRef(analysis, current.object);
|
|
27633
|
+
if (objectRef && isUseRefBinding(objectRef)) return true;
|
|
27631
27634
|
}
|
|
27632
|
-
|
|
27633
|
-
}
|
|
27635
|
+
current = current.object;
|
|
27636
|
+
} else break;
|
|
27637
|
+
if (!isNodeOfType(current, "Identifier")) return false;
|
|
27638
|
+
const baseRef = getRef(analysis, current);
|
|
27639
|
+
return Boolean(baseRef && isOpaqueHookResultBinding(baseRef));
|
|
27640
|
+
};
|
|
27641
|
+
const containsPreviousValueComparison = (analysis, testNode) => {
|
|
27642
|
+
const stack = [testNode];
|
|
27643
|
+
let budget = REF_GUARD_SCAN_BUDGET;
|
|
27644
|
+
while (stack.length > 0 && budget-- > 0) {
|
|
27645
|
+
const node = stack.pop();
|
|
27646
|
+
if (isNodeOfType(node, "BinaryExpression")) {
|
|
27647
|
+
if (COMPARISON_OPERATORS$1.has(node.operator)) {
|
|
27648
|
+
const left = node.left;
|
|
27649
|
+
const right = node.right;
|
|
27650
|
+
if (isPreviousValueComparisonSide(analysis, left) && !isLiteralLikeNode(right)) return true;
|
|
27651
|
+
if (isPreviousValueComparisonSide(analysis, right) && !isLiteralLikeNode(left)) return true;
|
|
27652
|
+
}
|
|
27653
|
+
stack.push(node.left, node.right);
|
|
27654
|
+
} else if (isNodeOfType(node, "LogicalExpression")) stack.push(node.left, node.right);
|
|
27655
|
+
else if (isNodeOfType(node, "UnaryExpression")) stack.push(node.argument);
|
|
27656
|
+
else if (isNodeOfType(node, "ConditionalExpression")) stack.push(node.test, node.consequent, node.alternate);
|
|
27657
|
+
else if (isNodeOfType(node, "ChainExpression")) stack.push(node.expression);
|
|
27658
|
+
}
|
|
27659
|
+
return false;
|
|
27634
27660
|
};
|
|
27635
|
-
const
|
|
27636
|
-
|
|
27637
|
-
|
|
27638
|
-
|
|
27639
|
-
|
|
27640
|
-
|
|
27641
|
-
|
|
27642
|
-
|
|
27643
|
-
|
|
27644
|
-
|
|
27645
|
-
|
|
27646
|
-
|
|
27647
|
-
|
|
27648
|
-
return hasCall;
|
|
27649
|
-
};
|
|
27650
|
-
const isFunctionUsedOutsideHandlers = (analysis, functionNode, componentFunction) => {
|
|
27651
|
-
const definitionNode = findFunctionDefinitionNode(functionNode);
|
|
27652
|
-
if (!definitionNode) return true;
|
|
27653
|
-
const variable = findVariableForDefinition(analysis, definitionNode);
|
|
27654
|
-
if (!variable) return true;
|
|
27655
|
-
return variable.references.some((reference) => {
|
|
27656
|
-
if (reference.init) return false;
|
|
27657
|
-
const identifier = reference.identifier;
|
|
27658
|
-
if (isInsideInlineEventHandler(identifier, componentFunction)) return false;
|
|
27659
|
-
const parent = identifier.parent;
|
|
27660
|
-
if (parent && isNodeOfType(parent, "CallExpression") && parent.callee === identifier) {
|
|
27661
|
-
if (findEnclosingFunction(parent) === functionNode) return false;
|
|
27662
|
-
if (isInsideProvenEventHandler(analysis, parent, componentFunction, false)) return false;
|
|
27663
|
-
}
|
|
27664
|
-
return true;
|
|
27665
|
-
});
|
|
27661
|
+
const TRANSPARENT_HOOK_NAMES = new Set([
|
|
27662
|
+
"useMemo",
|
|
27663
|
+
"useCallback",
|
|
27664
|
+
"useState",
|
|
27665
|
+
"useRef",
|
|
27666
|
+
"useDeferredValue"
|
|
27667
|
+
]);
|
|
27668
|
+
const getHookCalleeName = (node) => {
|
|
27669
|
+
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
27670
|
+
const callee = node.callee;
|
|
27671
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
27672
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
27673
|
+
return null;
|
|
27666
27674
|
};
|
|
27667
|
-
const
|
|
27668
|
-
|
|
27669
|
-
|
|
27670
|
-
|
|
27671
|
-
|
|
27672
|
-
|
|
27673
|
-
|
|
27675
|
+
const isOpaqueCustomHookInit = (init) => {
|
|
27676
|
+
const calleeName = getHookCalleeName(init);
|
|
27677
|
+
return calleeName !== null && HOOK_NAME_PATTERN$2.test(calleeName) && !TRANSPARENT_HOOK_NAMES.has(calleeName);
|
|
27678
|
+
};
|
|
27679
|
+
const getDeclaratorInit = (ref) => {
|
|
27680
|
+
for (const def of ref.resolved?.defs ?? []) {
|
|
27681
|
+
const node = def.node;
|
|
27682
|
+
if (isNodeOfType(node, "VariableDeclarator") && node.init) return node.init;
|
|
27683
|
+
}
|
|
27684
|
+
return null;
|
|
27685
|
+
};
|
|
27686
|
+
const isOpaqueHookResultBinding = (ref) => {
|
|
27687
|
+
const init = getDeclaratorInit(ref);
|
|
27688
|
+
return Boolean(init && isOpaqueCustomHookInit(init));
|
|
27689
|
+
};
|
|
27690
|
+
const isUseRefBinding = (ref) => {
|
|
27691
|
+
const init = getDeclaratorInit(ref);
|
|
27692
|
+
return Boolean(init && getHookCalleeName(init) === "useRef");
|
|
27693
|
+
};
|
|
27694
|
+
const REACT_BUILTIN_HOOK_NAMES = new Set([
|
|
27695
|
+
"useState",
|
|
27696
|
+
"useReducer",
|
|
27697
|
+
"useContext",
|
|
27698
|
+
"useMemo",
|
|
27699
|
+
"useCallback",
|
|
27700
|
+
"useRef",
|
|
27701
|
+
"useDeferredValue",
|
|
27702
|
+
"useTransition",
|
|
27703
|
+
"useSyncExternalStore",
|
|
27704
|
+
"useImperativeHandle",
|
|
27705
|
+
"useLayoutEffect",
|
|
27706
|
+
"useEffect",
|
|
27707
|
+
"useInsertionEffect",
|
|
27708
|
+
"useId",
|
|
27709
|
+
"useOptimistic",
|
|
27710
|
+
"useActionState",
|
|
27711
|
+
"useDebugValue"
|
|
27712
|
+
]);
|
|
27713
|
+
const isCustomHookReturnedBinding = (ref) => {
|
|
27714
|
+
const init = getDeclaratorInit(ref);
|
|
27715
|
+
if (!init) return false;
|
|
27716
|
+
const calleeName = getHookCalleeName(init);
|
|
27717
|
+
return calleeName !== null && HOOK_NAME_PATTERN$2.test(calleeName) && !REACT_BUILTIN_HOOK_NAMES.has(calleeName);
|
|
27718
|
+
};
|
|
27719
|
+
const collectGuardUpstreamRefs = (analysis, ref, refs, visited) => {
|
|
27720
|
+
if (visited.has(ref)) return;
|
|
27721
|
+
visited.add(ref);
|
|
27722
|
+
refs.push(ref);
|
|
27723
|
+
for (const def of ref.resolved?.defs ?? []) {
|
|
27724
|
+
if (def.type === "ImportBinding" || def.type === "Parameter") continue;
|
|
27725
|
+
const defNode = def.node;
|
|
27726
|
+
const next = defNode.init ?? defNode.body;
|
|
27727
|
+
if (!next) continue;
|
|
27728
|
+
if (isOpaqueCustomHookInit(next)) continue;
|
|
27729
|
+
for (const innerRef of getDownstreamRefs(analysis, next)) {
|
|
27730
|
+
if (isInsideCallbackArgumentOf(innerRef.identifier, next)) continue;
|
|
27731
|
+
collectGuardUpstreamRefs(analysis, innerRef, refs, visited);
|
|
27674
27732
|
}
|
|
27675
|
-
current = current.parent;
|
|
27676
27733
|
}
|
|
27734
|
+
};
|
|
27735
|
+
const getGuardUpstreamRefs = (analysis, ref) => {
|
|
27736
|
+
const refs = [];
|
|
27737
|
+
collectGuardUpstreamRefs(analysis, ref, refs, /* @__PURE__ */ new Set());
|
|
27738
|
+
return refs;
|
|
27739
|
+
};
|
|
27740
|
+
const isSideEffectFreeExit = (statement) => {
|
|
27741
|
+
if (isNodeOfType(statement, "ContinueStatement")) return true;
|
|
27742
|
+
if (isNodeOfType(statement, "BreakStatement")) return true;
|
|
27743
|
+
if (!isNodeOfType(statement, "ReturnStatement")) return false;
|
|
27744
|
+
const argument = statement.argument;
|
|
27745
|
+
if (!argument) return true;
|
|
27746
|
+
if (isNodeOfType(argument, "Literal")) return true;
|
|
27747
|
+
if (isNodeOfType(argument, "Identifier")) return true;
|
|
27748
|
+
if (isNodeOfType(argument, "UnaryExpression") && argument.operator === "void") return true;
|
|
27677
27749
|
return false;
|
|
27678
27750
|
};
|
|
27679
|
-
const
|
|
27680
|
-
|
|
27681
|
-
|
|
27682
|
-
|
|
27683
|
-
|
|
27684
|
-
|
|
27685
|
-
|
|
27686
|
-
|
|
27751
|
+
const getConsequentStatements = (consequent) => {
|
|
27752
|
+
if (isNodeOfType(consequent, "BlockStatement")) return consequent.body ?? [];
|
|
27753
|
+
return [consequent];
|
|
27754
|
+
};
|
|
27755
|
+
const isControlledPropMirrorStatement = (analysis, statement, testedPropBindings) => {
|
|
27756
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
27757
|
+
let expression = statement.expression;
|
|
27758
|
+
if (expression && isNodeOfType(expression, "ChainExpression")) expression = expression.expression;
|
|
27759
|
+
if (!expression || !isNodeOfType(expression, "CallExpression")) return false;
|
|
27760
|
+
const callee = expression.callee;
|
|
27761
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
27762
|
+
const calleeRef = getRef(analysis, callee);
|
|
27763
|
+
if (!calleeRef || !isStateSetter(analysis, calleeRef)) return false;
|
|
27764
|
+
const callArguments = expression.arguments ?? [];
|
|
27765
|
+
if (callArguments.length !== 1) return false;
|
|
27766
|
+
const argument = callArguments[0];
|
|
27767
|
+
if (!isNodeOfType(argument, "Identifier")) return false;
|
|
27768
|
+
const argumentRef = getRef(analysis, argument);
|
|
27769
|
+
if (!argumentRef?.resolved || !isProp(analysis, argumentRef)) return false;
|
|
27770
|
+
return testedPropBindings.has(argumentRef.resolved);
|
|
27771
|
+
};
|
|
27772
|
+
const isControlledPropMirrorConsequent = (analysis, ifNode) => {
|
|
27773
|
+
const statements = getConsequentStatements(ifNode.consequent);
|
|
27774
|
+
if (statements.length === 0) return false;
|
|
27775
|
+
const testRefs = getDownstreamRefs(analysis, ifNode.test);
|
|
27776
|
+
if (testRefs.some((ref) => isState(analysis, ref))) return false;
|
|
27777
|
+
const testedPropBindings = new Set(testRefs.filter((ref) => isProp(analysis, ref)).map((ref) => ref.resolved).filter(Boolean));
|
|
27778
|
+
if (testedPropBindings.size === 0) return false;
|
|
27779
|
+
return statements.every((statement) => isControlledPropMirrorStatement(analysis, statement, testedPropBindings));
|
|
27780
|
+
};
|
|
27781
|
+
const isPureEarlyExitConsequent = (consequent) => {
|
|
27782
|
+
if (isNodeOfType(consequent, "ReturnStatement") || isNodeOfType(consequent, "ContinueStatement") || isNodeOfType(consequent, "BreakStatement")) return isSideEffectFreeExit(consequent);
|
|
27783
|
+
if (isNodeOfType(consequent, "BlockStatement")) {
|
|
27784
|
+
const body = consequent.body ?? [];
|
|
27785
|
+
if (body.length === 0) return false;
|
|
27786
|
+
const last = body[body.length - 1];
|
|
27787
|
+
if (!isSideEffectFreeExit(last)) return false;
|
|
27788
|
+
for (let i = 0; i < body.length - 1; i++) if (!isSetterCallExpressionStatement(body[i])) return false;
|
|
27789
|
+
return true;
|
|
27687
27790
|
}
|
|
27688
27791
|
return false;
|
|
27689
27792
|
};
|
|
27690
|
-
const
|
|
27691
|
-
|
|
27692
|
-
if (
|
|
27693
|
-
if (
|
|
27694
|
-
|
|
27695
|
-
|
|
27696
|
-
|
|
27697
|
-
|
|
27698
|
-
|
|
27699
|
-
|
|
27793
|
+
const isEffectLocalBinding = (ref, effectFn) => Boolean(ref.resolved && ref.resolved.defs.length > 0 && ref.resolved.defs.every((def) => isAstDescendant(def.node, effectFn)));
|
|
27794
|
+
const isEffectLocalWriteStatement = (analysis, statement, effectFn) => {
|
|
27795
|
+
if (isSideEffectFreeExit(statement)) return true;
|
|
27796
|
+
if (isNodeOfType(statement, "VariableDeclaration")) return true;
|
|
27797
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
27798
|
+
const expression = statement.expression;
|
|
27799
|
+
if (!isNodeOfType(expression, "AssignmentExpression")) return false;
|
|
27800
|
+
const left = expression.left;
|
|
27801
|
+
if (!isNodeOfType(left, "Identifier")) return false;
|
|
27802
|
+
const leftRef = getRef(analysis, left);
|
|
27803
|
+
return Boolean(leftRef && isEffectLocalBinding(leftRef, effectFn));
|
|
27804
|
+
};
|
|
27805
|
+
const isLocalComputationConsequent = (analysis, ifNode, effectFn) => {
|
|
27806
|
+
const statements = getConsequentStatements(ifNode.consequent);
|
|
27807
|
+
if (statements.length === 0) return false;
|
|
27808
|
+
return statements.every((statement) => isEffectLocalWriteStatement(analysis, statement, effectFn));
|
|
27809
|
+
};
|
|
27810
|
+
const isEffectLocalOnlyTest = (analysis, ifNode, effectFn) => {
|
|
27811
|
+
const testRefs = getDownstreamRefs(analysis, ifNode.test);
|
|
27812
|
+
if (testRefs.length === 0) return false;
|
|
27813
|
+
return testRefs.every((ref) => isEffectLocalBinding(ref, effectFn));
|
|
27814
|
+
};
|
|
27815
|
+
const isDepsListedCallbackInvocationConsequent = (analysis, ifNode, effectNode) => {
|
|
27816
|
+
const statements = getConsequentStatements(ifNode.consequent);
|
|
27817
|
+
if (statements.length !== 1) return false;
|
|
27818
|
+
const statement = statements[0];
|
|
27819
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
27820
|
+
let expression = statement.expression;
|
|
27821
|
+
if (isNodeOfType(expression, "ChainExpression")) expression = expression.expression;
|
|
27822
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
27823
|
+
if ((expression.arguments ?? []).length !== 0) return false;
|
|
27824
|
+
const callee = expression.callee;
|
|
27825
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
27826
|
+
const calleeRef = getRef(analysis, callee);
|
|
27827
|
+
if (!calleeRef) return false;
|
|
27828
|
+
const init = getDeclaratorInit(calleeRef);
|
|
27829
|
+
if (!init || getHookCalleeName(init) !== "useCallback") return false;
|
|
27830
|
+
const deps = effectNode.arguments?.[1];
|
|
27831
|
+
if (!deps || !isNodeOfType(deps, "ArrayExpression")) return false;
|
|
27832
|
+
return (deps.elements ?? []).some((element) => isNodeOfType(element, "Identifier") && element.name === callee.name);
|
|
27833
|
+
};
|
|
27834
|
+
const IMPERATIVE_ROOT_GLOBAL_NAMES = new Set([
|
|
27835
|
+
"window",
|
|
27836
|
+
"document",
|
|
27837
|
+
"globalThis"
|
|
27838
|
+
]);
|
|
27839
|
+
const PARENT_CALLBACK_NAME_PATTERN = /^on[A-Z]/;
|
|
27840
|
+
const DATA_ITERATION_METHOD_NAMES = new Set([
|
|
27841
|
+
"forEach",
|
|
27842
|
+
"map",
|
|
27843
|
+
"filter",
|
|
27844
|
+
"reduce",
|
|
27845
|
+
"flatMap",
|
|
27846
|
+
"find",
|
|
27847
|
+
"some",
|
|
27848
|
+
"every",
|
|
27849
|
+
"push",
|
|
27850
|
+
"pop",
|
|
27851
|
+
"shift",
|
|
27852
|
+
"unshift",
|
|
27853
|
+
"splice",
|
|
27854
|
+
"sort",
|
|
27855
|
+
"reverse",
|
|
27856
|
+
"concat"
|
|
27857
|
+
]);
|
|
27858
|
+
const getImperativeTargetChain = (target) => {
|
|
27859
|
+
let current = target;
|
|
27860
|
+
let passesThroughRefCurrent = false;
|
|
27861
|
+
let memberDepth = 0;
|
|
27862
|
+
for (;;) if (isNodeOfType(current, "ChainExpression")) current = current.expression;
|
|
27863
|
+
else if (isNodeOfType(current, "TSNonNullExpression")) current = current.expression;
|
|
27864
|
+
else if (isNodeOfType(current, "MemberExpression")) {
|
|
27865
|
+
if (!current.computed && isNodeOfType(current.property, "Identifier") && current.property.name === "current") passesThroughRefCurrent = true;
|
|
27866
|
+
memberDepth += 1;
|
|
27867
|
+
current = current.object;
|
|
27868
|
+
} else if (isNodeOfType(current, "CallExpression")) current = current.callee;
|
|
27869
|
+
else break;
|
|
27870
|
+
return {
|
|
27871
|
+
baseIdentifier: isNodeOfType(current, "Identifier") ? current : null,
|
|
27872
|
+
passesThroughRefCurrent,
|
|
27873
|
+
memberDepth
|
|
27874
|
+
};
|
|
27875
|
+
};
|
|
27876
|
+
const getInvokedMemberName = (callee) => {
|
|
27877
|
+
let current = callee;
|
|
27878
|
+
if (isNodeOfType(current, "ChainExpression")) current = current.expression;
|
|
27879
|
+
if (isNodeOfType(current, "MemberExpression") && isNodeOfType(current.property, "Identifier")) return current.property.name;
|
|
27700
27880
|
return null;
|
|
27701
27881
|
};
|
|
27702
|
-
const
|
|
27703
|
-
const
|
|
27704
|
-
if (!
|
|
27705
|
-
const
|
|
27706
|
-
|
|
27707
|
-
|
|
27708
|
-
|
|
27709
|
-
|
|
27710
|
-
|
|
27711
|
-
|
|
27712
|
-
|
|
27713
|
-
|
|
27714
|
-
|
|
27882
|
+
const isCustomHookInstanceBinding = (ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
27883
|
+
const node = def.node;
|
|
27884
|
+
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
27885
|
+
const calleeName = getHookCalleeName(node.init);
|
|
27886
|
+
return calleeName !== null && HOOK_NAME_PATTERN$2.test(calleeName);
|
|
27887
|
+
}));
|
|
27888
|
+
const isImperativeSyncStatement = (analysis, statement, testedBindings) => {
|
|
27889
|
+
if (isSideEffectFreeExit(statement)) return true;
|
|
27890
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
27891
|
+
if (statement.alternate) return false;
|
|
27892
|
+
const innerTestedBindings = new Set(testedBindings);
|
|
27893
|
+
for (const testRef of getDownstreamRefs(analysis, statement.test)) if (testRef.resolved) innerTestedBindings.add(testRef.resolved);
|
|
27894
|
+
const innerStatements = getConsequentStatements(statement.consequent);
|
|
27895
|
+
if (innerStatements.length === 0) return false;
|
|
27896
|
+
return innerStatements.every((innerStatement) => isImperativeSyncStatement(analysis, innerStatement, innerTestedBindings));
|
|
27715
27897
|
}
|
|
27716
|
-
return
|
|
27898
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
27899
|
+
let expression = statement.expression;
|
|
27900
|
+
if (isNodeOfType(expression, "ChainExpression")) expression = expression.expression;
|
|
27901
|
+
let target;
|
|
27902
|
+
let invokedName = null;
|
|
27903
|
+
if (isNodeOfType(expression, "CallExpression")) {
|
|
27904
|
+
target = expression.callee;
|
|
27905
|
+
invokedName = getInvokedMemberName(target);
|
|
27906
|
+
} else if (isNodeOfType(expression, "AssignmentExpression")) target = expression.left;
|
|
27907
|
+
else return false;
|
|
27908
|
+
const chain = getImperativeTargetChain(target);
|
|
27909
|
+
if (!chain.baseIdentifier) return false;
|
|
27910
|
+
if (chain.memberDepth === 0) {
|
|
27911
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
27912
|
+
const calleeRef = getRef(analysis, chain.baseIdentifier);
|
|
27913
|
+
if (!calleeRef?.resolved) return false;
|
|
27914
|
+
if (isState(analysis, calleeRef) || isStateSetter(analysis, calleeRef)) return false;
|
|
27915
|
+
if (PARENT_CALLBACK_NAME_PATTERN.test(chain.baseIdentifier.name)) return false;
|
|
27916
|
+
return isCustomHookReturnedBinding(calleeRef);
|
|
27917
|
+
}
|
|
27918
|
+
const baseName = chain.baseIdentifier.name;
|
|
27919
|
+
if (IMPERATIVE_ROOT_GLOBAL_NAMES.has(baseName)) return true;
|
|
27920
|
+
if (chain.passesThroughRefCurrent) return true;
|
|
27921
|
+
if (baseName === "ref" || baseName.endsWith("Ref") || baseName.endsWith("ref")) return true;
|
|
27922
|
+
const baseRef = getRef(analysis, chain.baseIdentifier);
|
|
27923
|
+
if (!baseRef?.resolved) return false;
|
|
27924
|
+
if (isState(analysis, baseRef) || isStateSetter(analysis, baseRef)) return false;
|
|
27925
|
+
if (isCustomHookInstanceBinding(baseRef)) return true;
|
|
27926
|
+
if (invokedName && PARENT_CALLBACK_NAME_PATTERN.test(invokedName)) return false;
|
|
27927
|
+
if (invokedName && DATA_ITERATION_METHOD_NAMES.has(invokedName)) return false;
|
|
27928
|
+
if (!testedBindings.has(baseRef.resolved)) return false;
|
|
27929
|
+
if (!isCustomHookParameter(baseRef) && isWholePropsObjectReference(analysis, baseRef)) return false;
|
|
27930
|
+
return true;
|
|
27717
27931
|
};
|
|
27718
|
-
|
|
27719
|
-
|
|
27720
|
-
|
|
27721
|
-
|
|
27722
|
-
const
|
|
27723
|
-
for (const
|
|
27724
|
-
|
|
27725
|
-
const binding = reference.resolved ?? reference.identifier;
|
|
27726
|
-
if (seenBindings.has(binding)) continue;
|
|
27727
|
-
seenBindings.add(binding);
|
|
27728
|
-
stateReferences.push(reference);
|
|
27729
|
-
}
|
|
27730
|
-
return stateReferences;
|
|
27731
|
-
};
|
|
27732
|
-
const consequentHasTransferableWork = (analysis, consequent) => {
|
|
27733
|
-
if (findTriggeredSideEffectCalleeName(consequent)) return true;
|
|
27734
|
-
if (getDownstreamRefs(analysis, consequent).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return true;
|
|
27735
|
-
let hasTransferableWork = false;
|
|
27736
|
-
walkAst(consequent, (child) => {
|
|
27737
|
-
if (hasTransferableWork) return false;
|
|
27738
|
-
if (child !== consequent && isFunctionLike$1(child)) return false;
|
|
27739
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
27740
|
-
if (isNodeOfType(child.callee, "Identifier")) {
|
|
27741
|
-
const calleeReference = getRef(analysis, child.callee);
|
|
27742
|
-
if (calleeReference && isStateSetterCall(analysis, calleeReference)) return;
|
|
27743
|
-
if (calleeReference?.resolved?.defs.some((definition) => {
|
|
27744
|
-
const definitionNode = definition.node;
|
|
27745
|
-
if (!isNodeOfType(definitionNode, "VariableDeclarator")) return false;
|
|
27746
|
-
if (!isNodeOfType(definitionNode.init, "CallExpression")) return false;
|
|
27747
|
-
const hookCallee = definitionNode.init.callee;
|
|
27748
|
-
return isNodeOfType(hookCallee, "Identifier") && hookCallee.name !== "useReducer" && /^use[A-Z0-9]/.test(hookCallee.name);
|
|
27749
|
-
})) return;
|
|
27750
|
-
} else if (!isNodeOfType(child.callee, "MemberExpression") || !isNodeOfType(child.callee.property, "Identifier") || child.callee.property.name !== "setAttribute") return;
|
|
27751
|
-
hasTransferableWork = true;
|
|
27752
|
-
return false;
|
|
27753
|
-
});
|
|
27754
|
-
return hasTransferableWork;
|
|
27932
|
+
const isImperativeSyncConsequent = (analysis, ifNode) => {
|
|
27933
|
+
const statements = getConsequentStatements(ifNode.consequent);
|
|
27934
|
+
if (statements.length === 0) return false;
|
|
27935
|
+
if (statements.every((statement) => isSideEffectFreeExit(statement))) return false;
|
|
27936
|
+
const testedBindings = /* @__PURE__ */ new Set();
|
|
27937
|
+
for (const testRef of getDownstreamRefs(analysis, ifNode.test)) if (testRef.resolved) testedBindings.add(testRef.resolved);
|
|
27938
|
+
return statements.every((statement) => isImperativeSyncStatement(analysis, statement, testedBindings));
|
|
27755
27939
|
};
|
|
27756
|
-
const
|
|
27757
|
-
|
|
27758
|
-
if (!
|
|
27759
|
-
|
|
27760
|
-
|
|
27761
|
-
|
|
27762
|
-
|
|
27763
|
-
|
|
27764
|
-
|
|
27765
|
-
|
|
27766
|
-
|
|
27767
|
-
|
|
27768
|
-
|
|
27769
|
-
|
|
27770
|
-
|
|
27771
|
-
|
|
27772
|
-
|
|
27940
|
+
const isSetterInvocationUsage = (identifier) => {
|
|
27941
|
+
const parent = identifier.parent;
|
|
27942
|
+
if (!parent) return false;
|
|
27943
|
+
if (isNodeOfType(parent, "CallExpression")) {
|
|
27944
|
+
if (parent.callee === identifier) return true;
|
|
27945
|
+
return (parent.arguments ?? []).some((argument) => argument === identifier);
|
|
27946
|
+
}
|
|
27947
|
+
return isNodeOfType(parent, "JSXExpressionContainer");
|
|
27948
|
+
};
|
|
27949
|
+
const isStateSetterUsedOutsideEffect = (analysis, stateRef, effectNode) => {
|
|
27950
|
+
const declarator = stateRef.resolved?.defs.map((def) => def.node).find((defNode) => isNodeOfType(defNode, "VariableDeclarator") && isNodeOfType(defNode.id, "ArrayPattern"));
|
|
27951
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
|
|
27952
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
27953
|
+
const setterElement = declarator.id.elements?.[1];
|
|
27954
|
+
if (!setterElement || !isNodeOfType(setterElement, "Identifier")) return false;
|
|
27955
|
+
const setterName = setterElement.name;
|
|
27956
|
+
for (const scope of analysis.scopeManager.scopes) {
|
|
27957
|
+
const setterVariable = scope.variables.find((variable) => variable.name === setterName && variable.defs.some((def) => def.node === declarator));
|
|
27958
|
+
if (!setterVariable) continue;
|
|
27959
|
+
return setterVariable.references.some((reference) => {
|
|
27960
|
+
const identifier = reference.identifier;
|
|
27961
|
+
if (isAstDescendant(identifier, effectNode)) return false;
|
|
27962
|
+
return isSetterInvocationUsage(identifier);
|
|
27963
|
+
});
|
|
27964
|
+
}
|
|
27965
|
+
return false;
|
|
27773
27966
|
};
|
|
27774
27967
|
const noEventHandler = defineRule({
|
|
27775
27968
|
id: "no-event-handler",
|
|
@@ -27779,30 +27972,58 @@ const noEventHandler = defineRule({
|
|
|
27779
27972
|
recommendation: "Run the side effect in the event handler that triggers it, instead of watching its state from a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers",
|
|
27780
27973
|
create: (context) => ({ CallExpression(node) {
|
|
27781
27974
|
if (!isUseEffect(node)) return;
|
|
27975
|
+
const depsArgument = node.arguments?.[1];
|
|
27976
|
+
const isMountOnlyEffect = Boolean(depsArgument && isNodeOfType(depsArgument, "ArrayExpression") && (depsArgument.elements ?? []).length === 0);
|
|
27782
27977
|
const analysis = getProgramAnalysis(node);
|
|
27783
|
-
if (!analysis
|
|
27784
|
-
|
|
27785
|
-
|
|
27786
|
-
|
|
27787
|
-
|
|
27788
|
-
|
|
27789
|
-
|
|
27790
|
-
|
|
27791
|
-
|
|
27792
|
-
|
|
27793
|
-
|
|
27794
|
-
|
|
27795
|
-
|
|
27796
|
-
|
|
27797
|
-
|
|
27798
|
-
|
|
27799
|
-
|
|
27800
|
-
|
|
27801
|
-
|
|
27802
|
-
|
|
27803
|
-
|
|
27978
|
+
if (!analysis) return;
|
|
27979
|
+
if (hasCleanup(analysis, node)) return;
|
|
27980
|
+
if (!getEffectFnRefs(analysis, node)) return;
|
|
27981
|
+
const effectFn = getEffectFn(analysis, node);
|
|
27982
|
+
if (!effectFn) return;
|
|
27983
|
+
const ifTestRefEntries = findDownstreamNodes(node, "IfStatement").filter((ifNode) => isNodeOfType(ifNode, "IfStatement") && !ifNode.alternate && !isPureEarlyExitConsequent(ifNode.consequent) && !isControlledPropMirrorConsequent(analysis, ifNode) && !containsRefGuard(ifNode.test) && !containsPreviousValueComparison(analysis, ifNode.test) && !isLocalComputationConsequent(analysis, ifNode, effectFn) && !isEffectLocalOnlyTest(analysis, ifNode, effectFn) && !isDepsListedCallbackInvocationConsequent(analysis, ifNode, node) && !isImperativeSyncConsequent(analysis, ifNode)).flatMap((ifNode) => {
|
|
27984
|
+
if (!isNodeOfType(ifNode, "IfStatement")) return [];
|
|
27985
|
+
const directTestRefs = getDownstreamRefs(analysis, ifNode.test);
|
|
27986
|
+
const isAsyncDataGated = directTestRefs.some((ref) => isOpaqueHookResultBinding(ref));
|
|
27987
|
+
return directTestRefs.filter((ref) => !(isState(analysis, ref) && isExternallyDrivenState(analysis, ref))).flatMap((ref) => getGuardUpstreamRefs(analysis, ref)).map((ref) => ({
|
|
27988
|
+
ref,
|
|
27989
|
+
isAsyncDataGated
|
|
27990
|
+
}));
|
|
27991
|
+
});
|
|
27992
|
+
const seenBindings = /* @__PURE__ */ new Map();
|
|
27993
|
+
const seenIdentifiers = /* @__PURE__ */ new Set();
|
|
27994
|
+
const dedupedEntries = [];
|
|
27995
|
+
for (const entry of ifTestRefEntries) {
|
|
27996
|
+
const identifier = entry.ref.identifier;
|
|
27997
|
+
if (!identifier) continue;
|
|
27998
|
+
const resolved = entry.ref.resolved;
|
|
27999
|
+
if (resolved) {
|
|
28000
|
+
const seen = seenBindings.get(resolved);
|
|
28001
|
+
if (seen) {
|
|
28002
|
+
if (!entry.isAsyncDataGated) seen.isAsyncDataGated = false;
|
|
28003
|
+
continue;
|
|
28004
|
+
}
|
|
28005
|
+
seenBindings.set(resolved, entry);
|
|
28006
|
+
}
|
|
28007
|
+
if (seenIdentifiers.has(identifier)) continue;
|
|
28008
|
+
seenIdentifiers.add(identifier);
|
|
28009
|
+
dedupedEntries.push(entry);
|
|
28010
|
+
}
|
|
28011
|
+
const dedupedRefs = dedupedEntries.map((entry) => entry.ref);
|
|
28012
|
+
for (const ref of dedupedRefs) if (isState(analysis, ref)) {
|
|
28013
|
+
if (isExternallyDrivenState(analysis, ref)) continue;
|
|
28014
|
+
if (isMountOnlyEffect && !isStateSetterUsedOutsideEffect(analysis, ref, node)) continue;
|
|
28015
|
+
context.report({
|
|
28016
|
+
node: ref.identifier,
|
|
28017
|
+
message: "Faking an event handler with state plus a useEffect costs an extra render & runs late."
|
|
28018
|
+
});
|
|
28019
|
+
}
|
|
28020
|
+
for (const entry of dedupedEntries) if (isProp(analysis, entry.ref)) {
|
|
28021
|
+
if (isMountOnlyEffect) continue;
|
|
28022
|
+
if (entry.isAsyncDataGated) continue;
|
|
28023
|
+
context.report({
|
|
28024
|
+
node: entry.ref.identifier,
|
|
28025
|
+
message: "Faking an event handler with a prop plus a useEffect costs an extra render & runs late."
|
|
27804
28026
|
});
|
|
27805
|
-
if (reported) return;
|
|
27806
28027
|
}
|
|
27807
28028
|
} })
|
|
27808
28029
|
});
|
|
@@ -28410,6 +28631,46 @@ const collectFunctionLikeLocalNames = (componentBody) => {
|
|
|
28410
28631
|
return functionLikeLocalNames;
|
|
28411
28632
|
};
|
|
28412
28633
|
//#endregion
|
|
28634
|
+
//#region src/plugin/rules/state-and-effects/utils/collect-handler-binding-names.ts
|
|
28635
|
+
const collectHandlerBindingNames = (componentBody) => {
|
|
28636
|
+
const handlerNames = /* @__PURE__ */ new Set();
|
|
28637
|
+
walkAst(componentBody, (child) => {
|
|
28638
|
+
if (!isNodeOfType(child, "JSXAttribute")) return;
|
|
28639
|
+
if (!isNodeOfType(child.name, "JSXIdentifier")) return;
|
|
28640
|
+
if (!/^on[A-Z]/.test(child.name.name)) return;
|
|
28641
|
+
if (!isNodeOfType(child.value, "JSXExpressionContainer")) return;
|
|
28642
|
+
const expression = child.value.expression;
|
|
28643
|
+
if (isNodeOfType(expression, "Identifier")) handlerNames.add(expression.name);
|
|
28644
|
+
});
|
|
28645
|
+
return handlerNames;
|
|
28646
|
+
};
|
|
28647
|
+
//#endregion
|
|
28648
|
+
//#region src/plugin/rules/state-and-effects/utils/is-inside-event-handler.ts
|
|
28649
|
+
const isInsideEventHandler = (node, handlerBindingNames) => {
|
|
28650
|
+
let cursor = node.parent ?? null;
|
|
28651
|
+
while (cursor) {
|
|
28652
|
+
if (isNodeOfType(cursor, "ArrowFunctionExpression") || isNodeOfType(cursor, "FunctionExpression") || isNodeOfType(cursor, "FunctionDeclaration")) {
|
|
28653
|
+
let outer = cursor.parent ?? null;
|
|
28654
|
+
while (outer) {
|
|
28655
|
+
if (isNodeOfType(outer, "JSXAttribute")) {
|
|
28656
|
+
const attrName = isNodeOfType(outer.name, "JSXIdentifier") ? outer.name.name : null;
|
|
28657
|
+
if (attrName && /^on[A-Z]/.test(attrName)) return true;
|
|
28658
|
+
return false;
|
|
28659
|
+
}
|
|
28660
|
+
if (isNodeOfType(outer, "VariableDeclarator")) {
|
|
28661
|
+
const declaredName = isNodeOfType(outer.id, "Identifier") ? outer.id.name : null;
|
|
28662
|
+
return Boolean(declaredName && handlerBindingNames.has(declaredName));
|
|
28663
|
+
}
|
|
28664
|
+
if (isNodeOfType(outer, "Program")) return false;
|
|
28665
|
+
outer = outer.parent ?? null;
|
|
28666
|
+
}
|
|
28667
|
+
return false;
|
|
28668
|
+
}
|
|
28669
|
+
cursor = cursor.parent ?? null;
|
|
28670
|
+
}
|
|
28671
|
+
return false;
|
|
28672
|
+
};
|
|
28673
|
+
//#endregion
|
|
28413
28674
|
//#region src/plugin/rules/state-and-effects/no-event-trigger-state.ts
|
|
28414
28675
|
const SENTINEL_IDENTIFIER_NAMES = new Set([
|
|
28415
28676
|
"undefined",
|
|
@@ -28436,6 +28697,23 @@ const getTriggerGuardRootName = (testNode) => {
|
|
|
28436
28697
|
if (isNodeOfType(testNode, "UnaryExpression") && testNode.operator === "!") return getTriggerGuardRootName(testNode.argument);
|
|
28437
28698
|
return null;
|
|
28438
28699
|
};
|
|
28700
|
+
const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
|
|
28701
|
+
const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
|
|
28702
|
+
const settersWithAnyCall = /* @__PURE__ */ new Set();
|
|
28703
|
+
const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
|
|
28704
|
+
walkAst(componentBody, (child) => {
|
|
28705
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
28706
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
28707
|
+
const setterName = child.callee.name;
|
|
28708
|
+
if (!setterNames.has(setterName)) return;
|
|
28709
|
+
settersWithAnyCall.add(setterName);
|
|
28710
|
+
if (settersWithNonHandlerCall.has(setterName)) return;
|
|
28711
|
+
if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
|
|
28712
|
+
});
|
|
28713
|
+
const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
|
|
28714
|
+
for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
28715
|
+
return handlerOnlyWriteStateNames;
|
|
28716
|
+
};
|
|
28439
28717
|
const noEventTriggerState = defineRule({
|
|
28440
28718
|
id: "no-event-trigger-state",
|
|
28441
28719
|
title: "State exists only to trigger an effect",
|
|
@@ -28447,9 +28725,8 @@ const noEventTriggerState = defineRule({
|
|
|
28447
28725
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
28448
28726
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
28449
28727
|
if (useStateBindings.length === 0) return;
|
|
28450
|
-
const
|
|
28451
|
-
if (
|
|
28452
|
-
const localStateNames = new Set(useStateBindings.map((binding) => binding.valueName));
|
|
28728
|
+
const handlerOnlyWriteStateNames = collectHandlerOnlyWriteStateNames(componentBody, useStateBindings, collectHandlerBindingNames(componentBody));
|
|
28729
|
+
if (handlerOnlyWriteStateNames.size === 0) return;
|
|
28453
28730
|
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody);
|
|
28454
28731
|
const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
|
|
28455
28732
|
const renderReachableNames = expandTransitiveDependencies(collectRenderReachableNames(componentBody, eventHandlerReferenceNames), dependencyGraph);
|
|
@@ -28462,9 +28739,7 @@ const noEventTriggerState = defineRule({
|
|
|
28462
28739
|
if ((depsNode.elements?.length ?? 0) !== 1) return;
|
|
28463
28740
|
const depElement = depsNode.elements[0];
|
|
28464
28741
|
if (!isNodeOfType(depElement, "Identifier")) return;
|
|
28465
|
-
if (!
|
|
28466
|
-
const dependencyReference = getRef(analysis, depElement);
|
|
28467
|
-
if (!dependencyReference || !isState(analysis, dependencyReference) || !isStateWrittenOnlyFromEventHandlers(analysis, dependencyReference)) return;
|
|
28742
|
+
if (!handlerOnlyWriteStateNames.has(depElement.name)) return;
|
|
28468
28743
|
if (renderReachableNames.has(depElement.name)) return;
|
|
28469
28744
|
const callback = getEffectCallback(effectCall);
|
|
28470
28745
|
if (!callback) return;
|
|
@@ -29361,12 +29636,264 @@ const noIndeterminateAttribute = defineRule({
|
|
|
29361
29636
|
}
|
|
29362
29637
|
});
|
|
29363
29638
|
//#endregion
|
|
29639
|
+
//#region src/plugin/utils/contains-non-deterministic-source.ts
|
|
29640
|
+
const NON_DETERMINISTIC_MEMBER_CALLS = new Set([
|
|
29641
|
+
"Math.random",
|
|
29642
|
+
"Date.now",
|
|
29643
|
+
"performance.now",
|
|
29644
|
+
"crypto.randomUUID",
|
|
29645
|
+
"crypto.getRandomValues"
|
|
29646
|
+
]);
|
|
29647
|
+
const NON_DETERMINISTIC_ID_GENERATOR_NAMES = new Set([
|
|
29648
|
+
"nanoid",
|
|
29649
|
+
"uuid",
|
|
29650
|
+
"cuid",
|
|
29651
|
+
"ulid",
|
|
29652
|
+
"createId"
|
|
29653
|
+
]);
|
|
29654
|
+
const isZeroArgDateConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Date" && (node.arguments?.length ?? 0) === 0;
|
|
29655
|
+
const containsNonDeterministicSource = (root) => {
|
|
29656
|
+
let found = false;
|
|
29657
|
+
walkAst(root, (child) => {
|
|
29658
|
+
if (found) return false;
|
|
29659
|
+
if (isFunctionLike$1(child)) return false;
|
|
29660
|
+
if (isZeroArgDateConstruction(child)) {
|
|
29661
|
+
found = true;
|
|
29662
|
+
return false;
|
|
29663
|
+
}
|
|
29664
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
29665
|
+
const callee = child.callee;
|
|
29666
|
+
if (isNodeOfType(callee, "Identifier") && NON_DETERMINISTIC_ID_GENERATOR_NAMES.has(callee.name)) {
|
|
29667
|
+
found = true;
|
|
29668
|
+
return false;
|
|
29669
|
+
}
|
|
29670
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && NON_DETERMINISTIC_MEMBER_CALLS.has(`${callee.object.name}.${callee.property.name}`)) {
|
|
29671
|
+
found = true;
|
|
29672
|
+
return false;
|
|
29673
|
+
}
|
|
29674
|
+
});
|
|
29675
|
+
return found;
|
|
29676
|
+
};
|
|
29677
|
+
//#endregion
|
|
29364
29678
|
//#region src/plugin/rules/state-and-effects/no-initialize-state.ts
|
|
29365
|
-
const
|
|
29366
|
-
|
|
29367
|
-
|
|
29368
|
-
|
|
29369
|
-
|
|
29679
|
+
const MEASUREMENT_GLOBAL_NAMES = new Set([
|
|
29680
|
+
"window",
|
|
29681
|
+
"document",
|
|
29682
|
+
"navigator"
|
|
29683
|
+
]);
|
|
29684
|
+
const objectPatternBindsName = (pattern, name) => {
|
|
29685
|
+
if (!isNodeOfType(pattern, "ObjectPattern")) return false;
|
|
29686
|
+
return (pattern.properties ?? []).some((property) => {
|
|
29687
|
+
if (!isNodeOfType(property, "Property")) return false;
|
|
29688
|
+
const bound = property.value;
|
|
29689
|
+
return Boolean(bound && isNodeOfType(bound, "Identifier") && bound.name === name);
|
|
29690
|
+
});
|
|
29691
|
+
};
|
|
29692
|
+
const findEffectLocalInitializer = (effectFn, name) => {
|
|
29693
|
+
let initializer = null;
|
|
29694
|
+
walkAst(effectFn, (child) => {
|
|
29695
|
+
if (initializer) return false;
|
|
29696
|
+
if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
|
|
29697
|
+
if (isNodeOfType(child.id, "Identifier") && child.id.name === name || objectPatternBindsName(child.id, name)) {
|
|
29698
|
+
initializer = child.init;
|
|
29699
|
+
return false;
|
|
29700
|
+
}
|
|
29701
|
+
});
|
|
29702
|
+
return initializer;
|
|
29703
|
+
};
|
|
29704
|
+
const isMeasurementGlobalValueRead = (identifier) => {
|
|
29705
|
+
const memberParent = identifier.parent;
|
|
29706
|
+
if (!isNodeOfType(memberParent, "MemberExpression") || memberParent.object !== identifier) return false;
|
|
29707
|
+
if (isNodeOfType(memberParent.property, "Identifier") && DOM_QUERY_MEMBER_NAMES.has(memberParent.property.name)) {
|
|
29708
|
+
const callGrandparent = memberParent.parent;
|
|
29709
|
+
return Boolean(callGrandparent && isNodeOfType(callGrandparent, "CallExpression") && callGrandparent.callee === memberParent);
|
|
29710
|
+
}
|
|
29711
|
+
return true;
|
|
29712
|
+
};
|
|
29713
|
+
const argumentReadsPostMountMeasurement = (argument, effectFn, visitedLocalNames = /* @__PURE__ */ new Set()) => {
|
|
29714
|
+
let found = false;
|
|
29715
|
+
walkAst(argument, (child) => {
|
|
29716
|
+
if (found) return false;
|
|
29717
|
+
if (isMeasurementMemberRead(child)) {
|
|
29718
|
+
found = true;
|
|
29719
|
+
return false;
|
|
29720
|
+
}
|
|
29721
|
+
if (!isNodeOfType(child, "Identifier")) return;
|
|
29722
|
+
if (isPostMountGlobalRead(child) && MEASUREMENT_GLOBAL_NAMES.has(child.name) && isMeasurementGlobalValueRead(child)) {
|
|
29723
|
+
found = true;
|
|
29724
|
+
return false;
|
|
29725
|
+
}
|
|
29726
|
+
if (visitedLocalNames.has(child.name)) return;
|
|
29727
|
+
visitedLocalNames.add(child.name);
|
|
29728
|
+
const localInitializer = findEffectLocalInitializer(effectFn, child.name);
|
|
29729
|
+
if (localInitializer && argumentReadsPostMountMeasurement(localInitializer, effectFn, visitedLocalNames)) {
|
|
29730
|
+
found = true;
|
|
29731
|
+
return false;
|
|
29732
|
+
}
|
|
29733
|
+
});
|
|
29734
|
+
return found;
|
|
29735
|
+
};
|
|
29736
|
+
const isResourceLikeInitializer = (initializer) => {
|
|
29737
|
+
if (isNodeOfType(initializer, "AwaitExpression")) return isResourceLikeInitializer(initializer.argument);
|
|
29738
|
+
return isNodeOfType(initializer, "NewExpression") || isNodeOfType(initializer, "CallExpression");
|
|
29739
|
+
};
|
|
29740
|
+
const collectArgumentSourceLocalNames = (argument, effectFn, sourceLocalNames = /* @__PURE__ */ new Set()) => {
|
|
29741
|
+
walkAst(argument, (child) => {
|
|
29742
|
+
if (!isNodeOfType(child, "Identifier")) return;
|
|
29743
|
+
if (sourceLocalNames.has(child.name)) return;
|
|
29744
|
+
const localInitializer = findEffectLocalInitializer(effectFn, child.name);
|
|
29745
|
+
if (!localInitializer || !isResourceLikeInitializer(localInitializer)) return;
|
|
29746
|
+
sourceLocalNames.add(child.name);
|
|
29747
|
+
collectArgumentSourceLocalNames(localInitializer, effectFn, sourceLocalNames);
|
|
29748
|
+
});
|
|
29749
|
+
return sourceLocalNames;
|
|
29750
|
+
};
|
|
29751
|
+
const isFunctionExpressionLike$2 = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression");
|
|
29752
|
+
const findCleanupFunction = (effectFn) => {
|
|
29753
|
+
if (!isNodeOfType(effectFn, "ArrowFunctionExpression") && !isNodeOfType(effectFn, "FunctionExpression")) return null;
|
|
29754
|
+
const body = effectFn.body;
|
|
29755
|
+
if (!isNodeOfType(body, "BlockStatement")) return null;
|
|
29756
|
+
let cleanupFunction = null;
|
|
29757
|
+
walkAst(body, (child) => {
|
|
29758
|
+
if (cleanupFunction) return false;
|
|
29759
|
+
if (isNodeOfType(child, "ReturnStatement")) {
|
|
29760
|
+
if (child.argument && isFunctionExpressionLike$2(child.argument)) cleanupFunction = child.argument;
|
|
29761
|
+
return false;
|
|
29762
|
+
}
|
|
29763
|
+
if (child !== body && isFunctionExpressionLike$2(child)) return false;
|
|
29764
|
+
if (isNodeOfType(child, "FunctionDeclaration")) return false;
|
|
29765
|
+
});
|
|
29766
|
+
return cleanupFunction;
|
|
29767
|
+
};
|
|
29768
|
+
const cleanupDisposesArgumentSource = (argument, effectFn) => {
|
|
29769
|
+
const cleanupFunction = findCleanupFunction(effectFn);
|
|
29770
|
+
if (!cleanupFunction) return false;
|
|
29771
|
+
const sourceLocalNames = collectArgumentSourceLocalNames(argument, effectFn);
|
|
29772
|
+
if (sourceLocalNames.size === 0) return false;
|
|
29773
|
+
let referencesSource = false;
|
|
29774
|
+
walkAst(cleanupFunction, (child) => {
|
|
29775
|
+
if (referencesSource) return false;
|
|
29776
|
+
if (isNodeOfType(child, "Identifier") && sourceLocalNames.has(child.name)) {
|
|
29777
|
+
referencesSource = true;
|
|
29778
|
+
return false;
|
|
29779
|
+
}
|
|
29780
|
+
});
|
|
29781
|
+
return referencesSource;
|
|
29782
|
+
};
|
|
29783
|
+
const cleanupResetsSameSetter = (effectFn, setterName) => {
|
|
29784
|
+
const cleanupFunction = findCleanupFunction(effectFn);
|
|
29785
|
+
if (!cleanupFunction) return false;
|
|
29786
|
+
let resets = false;
|
|
29787
|
+
walkAst(cleanupFunction, (child) => {
|
|
29788
|
+
if (resets) return false;
|
|
29789
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
|
|
29790
|
+
resets = true;
|
|
29791
|
+
return false;
|
|
29792
|
+
}
|
|
29793
|
+
});
|
|
29794
|
+
return resets;
|
|
29795
|
+
};
|
|
29796
|
+
const TYPEOF_BROWSER_GLOBAL_NAMES = new Set(["window", "document"]);
|
|
29797
|
+
const initializerHasTypeofBrowserGlobalCheck = (useStateDecl) => {
|
|
29798
|
+
if (!isNodeOfType(useStateDecl, "VariableDeclarator")) return false;
|
|
29799
|
+
if (!isNodeOfType(useStateDecl.init, "CallExpression")) return false;
|
|
29800
|
+
const initialArgument = useStateDecl.init.arguments?.[0];
|
|
29801
|
+
if (!initialArgument) return false;
|
|
29802
|
+
let found = false;
|
|
29803
|
+
walkAst(initialArgument, (child) => {
|
|
29804
|
+
if (found) return false;
|
|
29805
|
+
if (isNodeOfType(child, "UnaryExpression") && child.operator === "typeof" && isNodeOfType(child.argument, "Identifier") && TYPEOF_BROWSER_GLOBAL_NAMES.has(child.argument.name)) {
|
|
29806
|
+
found = true;
|
|
29807
|
+
return false;
|
|
29808
|
+
}
|
|
29809
|
+
});
|
|
29810
|
+
return found;
|
|
29811
|
+
};
|
|
29812
|
+
const HANDLER_HELPER_NAME_PATTERN = /^(on|handle)[A-Z_]/;
|
|
29813
|
+
const isSameValueExpression = (leftNode, rightNode) => {
|
|
29814
|
+
const left = stripParenExpression(leftNode);
|
|
29815
|
+
const right = stripParenExpression(rightNode);
|
|
29816
|
+
if (left.type !== right.type) return false;
|
|
29817
|
+
if (isNodeOfType(left, "Identifier") && isNodeOfType(right, "Identifier")) return left.name === right.name;
|
|
29818
|
+
if (isNodeOfType(left, "Literal") && isNodeOfType(right, "Literal")) return left.value === right.value;
|
|
29819
|
+
if (isNodeOfType(left, "MemberExpression") && isNodeOfType(right, "MemberExpression")) {
|
|
29820
|
+
if (left.computed !== right.computed) return false;
|
|
29821
|
+
return isSameValueExpression(left.property, right.property) && isSameValueExpression(left.object, right.object);
|
|
29822
|
+
}
|
|
29823
|
+
if (isNodeOfType(left, "CallExpression") && isNodeOfType(right, "CallExpression")) {
|
|
29824
|
+
const leftArguments = left.arguments ?? [];
|
|
29825
|
+
const rightArguments = right.arguments ?? [];
|
|
29826
|
+
if (leftArguments.length !== rightArguments.length) return false;
|
|
29827
|
+
if (!isSameValueExpression(left.callee, right.callee)) return false;
|
|
29828
|
+
return leftArguments.every((leftArgument, argumentIndex) => isSameValueExpression(leftArgument, rightArguments[argumentIndex]));
|
|
29829
|
+
}
|
|
29830
|
+
if (isNodeOfType(left, "UnaryExpression") && isNodeOfType(right, "UnaryExpression")) return left.operator === right.operator && isSameValueExpression(left.argument, right.argument);
|
|
29831
|
+
return false;
|
|
29832
|
+
};
|
|
29833
|
+
const unwrapLazyInitializer = (initializer) => {
|
|
29834
|
+
if (!isNodeOfType(initializer, "ArrowFunctionExpression")) return initializer;
|
|
29835
|
+
const body = initializer.body;
|
|
29836
|
+
if (!isNodeOfType(body, "BlockStatement")) return body;
|
|
29837
|
+
const statements = body.body ?? [];
|
|
29838
|
+
if (statements.length === 1 && isNodeOfType(statements[0], "ReturnStatement")) {
|
|
29839
|
+
const returned = statements[0].argument;
|
|
29840
|
+
if (returned) return returned;
|
|
29841
|
+
}
|
|
29842
|
+
return initializer;
|
|
29843
|
+
};
|
|
29844
|
+
const collectInitializerValueExpressions = (useStateDecl) => {
|
|
29845
|
+
if (!isNodeOfType(useStateDecl, "VariableDeclarator")) return [];
|
|
29846
|
+
if (!isNodeOfType(useStateDecl.init, "CallExpression")) return [];
|
|
29847
|
+
const initialArgument = useStateDecl.init.arguments?.[0];
|
|
29848
|
+
if (!initialArgument) return [];
|
|
29849
|
+
const expressions = [];
|
|
29850
|
+
const expand = (node) => {
|
|
29851
|
+
expressions.push(node);
|
|
29852
|
+
if (isNodeOfType(node, "LogicalExpression") && (node.operator === "??" || node.operator === "||")) {
|
|
29853
|
+
expand(node.left);
|
|
29854
|
+
expand(node.right);
|
|
29855
|
+
}
|
|
29856
|
+
};
|
|
29857
|
+
expand(unwrapLazyInitializer(initialArgument));
|
|
29858
|
+
return expressions;
|
|
29859
|
+
};
|
|
29860
|
+
const isUseStateWithoutArgument = (useStateDecl) => isNodeOfType(useStateDecl, "VariableDeclarator") && isNodeOfType(useStateDecl.init, "CallExpression") && (useStateDecl.init.arguments ?? []).length === 0;
|
|
29861
|
+
const isUndefinedExpression = (node) => {
|
|
29862
|
+
const unwrapped = stripParenExpression(node);
|
|
29863
|
+
if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === "undefined";
|
|
29864
|
+
return isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "void";
|
|
29865
|
+
};
|
|
29866
|
+
const isSameValueAsInitializer = (callExpr, useStateDecl) => {
|
|
29867
|
+
if (!isNodeOfType(callExpr, "CallExpression")) return false;
|
|
29868
|
+
const args = callExpr.arguments ?? [];
|
|
29869
|
+
if (args.length !== 1) return false;
|
|
29870
|
+
const setterArgument = args[0];
|
|
29871
|
+
if (isUseStateWithoutArgument(useStateDecl) && isUndefinedExpression(setterArgument)) return true;
|
|
29872
|
+
return collectInitializerValueExpressions(useStateDecl).some((initializerValue) => isSameValueExpression(setterArgument, initializerValue));
|
|
29873
|
+
};
|
|
29874
|
+
const collectInnerStateSetterCalls = (analysis, helperFn) => {
|
|
29875
|
+
const innerCalls = [];
|
|
29876
|
+
const helperBody = helperFn.body;
|
|
29877
|
+
if (!helperBody) return innerCalls;
|
|
29878
|
+
walkAst(helperBody, (child) => {
|
|
29879
|
+
if (!isNodeOfType(child, "Identifier")) return;
|
|
29880
|
+
const innerRef = getRef(analysis, child);
|
|
29881
|
+
if (!innerRef || !isStateSetter(analysis, innerRef)) return;
|
|
29882
|
+
const innerCallExpr = getCallExpr(innerRef);
|
|
29883
|
+
if (!innerCallExpr) return;
|
|
29884
|
+
innerCalls.push({
|
|
29885
|
+
ref: innerRef,
|
|
29886
|
+
callExpr: innerCallExpr,
|
|
29887
|
+
isSyncWithinFunction: isSynchronous(child, helperFn)
|
|
29888
|
+
});
|
|
29889
|
+
});
|
|
29890
|
+
return innerCalls;
|
|
29891
|
+
};
|
|
29892
|
+
const isNodeWithinRange = (inner, outer) => {
|
|
29893
|
+
const innerRange = inner.range;
|
|
29894
|
+
const outerRange = outer.range;
|
|
29895
|
+
if (!innerRange || !outerRange) return false;
|
|
29896
|
+
return outerRange[0] <= innerRange[0] && innerRange[1] <= outerRange[1];
|
|
29370
29897
|
};
|
|
29371
29898
|
const noInitializeState = defineRule({
|
|
29372
29899
|
id: "no-initialize-state",
|
|
@@ -29376,15 +29903,53 @@ const noInitializeState = defineRule({
|
|
|
29376
29903
|
recommendation: "Pass the initial value directly to useState() instead of setting it from a mount-only useEffect. For SSR hydration, prefer useSyncExternalStore().",
|
|
29377
29904
|
create: (context) => ({ CallExpression(node) {
|
|
29378
29905
|
if (!isUseEffect(node)) return;
|
|
29379
|
-
const dependencies = node.arguments?.[1];
|
|
29380
|
-
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
29381
29906
|
const analysis = getProgramAnalysis(node);
|
|
29382
29907
|
if (!analysis) return;
|
|
29383
|
-
|
|
29384
|
-
|
|
29385
|
-
|
|
29908
|
+
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
29909
|
+
const depsRefs = getEffectDepsRefs(analysis, node);
|
|
29910
|
+
if (!effectFnRefs || !depsRefs) return;
|
|
29911
|
+
const effectFn = getEffectFn(analysis, node);
|
|
29912
|
+
if (!effectFn) return;
|
|
29913
|
+
if (!(depsRefs.filter((ref) => !isStateSetter(analysis, ref)).length === 0)) return;
|
|
29914
|
+
for (const ref of effectFnRefs) {
|
|
29915
|
+
if (!isSyncStateSetterCall(analysis, ref, effectFn)) continue;
|
|
29916
|
+
const callExpr = getCallExpr(ref);
|
|
29917
|
+
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
29918
|
+
if (callExpr.arguments?.some((argument) => Boolean(argument) && containsNonDeterministicSource(argument))) continue;
|
|
29919
|
+
if (callExpr.arguments?.some((argument) => Boolean(argument) && argumentReadsPostMountMeasurement(argument, effectFn))) continue;
|
|
29920
|
+
if (callExpr.arguments?.some((argument) => Boolean(argument) && cleanupDisposesArgumentSource(argument, effectFn))) continue;
|
|
29921
|
+
if (!isStateSetter(analysis, ref)) {
|
|
29922
|
+
const helperFn = resolveToFunction(ref);
|
|
29923
|
+
if (helperFn) {
|
|
29924
|
+
const helperName = getFunctionBindingName$1(helperFn);
|
|
29925
|
+
if (helperName && HANDLER_HELPER_NAME_PATTERN.test(helperName)) continue;
|
|
29926
|
+
}
|
|
29927
|
+
if (helperFn) {
|
|
29928
|
+
const innerSetterCalls = collectInnerStateSetterCalls(analysis, helperFn);
|
|
29929
|
+
if (innerSetterCalls.length > 0) {
|
|
29930
|
+
const syncInnerCalls = innerSetterCalls.filter((innerCall) => innerCall.isSyncWithinFunction);
|
|
29931
|
+
if (syncInnerCalls.length === 0) continue;
|
|
29932
|
+
const measurementLookupFn = isNodeWithinRange(helperFn, effectFn) ? effectFn : helperFn;
|
|
29933
|
+
if (syncInnerCalls.every((innerCall) => {
|
|
29934
|
+
if (!isNodeOfType(innerCall.callExpr, "CallExpression")) return false;
|
|
29935
|
+
const innerUseStateDecl = getUseStateDecl(analysis, innerCall.ref);
|
|
29936
|
+
if (innerUseStateDecl && isSameValueAsInitializer(innerCall.callExpr, innerUseStateDecl)) return true;
|
|
29937
|
+
return (innerCall.callExpr.arguments ?? []).some((argument) => Boolean(argument) && (containsNonDeterministicSource(argument) || argumentReadsPostMountMeasurement(argument, measurementLookupFn)));
|
|
29938
|
+
})) continue;
|
|
29939
|
+
}
|
|
29940
|
+
}
|
|
29941
|
+
}
|
|
29942
|
+
const useStateDecl = getUseStateDecl(analysis, ref);
|
|
29943
|
+
if (!useStateDecl || !isNodeOfType(useStateDecl, "VariableDeclarator")) continue;
|
|
29944
|
+
if (isSameValueAsInitializer(callExpr, useStateDecl)) continue;
|
|
29945
|
+
if (initializerHasTypeofBrowserGlobalCheck(useStateDecl)) continue;
|
|
29946
|
+
if (isNodeOfType(callExpr.callee, "Identifier") && cleanupResetsSameSetter(effectFn, callExpr.callee.name)) continue;
|
|
29947
|
+
if (!isNodeOfType(useStateDecl.id, "ArrayPattern")) continue;
|
|
29948
|
+
const elements = useStateDecl.id.elements ?? [];
|
|
29949
|
+
const stateBinding = elements[0] ?? elements[1];
|
|
29950
|
+
const stateName = stateBinding && isNodeOfType(stateBinding, "Identifier") ? stateBinding.name : "<state>";
|
|
29386
29951
|
context.report({
|
|
29387
|
-
node:
|
|
29952
|
+
node: callExpr,
|
|
29388
29953
|
message: `Your users see an extra render with empty "${stateName}" because a useEffect sets its starting value.`
|
|
29389
29954
|
});
|
|
29390
29955
|
}
|
|
@@ -32445,19 +33010,6 @@ const DATA_SINK_METHOD_NAMES = new Set([
|
|
|
32445
33010
|
"deserialize"
|
|
32446
33011
|
]);
|
|
32447
33012
|
//#endregion
|
|
32448
|
-
//#region src/plugin/utils/get-call-method-name.ts
|
|
32449
|
-
/**
|
|
32450
|
-
* Returns the static method name of a call's callee when it's a
|
|
32451
|
-
* non-computed MemberExpression (`obj.method` → `"method"`), or
|
|
32452
|
-
* `null` otherwise. Used by `no-pass-data-to-parent` and
|
|
32453
|
-
* `no-pass-live-state-to-parent` to match the receiver-bound
|
|
32454
|
-
* method-call shape against an allow/block list.
|
|
32455
|
-
*/
|
|
32456
|
-
const getCallMethodName = (callee) => {
|
|
32457
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
32458
|
-
return null;
|
|
32459
|
-
};
|
|
32460
|
-
//#endregion
|
|
32461
33013
|
//#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
|
|
32462
33014
|
const isUseStateIdentifier = (identifier) => {
|
|
32463
33015
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
@@ -38448,7 +39000,7 @@ const containsVnodeFactoryCall = (root) => {
|
|
|
38448
39000
|
};
|
|
38449
39001
|
const isComponentLikeFunction = (functionNode) => {
|
|
38450
39002
|
const bindingName = getFunctionBindingName$1(functionNode);
|
|
38451
|
-
if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN$
|
|
39003
|
+
if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN$4.test(bindingName))) return true;
|
|
38452
39004
|
const body = "body" in functionNode ? functionNode.body : null;
|
|
38453
39005
|
if (!body || !isAstNode(body)) return false;
|
|
38454
39006
|
return containsJsxElement(body) || containsVnodeFactoryCall(body);
|
|
@@ -41864,46 +42416,6 @@ const requireRenderReturn = defineRule({
|
|
|
41864
42416
|
}
|
|
41865
42417
|
});
|
|
41866
42418
|
//#endregion
|
|
41867
|
-
//#region src/plugin/rules/state-and-effects/utils/collect-handler-binding-names.ts
|
|
41868
|
-
const collectHandlerBindingNames = (componentBody) => {
|
|
41869
|
-
const handlerNames = /* @__PURE__ */ new Set();
|
|
41870
|
-
walkAst(componentBody, (child) => {
|
|
41871
|
-
if (!isNodeOfType(child, "JSXAttribute")) return;
|
|
41872
|
-
if (!isNodeOfType(child.name, "JSXIdentifier")) return;
|
|
41873
|
-
if (!/^on[A-Z]/.test(child.name.name)) return;
|
|
41874
|
-
if (!isNodeOfType(child.value, "JSXExpressionContainer")) return;
|
|
41875
|
-
const expression = child.value.expression;
|
|
41876
|
-
if (isNodeOfType(expression, "Identifier")) handlerNames.add(expression.name);
|
|
41877
|
-
});
|
|
41878
|
-
return handlerNames;
|
|
41879
|
-
};
|
|
41880
|
-
//#endregion
|
|
41881
|
-
//#region src/plugin/rules/state-and-effects/utils/is-inside-event-handler.ts
|
|
41882
|
-
const isInsideEventHandler = (node, handlerBindingNames) => {
|
|
41883
|
-
let cursor = node.parent ?? null;
|
|
41884
|
-
while (cursor) {
|
|
41885
|
-
if (isNodeOfType(cursor, "ArrowFunctionExpression") || isNodeOfType(cursor, "FunctionExpression") || isNodeOfType(cursor, "FunctionDeclaration")) {
|
|
41886
|
-
let outer = cursor.parent ?? null;
|
|
41887
|
-
while (outer) {
|
|
41888
|
-
if (isNodeOfType(outer, "JSXAttribute")) {
|
|
41889
|
-
const attrName = isNodeOfType(outer.name, "JSXIdentifier") ? outer.name.name : null;
|
|
41890
|
-
if (attrName && /^on[A-Z]/.test(attrName)) return true;
|
|
41891
|
-
return false;
|
|
41892
|
-
}
|
|
41893
|
-
if (isNodeOfType(outer, "VariableDeclarator")) {
|
|
41894
|
-
const declaredName = isNodeOfType(outer.id, "Identifier") ? outer.id.name : null;
|
|
41895
|
-
return Boolean(declaredName && handlerBindingNames.has(declaredName));
|
|
41896
|
-
}
|
|
41897
|
-
if (isNodeOfType(outer, "Program")) return false;
|
|
41898
|
-
outer = outer.parent ?? null;
|
|
41899
|
-
}
|
|
41900
|
-
return false;
|
|
41901
|
-
}
|
|
41902
|
-
cursor = cursor.parent ?? null;
|
|
41903
|
-
}
|
|
41904
|
-
return false;
|
|
41905
|
-
};
|
|
41906
|
-
//#endregion
|
|
41907
42419
|
//#region src/plugin/rules/state-and-effects/rerender-defer-reads-hook.ts
|
|
41908
42420
|
const DEFERRABLE_HOOK_NAMES = new Set([
|
|
41909
42421
|
"useSearchParams",
|
|
@@ -51397,7 +51909,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
51397
51909
|
if (!isNodeOfType(callParent, "CallExpression")) return false;
|
|
51398
51910
|
if (callParent.callee === functionNode) return false;
|
|
51399
51911
|
if (isNodeOfType(callParent.callee, "MemberExpression") && !callParent.callee.computed && isNodeOfType(callParent.callee.property, "Identifier") && PROMISE_CONTINUATION_METHODS.has(callParent.callee.property.name)) return true;
|
|
51400
|
-
return isNodeOfType(callParent.callee, "Identifier") && HOOK_NAME_PATTERN$
|
|
51912
|
+
return isNodeOfType(callParent.callee, "Identifier") && HOOK_NAME_PATTERN$4.test(callParent.callee.name) && !RENDER_SYNCHRONOUS_HOOK_NAMES.has(callParent.callee.name) && callParent.arguments?.[0] === functionNode;
|
|
51401
51913
|
};
|
|
51402
51914
|
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && REACT_HANDLER_PROP_PATTERN.test(node.name.name);
|
|
51403
51915
|
const isEventHandlerNamedProperty = (node) => isNodeOfType(node, "Property") && (isNodeOfType(node.key, "Identifier") && typeof node.key.name === "string" && REACT_HANDLER_PROP_PATTERN.test(node.key.name) || isNodeOfType(node.key, "Literal") && typeof node.key.value === "string" && REACT_HANDLER_PROP_PATTERN.test(node.key.value));
|
|
@@ -51425,11 +51937,11 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
51425
51937
|
if (isNodeOfType(parent, "ReturnStatement")) {
|
|
51426
51938
|
const outerFunction = findEnclosingFunction(parent);
|
|
51427
51939
|
const hookName = outerFunction ? getFunctionBindingName$1(outerFunction) : null;
|
|
51428
|
-
return Boolean(hookName && HOOK_NAME_PATTERN$
|
|
51940
|
+
return Boolean(hookName && HOOK_NAME_PATTERN$4.test(hookName));
|
|
51429
51941
|
}
|
|
51430
51942
|
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === functionNode) {
|
|
51431
51943
|
const hookName = getFunctionBindingName$1(parent);
|
|
51432
|
-
return Boolean(hookName && HOOK_NAME_PATTERN$
|
|
51944
|
+
return Boolean(hookName && HOOK_NAME_PATTERN$4.test(hookName));
|
|
51433
51945
|
}
|
|
51434
51946
|
return false;
|
|
51435
51947
|
};
|