oxlint-plugin-react-doctor 0.8.1-dev.7818922 → 0.8.1-dev.7c9dbed
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -48
- package/dist/index.js +2488 -687
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -762,7 +762,7 @@ const EXTERNAL_SYNC_MEMBER_METHOD_NAMES = new Set([
|
|
|
762
762
|
"put",
|
|
763
763
|
"patch"
|
|
764
764
|
]);
|
|
765
|
-
|
|
765
|
+
new Set([
|
|
766
766
|
...FETCH_MEMBER_OBJECTS,
|
|
767
767
|
"api",
|
|
768
768
|
"client",
|
|
@@ -2622,7 +2622,7 @@ const PREFER_ALT = "Screen readers skip a decorative image more reliably with `a
|
|
|
2622
2622
|
const MESSAGE_OBJECT = "Screen reader users cannot use this `<object>` because assistive tech cannot describe it, so add `alt`, `aria-label`, `aria-labelledby`, `title`, or inner fallback text.";
|
|
2623
2623
|
const MESSAGE_AREA = "Blind users can't use this `<area>` of the image map because screen readers can't describe it, so add `alt`, `aria-label`, or `aria-labelledby`.";
|
|
2624
2624
|
const MESSAGE_INPUT_IMAGE = "Blind users can't use this image button because screen readers can't describe it, so add `alt`, `aria-label`, or `aria-labelledby`.";
|
|
2625
|
-
const resolveSettings$
|
|
2625
|
+
const resolveSettings$52 = (settings) => {
|
|
2626
2626
|
const reactDoctor = settings?.["react-doctor"];
|
|
2627
2627
|
return typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.altText ?? {} : {};
|
|
2628
2628
|
};
|
|
@@ -2745,7 +2745,7 @@ const altText = defineRule({
|
|
|
2745
2745
|
category: "Accessibility",
|
|
2746
2746
|
create: (context) => {
|
|
2747
2747
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
2748
|
-
const settings = resolveSettings$
|
|
2748
|
+
const settings = resolveSettings$52(context.settings);
|
|
2749
2749
|
const checkImg = !settings.elements || settings.elements.includes("img");
|
|
2750
2750
|
const checkObject = !settings.elements || settings.elements.includes("object");
|
|
2751
2751
|
const checkArea = !settings.elements || settings.elements.includes("area");
|
|
@@ -2807,7 +2807,7 @@ const DEFAULT_AMBIGUOUS = [
|
|
|
2807
2807
|
"a link",
|
|
2808
2808
|
"learn more"
|
|
2809
2809
|
];
|
|
2810
|
-
const resolveSettings$
|
|
2810
|
+
const resolveSettings$51 = (settings) => {
|
|
2811
2811
|
const reactDoctor = settings?.["react-doctor"];
|
|
2812
2812
|
return { words: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorAmbiguousText ?? {} : {}).words ?? DEFAULT_AMBIGUOUS };
|
|
2813
2813
|
};
|
|
@@ -2857,7 +2857,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
2857
2857
|
category: "Accessibility",
|
|
2858
2858
|
create: (context) => {
|
|
2859
2859
|
if (isTestlikeFilename(context.filename)) return {};
|
|
2860
|
-
const settings = resolveSettings$
|
|
2860
|
+
const settings = resolveSettings$51(context.settings);
|
|
2861
2861
|
const ambiguousSet = new Set(settings.words.map((word) => word.toLowerCase()));
|
|
2862
2862
|
return { JSXElement(node) {
|
|
2863
2863
|
if (getElementType(node.openingElement, context.settings) !== "a") return;
|
|
@@ -3225,7 +3225,7 @@ const anchorHasContent = defineRule({
|
|
|
3225
3225
|
const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
|
|
3226
3226
|
const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
|
|
3227
3227
|
const MESSAGE_CANT_BE_ANCHOR = "Keyboard users can't trigger this link because it's a click handler with no real `href`, so use `<button>` instead.";
|
|
3228
|
-
const resolveSettings$
|
|
3228
|
+
const resolveSettings$50 = (settings) => {
|
|
3229
3229
|
const reactDoctor = settings?.["react-doctor"];
|
|
3230
3230
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorIsValid ?? {} : {};
|
|
3231
3231
|
const jsxA11y = settings?.["jsx-a11y"];
|
|
@@ -3268,7 +3268,7 @@ const anchorIsValid = defineRule({
|
|
|
3268
3268
|
recommendation: "Give links a real destination. Use `<button>` for in-page actions.",
|
|
3269
3269
|
category: "Accessibility",
|
|
3270
3270
|
create: (context) => {
|
|
3271
|
-
const settings = resolveSettings$
|
|
3271
|
+
const settings = resolveSettings$50(context.settings);
|
|
3272
3272
|
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
3273
3273
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3274
3274
|
return { JSXOpeningElement(node) {
|
|
@@ -4181,7 +4181,7 @@ const ariaProptypes = defineRule({
|
|
|
4181
4181
|
//#endregion
|
|
4182
4182
|
//#region src/plugin/rules/a11y/aria-role.ts
|
|
4183
4183
|
const buildBaseMessage = (suffix) => `This \`role\` is not a valid ARIA role, so assistive tech cannot expose it correctly. Use a real, non-abstract role.${suffix}`;
|
|
4184
|
-
const resolveSettings$
|
|
4184
|
+
const resolveSettings$49 = (settings) => {
|
|
4185
4185
|
const reactDoctor = settings?.["react-doctor"];
|
|
4186
4186
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.ariaRole ?? {} : {};
|
|
4187
4187
|
return {
|
|
@@ -4197,7 +4197,7 @@ const ariaRole = defineRule({
|
|
|
4197
4197
|
recommendation: "Use a real, non-abstract ARIA role so assistive tech can expose the element correctly.",
|
|
4198
4198
|
category: "Accessibility",
|
|
4199
4199
|
create: (context) => {
|
|
4200
|
-
const settings = resolveSettings$
|
|
4200
|
+
const settings = resolveSettings$49(context.settings);
|
|
4201
4201
|
return { JSXOpeningElement(node) {
|
|
4202
4202
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
4203
4203
|
if (!roleAttribute) return;
|
|
@@ -7115,7 +7115,7 @@ const FORM_CONTROL_TAGS = new Set([
|
|
|
7115
7115
|
"select",
|
|
7116
7116
|
"form"
|
|
7117
7117
|
]);
|
|
7118
|
-
const resolveSettings$
|
|
7118
|
+
const resolveSettings$48 = (settings) => {
|
|
7119
7119
|
const reactDoctor = settings?.["react-doctor"];
|
|
7120
7120
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {};
|
|
7121
7121
|
return { inputComponents: new Set(ruleSettings.inputComponents ?? []) };
|
|
@@ -7128,7 +7128,7 @@ const autocompleteValid = defineRule({
|
|
|
7128
7128
|
recommendation: "Use a valid autofill token in `autoComplete` so browsers can fill the right field reliably.",
|
|
7129
7129
|
category: "Accessibility",
|
|
7130
7130
|
create: skipNonProductionFiles((context) => {
|
|
7131
|
-
const settings = resolveSettings$
|
|
7131
|
+
const settings = resolveSettings$48(context.settings);
|
|
7132
7132
|
return { JSXOpeningElement: (node) => {
|
|
7133
7133
|
const tag = getElementType(node, context.settings);
|
|
7134
7134
|
if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.has(tag)) return;
|
|
@@ -7247,7 +7247,7 @@ const isCreateElementCall = (node) => {
|
|
|
7247
7247
|
//#region src/plugin/rules/react-builtins/button-has-type.ts
|
|
7248
7248
|
const MISSING_MESSAGE$2 = "Your users can submit the form by accident because a `<button>` with no `type` defaults to submit.";
|
|
7249
7249
|
const INVALID_MESSAGE = "This button has an invalid `type`, so the browser may treat it like a submit button.";
|
|
7250
|
-
const resolveSettings$
|
|
7250
|
+
const resolveSettings$47 = (settings) => {
|
|
7251
7251
|
const reactDoctor = settings?.["react-doctor"];
|
|
7252
7252
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.buttonHasType ?? {} : {};
|
|
7253
7253
|
return {
|
|
@@ -7485,7 +7485,7 @@ const buttonHasType = defineRule({
|
|
|
7485
7485
|
severity: "warn",
|
|
7486
7486
|
recommendation: "Set an explicit button `type` so plain buttons do not submit forms by accident: `type=\"button\"`, `\"submit\"`, or `\"reset\"`.",
|
|
7487
7487
|
create: (context) => {
|
|
7488
|
-
const settings = resolveSettings$
|
|
7488
|
+
const settings = resolveSettings$47(context.settings);
|
|
7489
7489
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
7490
7490
|
return {
|
|
7491
7491
|
JSXOpeningElement(node) {
|
|
@@ -7566,7 +7566,7 @@ const buttonHasType = defineRule({
|
|
|
7566
7566
|
//#region src/plugin/rules/react-builtins/checked-requires-onchange-or-readonly.ts
|
|
7567
7567
|
const MISSING_MESSAGE$1 = "Your users can't toggle this input because `checked` has no `onChange`.";
|
|
7568
7568
|
const EXCLUSIVE_MESSAGE = "This input mixes `checked` with `defaultChecked`, so React can't tell whether it is controlled or uncontrolled.";
|
|
7569
|
-
const resolveSettings$
|
|
7569
|
+
const resolveSettings$46 = (settings) => {
|
|
7570
7570
|
const reactDoctor = settings?.["react-doctor"];
|
|
7571
7571
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.checkedRequiresOnchangeOrReadonly ?? {} : {};
|
|
7572
7572
|
return {
|
|
@@ -7667,7 +7667,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
7667
7667
|
recommendation: "Add `onChange`, `readOnly`, or `defaultChecked` so React knows whether the checkbox is editable, display-only, or uncontrolled.",
|
|
7668
7668
|
category: "Correctness",
|
|
7669
7669
|
create: (context) => {
|
|
7670
|
-
const settings = resolveSettings$
|
|
7670
|
+
const settings = resolveSettings$46(context.settings);
|
|
7671
7671
|
const reportFromPresence = (presence) => {
|
|
7672
7672
|
if (presence.checkedNode && presence.defaultCheckedNode && !presence.bothCheckedAndDefaultForwarded && !settings.ignoreExclusiveCheckedAttribute) context.report({
|
|
7673
7673
|
node: presence.checkedNode,
|
|
@@ -9008,6 +9008,16 @@ const hasVisibleBindingNamed = (node, bindingName, scopes) => {
|
|
|
9008
9008
|
}
|
|
9009
9009
|
};
|
|
9010
9010
|
//#endregion
|
|
9011
|
+
//#region src/plugin/utils/is-global-match-media-call.ts
|
|
9012
|
+
const isGlobalMatchMediaCall = (node, scopes) => {
|
|
9013
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
9014
|
+
const callee = stripParenExpression(node.callee);
|
|
9015
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name === "matchMedia" && scopes.isGlobalReference(callee);
|
|
9016
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "matchMedia") return false;
|
|
9017
|
+
const receiver = stripParenExpression(callee.object);
|
|
9018
|
+
return isNodeOfType(receiver, "Identifier") && (receiver.name === "window" || receiver.name === "globalThis") && scopes.isGlobalReference(receiver);
|
|
9019
|
+
};
|
|
9020
|
+
//#endregion
|
|
9011
9021
|
//#region src/plugin/utils/is-proven-browser-api-receiver.ts
|
|
9012
9022
|
const DOM_EVENT_TARGET_TYPE_NAMES = new Set([
|
|
9013
9023
|
"AbortSignal",
|
|
@@ -9154,8 +9164,10 @@ const getProvenDomEventTargetPrototypeOwnerNames = (rawExpression, scopes, visit
|
|
|
9154
9164
|
if (memberName === "body" || memberName === "documentElement") return getDomPrototypeOwnerNamesForType("HTMLElement");
|
|
9155
9165
|
}
|
|
9156
9166
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
9167
|
+
if (isGlobalMatchMediaCall(expression, scopes)) return getDomPrototypeOwnerNamesForType("MediaQueryList");
|
|
9157
9168
|
const callee = stripParenExpression(expression.callee);
|
|
9158
9169
|
const methodName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
|
|
9170
|
+
if (methodName === "matchMedia" && isNodeOfType(callee, "MemberExpression") && getProvenDomEventTargetPrototypeOwnerNames(callee.object, scopes, visitedSymbolIds).includes("Window")) return getDomPrototypeOwnerNamesForType("MediaQueryList");
|
|
9159
9171
|
if (methodName === "createElement") return getDomPrototypeOwnerNamesForType("HTMLElement");
|
|
9160
9172
|
if (methodName === "createElementNS") return getDomPrototypeOwnerNamesForType("SVGElement");
|
|
9161
9173
|
if (methodName && DOM_EVENT_TARGET_FACTORY_METHOD_NAMES.has(methodName)) return getDomPrototypeOwnerNamesForType("Element");
|
|
@@ -10368,7 +10380,7 @@ const WRAPPER_LABEL_PROP = "label";
|
|
|
10368
10380
|
const SELECT_ELEMENT = "select";
|
|
10369
10381
|
const DEFAULT_DEPTH = 5;
|
|
10370
10382
|
const MAX_DEPTH = 25;
|
|
10371
|
-
const resolveSettings$
|
|
10383
|
+
const resolveSettings$45 = (settings) => {
|
|
10372
10384
|
const reactDoctor = settings?.["react-doctor"];
|
|
10373
10385
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.controlHasAssociatedLabel ?? {} : {};
|
|
10374
10386
|
return {
|
|
@@ -10701,7 +10713,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
10701
10713
|
recommendation: "Give every interactive control a label screen readers can read.",
|
|
10702
10714
|
category: "Accessibility",
|
|
10703
10715
|
create: (context) => {
|
|
10704
|
-
const settings = resolveSettings$
|
|
10716
|
+
const settings = resolveSettings$45(context.settings);
|
|
10705
10717
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10706
10718
|
const iconComponentNames = /* @__PURE__ */ new Set();
|
|
10707
10719
|
const iconNamespaceNames = /* @__PURE__ */ new Set();
|
|
@@ -12653,7 +12665,7 @@ const DEFAULT_ADDITIONAL_HOCS = [
|
|
|
12653
12665
|
"lazy",
|
|
12654
12666
|
"withTracking"
|
|
12655
12667
|
];
|
|
12656
|
-
const resolveSettings$
|
|
12668
|
+
const resolveSettings$44 = (settings) => {
|
|
12657
12669
|
const reactDoctor = settings?.["react-doctor"];
|
|
12658
12670
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.displayName ?? {} : {};
|
|
12659
12671
|
const additionalHoCs = new Set(ruleSettings.additionalHoCs ?? DEFAULT_ADDITIONAL_HOCS);
|
|
@@ -12835,7 +12847,7 @@ const displayName = defineRule({
|
|
|
12835
12847
|
recommendation: "Give each component a `displayName` so DevTools shows a clear name.",
|
|
12836
12848
|
category: "Architecture",
|
|
12837
12849
|
create: (context) => {
|
|
12838
|
-
const settings = resolveSettings$
|
|
12850
|
+
const settings = resolveSettings$44(context.settings);
|
|
12839
12851
|
const ignoreNamed = !settings.ignoreTranspilerName;
|
|
12840
12852
|
const reportAt = (node) => {
|
|
12841
12853
|
context.report({
|
|
@@ -13815,6 +13827,116 @@ const findRenderPhaseComponentOrHook = (node, scopes) => {
|
|
|
13815
13827
|
return null;
|
|
13816
13828
|
};
|
|
13817
13829
|
//#endregion
|
|
13830
|
+
//#region src/plugin/utils/get-final-sequence-expression-value.ts
|
|
13831
|
+
const getFinalSequenceExpressionValue = (expression) => {
|
|
13832
|
+
let finalExpression = stripParenExpression(expression);
|
|
13833
|
+
while (isNodeOfType(finalExpression, "SequenceExpression")) {
|
|
13834
|
+
const sequenceResult = finalExpression.expressions.at(-1);
|
|
13835
|
+
if (!sequenceResult) break;
|
|
13836
|
+
finalExpression = stripParenExpression(sequenceResult);
|
|
13837
|
+
}
|
|
13838
|
+
return finalExpression;
|
|
13839
|
+
};
|
|
13840
|
+
//#endregion
|
|
13841
|
+
//#region src/plugin/utils/collect-expression-path-coverage-nodes.ts
|
|
13842
|
+
const collectExpressionPathCoverageNodes = (owner, matchingNodes, context, expressionBoundary = owner) => {
|
|
13843
|
+
const coverageNodes = /* @__PURE__ */ new Set();
|
|
13844
|
+
const pendingNodes = matchingNodes.filter((matchingNode) => {
|
|
13845
|
+
if (context.cfg.enclosingFunction(matchingNode) !== owner) return false;
|
|
13846
|
+
let currentNode = matchingNode;
|
|
13847
|
+
while (currentNode && currentNode !== expressionBoundary) currentNode = currentNode.parent ?? null;
|
|
13848
|
+
return currentNode === expressionBoundary;
|
|
13849
|
+
});
|
|
13850
|
+
const visitedNodes = new Set(pendingNodes);
|
|
13851
|
+
const coveredBranchesByConditional = /* @__PURE__ */ new Map();
|
|
13852
|
+
while (pendingNodes.length > 0) {
|
|
13853
|
+
const coverageCandidate = pendingNodes.pop();
|
|
13854
|
+
if (!coverageCandidate) break;
|
|
13855
|
+
if (coverageCandidate === expressionBoundary) {
|
|
13856
|
+
coverageNodes.add(coverageCandidate);
|
|
13857
|
+
continue;
|
|
13858
|
+
}
|
|
13859
|
+
let currentChild = coverageCandidate;
|
|
13860
|
+
let currentParent = currentChild.parent ?? null;
|
|
13861
|
+
let conditionalExpression = null;
|
|
13862
|
+
let conditionalBranch = null;
|
|
13863
|
+
let isBlockedByNonExhaustiveExpression = false;
|
|
13864
|
+
while (currentParent && currentParent !== expressionBoundary) {
|
|
13865
|
+
if (isNodeOfType(currentParent, "ConditionalExpression")) {
|
|
13866
|
+
const isConsequent = currentParent.consequent === currentChild;
|
|
13867
|
+
const isAlternate = currentParent.alternate === currentChild;
|
|
13868
|
+
if (isConsequent || isAlternate) {
|
|
13869
|
+
const staticTestValue = readStaticBoolean(getFinalSequenceExpressionValue(currentParent.test));
|
|
13870
|
+
if (staticTestValue !== null) {
|
|
13871
|
+
if (staticTestValue !== isConsequent) {
|
|
13872
|
+
isBlockedByNonExhaustiveExpression = true;
|
|
13873
|
+
break;
|
|
13874
|
+
}
|
|
13875
|
+
currentChild = currentParent;
|
|
13876
|
+
currentParent = currentChild.parent ?? null;
|
|
13877
|
+
continue;
|
|
13878
|
+
}
|
|
13879
|
+
conditionalExpression = currentParent;
|
|
13880
|
+
conditionalBranch = isConsequent ? "consequent" : "alternate";
|
|
13881
|
+
break;
|
|
13882
|
+
}
|
|
13883
|
+
}
|
|
13884
|
+
if (isNodeOfType(currentParent, "LogicalExpression") && currentParent.right === currentChild) {
|
|
13885
|
+
const staticLeftValue = readStaticBoolean(getFinalSequenceExpressionValue(currentParent.left));
|
|
13886
|
+
if (!(currentParent.operator === "&&" && staticLeftValue === true || currentParent.operator === "||" && staticLeftValue === false)) {
|
|
13887
|
+
isBlockedByNonExhaustiveExpression = true;
|
|
13888
|
+
break;
|
|
13889
|
+
}
|
|
13890
|
+
}
|
|
13891
|
+
if (isNodeOfType(currentParent, "AssignmentPattern") && currentParent.right === currentChild) {
|
|
13892
|
+
isBlockedByNonExhaustiveExpression = true;
|
|
13893
|
+
break;
|
|
13894
|
+
}
|
|
13895
|
+
currentChild = currentParent;
|
|
13896
|
+
currentParent = currentChild.parent ?? null;
|
|
13897
|
+
}
|
|
13898
|
+
if (isBlockedByNonExhaustiveExpression) continue;
|
|
13899
|
+
if (!conditionalExpression || !conditionalBranch) {
|
|
13900
|
+
coverageNodes.add(coverageCandidate);
|
|
13901
|
+
continue;
|
|
13902
|
+
}
|
|
13903
|
+
const coveredBranches = coveredBranchesByConditional.get(conditionalExpression) ?? /* @__PURE__ */ new Set();
|
|
13904
|
+
coveredBranches.add(conditionalBranch);
|
|
13905
|
+
coveredBranchesByConditional.set(conditionalExpression, coveredBranches);
|
|
13906
|
+
if (coveredBranches.has("alternate") && coveredBranches.has("consequent") && !visitedNodes.has(conditionalExpression)) {
|
|
13907
|
+
visitedNodes.add(conditionalExpression);
|
|
13908
|
+
pendingNodes.push(conditionalExpression);
|
|
13909
|
+
}
|
|
13910
|
+
}
|
|
13911
|
+
return coverageNodes;
|
|
13912
|
+
};
|
|
13913
|
+
//#endregion
|
|
13914
|
+
//#region src/plugin/utils/do-nodes-cover-every-path-from-function-entry.ts
|
|
13915
|
+
const doNodesCoverEveryPathFromFunctionEntry = (owner, matchingNodes, context, options = {}) => {
|
|
13916
|
+
const functionCfg = context.cfg.cfgFor(owner);
|
|
13917
|
+
if (!functionCfg) return false;
|
|
13918
|
+
const matchingBlocks = new Set([...collectExpressionPathCoverageNodes(owner, matchingNodes, context)].flatMap((matchingNode) => {
|
|
13919
|
+
const matchingBlock = functionCfg.blockOf(matchingNode);
|
|
13920
|
+
return matchingBlock ? [matchingBlock] : [];
|
|
13921
|
+
}));
|
|
13922
|
+
if (matchingBlocks.size === 0) return false;
|
|
13923
|
+
const visitedBlocks = new Set([functionCfg.entry]);
|
|
13924
|
+
const pendingBlocks = [functionCfg.entry];
|
|
13925
|
+
while (pendingBlocks.length > 0) {
|
|
13926
|
+
const currentBlock = pendingBlocks.pop();
|
|
13927
|
+
if (!currentBlock) break;
|
|
13928
|
+
if (matchingBlocks.has(currentBlock)) continue;
|
|
13929
|
+
for (const edge of currentBlock.successors) {
|
|
13930
|
+
if (options.ignoreThrowEdges && edge.kind === "throw") continue;
|
|
13931
|
+
if (edge.to === functionCfg.exit) return false;
|
|
13932
|
+
if (visitedBlocks.has(edge.to)) continue;
|
|
13933
|
+
visitedBlocks.add(edge.to);
|
|
13934
|
+
pendingBlocks.push(edge.to);
|
|
13935
|
+
}
|
|
13936
|
+
}
|
|
13937
|
+
return true;
|
|
13938
|
+
};
|
|
13939
|
+
//#endregion
|
|
13818
13940
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
13819
13941
|
const getFunctionBindingIdentifier$1 = (functionNode) => {
|
|
13820
13942
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -13852,14 +13974,19 @@ const isAstDescendant = (inner, outer) => {
|
|
|
13852
13974
|
};
|
|
13853
13975
|
//#endregion
|
|
13854
13976
|
//#region src/plugin/utils/react-ref-origin.ts
|
|
13855
|
-
const
|
|
13977
|
+
const REACT_REF_API_NAMES = new Set(["createRef", "useRef"]);
|
|
13978
|
+
const resolveReactRefSymbol = (memberExpression, scopes, options = {}) => {
|
|
13856
13979
|
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
13857
13980
|
if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticPropertyName(memberExpression) !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
13858
13981
|
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
13859
13982
|
if (!symbol?.initializer) return null;
|
|
13860
13983
|
const initializer = stripParenExpression(symbol.initializer);
|
|
13861
13984
|
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
13862
|
-
return isReactApiCall(initializer, "useRef", scopes, {
|
|
13985
|
+
return isReactApiCall(initializer, options.includeCreateRef ? REACT_REF_API_NAMES : "useRef", scopes, {
|
|
13986
|
+
allowGlobalReactNamespace: true,
|
|
13987
|
+
allowUnboundBareCalls: options.allowUnboundBareCalls,
|
|
13988
|
+
resolveNamedAliases: options.resolveNamedAliases
|
|
13989
|
+
}) ? symbol : null;
|
|
13863
13990
|
};
|
|
13864
13991
|
const resolveReactRefCurrentOriginSymbol = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
13865
13992
|
const expression = stripParenExpression(node);
|
|
@@ -13958,9 +14085,35 @@ const isNodeReachableWithinFunction = (node, context) => {
|
|
|
13958
14085
|
return false;
|
|
13959
14086
|
};
|
|
13960
14087
|
//#endregion
|
|
14088
|
+
//#region src/plugin/utils/is-within-assignment-target.ts
|
|
14089
|
+
const isWithinAssignmentTarget = (identifier) => {
|
|
14090
|
+
let currentNode = identifier;
|
|
14091
|
+
let parentNode = currentNode.parent;
|
|
14092
|
+
while (parentNode) {
|
|
14093
|
+
if (isNodeOfType(parentNode, "AssignmentExpression")) return parentNode.left === currentNode;
|
|
14094
|
+
if (isNodeOfType(parentNode, "UpdateExpression") || isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "delete") return parentNode.argument === currentNode;
|
|
14095
|
+
if (isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return parentNode.left === currentNode;
|
|
14096
|
+
currentNode = parentNode;
|
|
14097
|
+
parentNode = currentNode.parent;
|
|
14098
|
+
}
|
|
14099
|
+
return false;
|
|
14100
|
+
};
|
|
14101
|
+
//#endregion
|
|
13961
14102
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
13962
14103
|
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
13963
14104
|
const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
|
|
14105
|
+
const REPLAY_ENTRY_DROPPING_ARRAY_METHOD_NAMES = new Set([
|
|
14106
|
+
"pop",
|
|
14107
|
+
"shift",
|
|
14108
|
+
"splice",
|
|
14109
|
+
"fill",
|
|
14110
|
+
"copyWithin"
|
|
14111
|
+
]);
|
|
14112
|
+
const REPLAY_ENTRY_DROPPING_COLLECTION_METHOD_NAMES = new Set([
|
|
14113
|
+
"clear",
|
|
14114
|
+
"delete",
|
|
14115
|
+
"set"
|
|
14116
|
+
]);
|
|
13964
14117
|
const REACT_REF_EFFECT_ANALYSIS_CACHE = /* @__PURE__ */ new WeakMap();
|
|
13965
14118
|
const RESOURCE_NOUN_BY_KIND = {
|
|
13966
14119
|
subscribe: "subscription",
|
|
@@ -13994,13 +14147,111 @@ const resolveExpressionKey = (expression, context, visitedSymbolIds = /* @__PURE
|
|
|
13994
14147
|
return objectKey ? `${objectKey}.${unwrappedExpression.property.name}` : null;
|
|
13995
14148
|
}
|
|
13996
14149
|
if (isNodeOfType(unwrappedExpression, "ThisExpression")) return "this";
|
|
13997
|
-
if (isNodeOfType(unwrappedExpression, "Literal") && (typeof unwrappedExpression.value === "string" || typeof unwrappedExpression.value === "number")) return `literal:${String(unwrappedExpression.value)}`;
|
|
14150
|
+
if (isNodeOfType(unwrappedExpression, "Literal") && (typeof unwrappedExpression.value === "string" || typeof unwrappedExpression.value === "number" || typeof unwrappedExpression.value === "boolean")) return `literal:${String(unwrappedExpression.value)}`;
|
|
13998
14151
|
if (isFunctionLike$1(unwrappedExpression)) {
|
|
13999
14152
|
const rangeStart = getRangeStart(unwrappedExpression);
|
|
14000
14153
|
return rangeStart === null ? null : `function:${rangeStart}`;
|
|
14001
14154
|
}
|
|
14002
14155
|
return null;
|
|
14003
14156
|
};
|
|
14157
|
+
const resolveForEachProjection = (expression, context) => {
|
|
14158
|
+
if (!expression) return null;
|
|
14159
|
+
let currentExpression = stripParenExpression(expression);
|
|
14160
|
+
const memberNames = [];
|
|
14161
|
+
while (isNodeOfType(currentExpression, "MemberExpression") && !currentExpression.computed) {
|
|
14162
|
+
if (!isNodeOfType(currentExpression.property, "Identifier")) return null;
|
|
14163
|
+
memberNames.unshift(currentExpression.property.name);
|
|
14164
|
+
currentExpression = stripParenExpression(currentExpression.object);
|
|
14165
|
+
}
|
|
14166
|
+
if (!isNodeOfType(currentExpression, "Identifier")) return null;
|
|
14167
|
+
const symbol = context.scopes.symbolFor(currentExpression);
|
|
14168
|
+
if (!symbol || symbol.kind !== "parameter") return null;
|
|
14169
|
+
let callbackNode = symbol.bindingIdentifier.parent;
|
|
14170
|
+
while (callbackNode && !isFunctionLike$1(callbackNode)) callbackNode = callbackNode.parent;
|
|
14171
|
+
if (!callbackNode || !isFunctionLike$1(callbackNode)) return null;
|
|
14172
|
+
const forEachCall = findEnclosingForEachCall(callbackNode);
|
|
14173
|
+
if (!forEachCall) return null;
|
|
14174
|
+
const forEachCallee = stripParenExpression(forEachCall.callee);
|
|
14175
|
+
if (!isNodeOfType(forEachCallee, "MemberExpression")) return null;
|
|
14176
|
+
const collectionKey = resolveExpressionKey(forEachCallee.object, context);
|
|
14177
|
+
if (!collectionKey) return null;
|
|
14178
|
+
const firstParameter = callbackNode.params[0];
|
|
14179
|
+
const assignmentPattern = isNodeOfType(symbol.bindingIdentifier.parent, "AssignmentPattern") && symbol.bindingIdentifier.parent.left === symbol.bindingIdentifier ? symbol.bindingIdentifier.parent : null;
|
|
14180
|
+
const propertyName = isNodeOfType(firstParameter, "ObjectPattern") ? getDestructuredBindingPropertyName(symbol.bindingIdentifier) : null;
|
|
14181
|
+
const defaultValueKey = assignmentPattern ? resolveExpressionKey(assignmentPattern.right, context) : null;
|
|
14182
|
+
if (assignmentPattern && !defaultValueKey) return null;
|
|
14183
|
+
const parameterProjection = firstParameter === symbol.bindingIdentifier ? "value" : propertyName && defaultValueKey ? `${propertyName}=default:${defaultValueKey}` : propertyName;
|
|
14184
|
+
if (!parameterProjection) return null;
|
|
14185
|
+
return {
|
|
14186
|
+
collectionKey,
|
|
14187
|
+
projectionKey: [parameterProjection, ...memberNames].join(".")
|
|
14188
|
+
};
|
|
14189
|
+
};
|
|
14190
|
+
const resolveForEachProjectionKey = (expression, context) => {
|
|
14191
|
+
const projection = resolveForEachProjection(expression, context);
|
|
14192
|
+
return projection ? `forEach:${projection.collectionKey}:${projection.projectionKey}` : null;
|
|
14193
|
+
};
|
|
14194
|
+
const resolveResourceIdentityKey = (expression, context) => resolveForEachProjectionKey(expression, context) ?? resolveExpressionKey(expression, context);
|
|
14195
|
+
const resolveEventListenerCaptureValueIdentityKey = (expression, context) => {
|
|
14196
|
+
if (!expression) return null;
|
|
14197
|
+
const directIdentityKey = resolveResourceIdentityKey(expression, context);
|
|
14198
|
+
if (directIdentityKey) return directIdentityKey;
|
|
14199
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
14200
|
+
if (!isNodeOfType(unwrappedExpression, "BinaryExpression") && !isNodeOfType(unwrappedExpression, "LogicalExpression")) return null;
|
|
14201
|
+
const leftIdentityKey = resolveEventListenerCaptureValueIdentityKey(unwrappedExpression.left, context);
|
|
14202
|
+
const rightIdentityKey = resolveEventListenerCaptureValueIdentityKey(unwrappedExpression.right, context);
|
|
14203
|
+
return leftIdentityKey && rightIdentityKey ? `${unwrappedExpression.type}:${unwrappedExpression.operator}:${leftIdentityKey}:${rightIdentityKey}` : null;
|
|
14204
|
+
};
|
|
14205
|
+
const resolveEventListenerCaptureIdentityKey = (optionsNode, context, allowOpaqueOptionsIdentity) => {
|
|
14206
|
+
const capture = resolveEventListenerCapture(optionsNode, { allowIndeterminateEntries: true });
|
|
14207
|
+
if (capture !== null) return `capture:${String(capture)}`;
|
|
14208
|
+
if (!optionsNode) return null;
|
|
14209
|
+
const unwrappedOptions = stripParenExpression(optionsNode);
|
|
14210
|
+
if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) {
|
|
14211
|
+
const optionsKey = allowOpaqueOptionsIdentity ? resolveEventListenerCaptureValueIdentityKey(unwrappedOptions, context) : null;
|
|
14212
|
+
return optionsKey ? `options:${optionsKey}` : null;
|
|
14213
|
+
}
|
|
14214
|
+
let captureKey = "capture:false";
|
|
14215
|
+
for (const property of unwrappedOptions.properties ?? []) {
|
|
14216
|
+
if (!isNodeOfType(property, "Property")) {
|
|
14217
|
+
captureKey = null;
|
|
14218
|
+
continue;
|
|
14219
|
+
}
|
|
14220
|
+
const propertyName = getStaticPropertyKeyName(property);
|
|
14221
|
+
if (propertyName === null || !property.computed && propertyName === "__proto__") {
|
|
14222
|
+
captureKey = null;
|
|
14223
|
+
continue;
|
|
14224
|
+
}
|
|
14225
|
+
if (propertyName === "capture") {
|
|
14226
|
+
const propertyValueKey = resolveEventListenerCaptureValueIdentityKey(property.value, context);
|
|
14227
|
+
captureKey = propertyValueKey ? `capture-value:${propertyValueKey}` : null;
|
|
14228
|
+
}
|
|
14229
|
+
}
|
|
14230
|
+
return captureKey;
|
|
14231
|
+
};
|
|
14232
|
+
const resolveEventListenerCaptureProjection = (optionsNode, context) => {
|
|
14233
|
+
if (!optionsNode) return null;
|
|
14234
|
+
const unwrappedOptions = stripParenExpression(optionsNode);
|
|
14235
|
+
if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) return resolveForEachProjection(unwrappedOptions, context);
|
|
14236
|
+
let captureProjection = null;
|
|
14237
|
+
for (const property of unwrappedOptions.properties ?? []) {
|
|
14238
|
+
if (!isNodeOfType(property, "Property")) {
|
|
14239
|
+
captureProjection = null;
|
|
14240
|
+
continue;
|
|
14241
|
+
}
|
|
14242
|
+
const propertyName = getStaticPropertyKeyName(property);
|
|
14243
|
+
if (propertyName === null || !property.computed && propertyName === "__proto__") {
|
|
14244
|
+
captureProjection = null;
|
|
14245
|
+
continue;
|
|
14246
|
+
}
|
|
14247
|
+
if (propertyName === "capture") captureProjection = resolveForEachProjection(property.value, context);
|
|
14248
|
+
}
|
|
14249
|
+
return captureProjection;
|
|
14250
|
+
};
|
|
14251
|
+
const doEventListenerCapturesMatch = (registrationOptions, releaseOptions, context, allowOpaqueOptionsIdentity = false) => {
|
|
14252
|
+
const registrationCaptureKey = resolveEventListenerCaptureIdentityKey(registrationOptions, context, allowOpaqueOptionsIdentity);
|
|
14253
|
+
return registrationCaptureKey !== null && registrationCaptureKey === resolveEventListenerCaptureIdentityKey(releaseOptions, context, allowOpaqueOptionsIdentity);
|
|
14254
|
+
};
|
|
14004
14255
|
const findAssignedResourceKey = (resourceNode, context) => {
|
|
14005
14256
|
let currentNode = resourceNode;
|
|
14006
14257
|
let parentNode = currentNode.parent;
|
|
@@ -14012,6 +14263,30 @@ const findAssignedResourceKey = (resourceNode, context) => {
|
|
|
14012
14263
|
if (isNodeOfType(parentNode, "AssignmentExpression") && parentNode.right === currentNode) return resolveExpressionKey(parentNode.left, context);
|
|
14013
14264
|
return null;
|
|
14014
14265
|
};
|
|
14266
|
+
const resolveStableMediaQueryListenerIdentityKey = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
14267
|
+
if (!expression) return null;
|
|
14268
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
14269
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
14270
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
14271
|
+
if (!symbol || visitedSymbolIds.has(symbol.id) || !symbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier))) return null;
|
|
14272
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
14273
|
+
if (!initializer) return `symbol:${symbol.id}`;
|
|
14274
|
+
const unwrappedInitializer = stripParenExpression(initializer);
|
|
14275
|
+
if (!isNodeOfType(unwrappedInitializer, "Identifier")) return `symbol:${symbol.id}`;
|
|
14276
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
14277
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
14278
|
+
return resolveStableMediaQueryListenerIdentityKey(unwrappedInitializer, context, nextVisitedSymbolIds) ?? `symbol:${symbol.id}`;
|
|
14279
|
+
}
|
|
14280
|
+
if (isFunctionLike$1(unwrappedExpression)) {
|
|
14281
|
+
const rangeStart = getRangeStart(unwrappedExpression);
|
|
14282
|
+
return rangeStart === null ? null : `function:${rangeStart}`;
|
|
14283
|
+
}
|
|
14284
|
+
return null;
|
|
14285
|
+
};
|
|
14286
|
+
const isProvenLegacyMediaQueryListMethodCall = (callNode, methodName, context) => {
|
|
14287
|
+
const callee = stripParenExpression(callNode.callee);
|
|
14288
|
+
return callNode.arguments?.length === 1 && isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName && getProvenDomEventTargetPrototypeOwnerNames(callee.object, context.scopes).includes("MediaQueryList");
|
|
14289
|
+
};
|
|
14015
14290
|
const getCallRegistrationDetails = (callNode, context) => {
|
|
14016
14291
|
const callee = stripParenExpression(callNode.callee);
|
|
14017
14292
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return {
|
|
@@ -14020,11 +14295,17 @@ const getCallRegistrationDetails = (callNode, context) => {
|
|
|
14020
14295
|
eventKey: null,
|
|
14021
14296
|
handlerKey: null
|
|
14022
14297
|
};
|
|
14298
|
+
if (isProvenLegacyMediaQueryListMethodCall(callNode, "addListener", context)) return {
|
|
14299
|
+
receiverKey: resolveStableMediaQueryListenerIdentityKey(callee.object, context),
|
|
14300
|
+
registrationVerbName: callee.property.name,
|
|
14301
|
+
eventKey: null,
|
|
14302
|
+
handlerKey: resolveStableMediaQueryListenerIdentityKey(callNode.arguments?.[0], context)
|
|
14303
|
+
};
|
|
14023
14304
|
return {
|
|
14024
|
-
receiverKey:
|
|
14305
|
+
receiverKey: resolveResourceIdentityKey(callee.object, context),
|
|
14025
14306
|
registrationVerbName: callee.property.name,
|
|
14026
|
-
eventKey:
|
|
14027
|
-
handlerKey:
|
|
14307
|
+
eventKey: resolveResourceIdentityKey(callNode.arguments?.[0], context),
|
|
14308
|
+
handlerKey: resolveResourceIdentityKey(callNode.arguments?.[1], context)
|
|
14028
14309
|
};
|
|
14029
14310
|
};
|
|
14030
14311
|
const findSubscribeLikeUsages = (callback, context) => {
|
|
@@ -14124,30 +14405,6 @@ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, conte
|
|
|
14124
14405
|
}
|
|
14125
14406
|
return matchingBlocks.size > 0;
|
|
14126
14407
|
};
|
|
14127
|
-
const doMatchingNodesCoverEveryPathFromFunctionEntry = (owner, matchingNodes, context) => {
|
|
14128
|
-
const functionCfg = context.cfg.cfgFor(owner);
|
|
14129
|
-
if (!functionCfg) return false;
|
|
14130
|
-
const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
|
|
14131
|
-
if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
|
|
14132
|
-
const matchingBlock = functionCfg.blockOf(matchingNode);
|
|
14133
|
-
return matchingBlock ? [matchingBlock] : [];
|
|
14134
|
-
}));
|
|
14135
|
-
if (matchingBlocks.size === 0) return false;
|
|
14136
|
-
const visitedBlocks = new Set([functionCfg.entry]);
|
|
14137
|
-
const pendingBlocks = [functionCfg.entry];
|
|
14138
|
-
while (pendingBlocks.length > 0) {
|
|
14139
|
-
const currentBlock = pendingBlocks.pop();
|
|
14140
|
-
if (!currentBlock) break;
|
|
14141
|
-
if (matchingBlocks.has(currentBlock)) continue;
|
|
14142
|
-
for (const edge of currentBlock.successors) {
|
|
14143
|
-
if (edge.to === functionCfg.exit) return false;
|
|
14144
|
-
if (visitedBlocks.has(edge.to)) continue;
|
|
14145
|
-
visitedBlocks.add(edge.to);
|
|
14146
|
-
pendingBlocks.push(edge.to);
|
|
14147
|
-
}
|
|
14148
|
-
}
|
|
14149
|
-
return true;
|
|
14150
|
-
};
|
|
14151
14408
|
const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
|
|
14152
14409
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
14153
14410
|
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
@@ -14390,18 +14647,6 @@ const findPushedResourceCollectionKey = (usage, context) => {
|
|
|
14390
14647
|
return memberNode.property.name === "forEach" || memberNode.property.name === "push";
|
|
14391
14648
|
}) ? resolveExpressionKey(pushCallee.object, context) : null;
|
|
14392
14649
|
};
|
|
14393
|
-
const isWithinAssignmentTarget = (identifier) => {
|
|
14394
|
-
let currentNode = identifier;
|
|
14395
|
-
let parentNode = currentNode.parent;
|
|
14396
|
-
while (parentNode) {
|
|
14397
|
-
if (isNodeOfType(parentNode, "AssignmentExpression")) return parentNode.left === currentNode;
|
|
14398
|
-
if (isNodeOfType(parentNode, "UpdateExpression") || isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "delete") return parentNode.argument === currentNode;
|
|
14399
|
-
if (isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return parentNode.left === currentNode;
|
|
14400
|
-
currentNode = parentNode;
|
|
14401
|
-
parentNode = currentNode.parent;
|
|
14402
|
-
}
|
|
14403
|
-
return false;
|
|
14404
|
-
};
|
|
14405
14650
|
const resolveStableValue = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
14406
14651
|
if (!expression) return null;
|
|
14407
14652
|
const unwrappedExpression = stripParenExpression(expression);
|
|
@@ -14449,8 +14694,8 @@ const isSynchronousIteratorCallback = (functionNode) => {
|
|
|
14449
14694
|
return SYNCHRONOUS_ITERATOR_METHOD_NAMES$2.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
|
|
14450
14695
|
};
|
|
14451
14696
|
const findEnclosingForEachCall = (node) => {
|
|
14452
|
-
const callbackNode = findEnclosingFunction$1(node);
|
|
14453
|
-
if (!callbackNode) return null;
|
|
14697
|
+
const callbackNode = isFunctionLike$1(node) ? node : findEnclosingFunction$1(node);
|
|
14698
|
+
if (!callbackNode || !isFunctionLike$1(callbackNode) || callbackNode.async || callbackNode.generator) return null;
|
|
14454
14699
|
const callNode = callbackNode.parent;
|
|
14455
14700
|
if (!isNodeOfType(callNode, "CallExpression") || callNode.arguments?.[0] !== callbackNode) return null;
|
|
14456
14701
|
const callee = stripParenExpression(callNode.callee);
|
|
@@ -14525,7 +14770,7 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
|
|
|
14525
14770
|
const helperFunction = isNodeOfType(stableHelperFunction, "Identifier") ? resolveSingleAssignedCleanupFunction(stableHelperFunction, usage, context) : stableHelperFunction;
|
|
14526
14771
|
if (helperFunction && isFunctionLike$1(helperFunction) && !helperFunction.async && !helperFunction.generator && doesCleanupFunctionReleaseUsage(helperFunction, usage, context, new Set(visitedFunctions))) matchingLoopOrHelperAnchors.push(cleanupCall);
|
|
14527
14772
|
});
|
|
14528
|
-
return didCleanupFunctionMatch ||
|
|
14773
|
+
return didCleanupFunctionMatch || doNodesCoverEveryPathFromFunctionEntry(cleanupFunction, matchingLoopOrHelperAnchors, context);
|
|
14529
14774
|
};
|
|
14530
14775
|
const doesBoundCleanupReleaseUsage = (expression, usage, context) => {
|
|
14531
14776
|
const callExpression = stripParenExpression(expression);
|
|
@@ -14554,7 +14799,7 @@ const callbackReturnsCleanupForUsage = (callback, usage, context) => {
|
|
|
14554
14799
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
14555
14800
|
if (isNodeOfType(child, "ReturnStatement") && child.argument && doesReturnedValueReleaseUsage(stripParenExpression(child.argument))) matchingCleanupReturns.push(child);
|
|
14556
14801
|
});
|
|
14557
|
-
return
|
|
14802
|
+
return doNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
|
|
14558
14803
|
};
|
|
14559
14804
|
const doesTestRequireLiveExpressionKey = (test, expressionKey, context) => {
|
|
14560
14805
|
if (resolveExpressionKey(test, context) === expressionKey) return true;
|
|
@@ -14578,7 +14823,24 @@ const findLiveExpressionGuardForRelease = (releaseCall, owner, expressionKey, co
|
|
|
14578
14823
|
return null;
|
|
14579
14824
|
};
|
|
14580
14825
|
const findDirectHandleGuardForRelease = (releaseCall, owner, usage, context) => usage.handleKey === null ? null : findLiveExpressionGuardForRelease(releaseCall, owner, usage.handleKey, context);
|
|
14581
|
-
const
|
|
14826
|
+
const hasExecutionBoundaryNotSharedWithUsage = (node, usageNode, owner) => {
|
|
14827
|
+
const usageAncestors = /* @__PURE__ */ new Set();
|
|
14828
|
+
let usageAncestor = usageNode;
|
|
14829
|
+
while (usageAncestor && usageAncestor !== owner) {
|
|
14830
|
+
usageAncestors.add(usageAncestor);
|
|
14831
|
+
usageAncestor = usageAncestor.parent ?? null;
|
|
14832
|
+
}
|
|
14833
|
+
let descendant = node;
|
|
14834
|
+
let ancestor = descendant.parent ?? null;
|
|
14835
|
+
while (ancestor && ancestor !== owner) {
|
|
14836
|
+
const guardedSubtree = isNodeOfType(ancestor, "IfStatement") && (ancestor.consequent === descendant || ancestor.alternate === descendant) || isNodeOfType(ancestor, "ConditionalExpression") && (ancestor.consequent === descendant || ancestor.alternate === descendant) || isNodeOfType(ancestor, "LogicalExpression") && ancestor.right === descendant || isNodeOfType(ancestor, "AssignmentPattern") && ancestor.right === descendant || (isNodeOfType(ancestor, "ForStatement") || isNodeOfType(ancestor, "ForInStatement") || isNodeOfType(ancestor, "ForOfStatement") || isNodeOfType(ancestor, "WhileStatement") || isNodeOfType(ancestor, "DoWhileStatement")) && ancestor.body === descendant ? descendant : isNodeOfType(ancestor, "SwitchCase") ? ancestor : null;
|
|
14837
|
+
if (guardedSubtree && !usageAncestors.has(guardedSubtree)) return true;
|
|
14838
|
+
descendant = ancestor;
|
|
14839
|
+
ancestor = descendant.parent ?? null;
|
|
14840
|
+
}
|
|
14841
|
+
return false;
|
|
14842
|
+
};
|
|
14843
|
+
const hasRerunReleaseBeforeUsage = (callback, usage, context, allowUnreleasedPathsWithoutUsage = false) => {
|
|
14582
14844
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
14583
14845
|
const functionCfg = context.cfg.cfgFor(callback);
|
|
14584
14846
|
const usageBlock = functionCfg?.blockOf(usage.node);
|
|
@@ -14589,7 +14851,8 @@ const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
|
|
|
14589
14851
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
14590
14852
|
const releaseStart = getRangeStart(child);
|
|
14591
14853
|
const handleGuard = findDirectHandleGuardForRelease(child, callback, usage, context);
|
|
14592
|
-
|
|
14854
|
+
const releaseBlock = functionCfg.blockOf(child);
|
|
14855
|
+
if (releaseStart === null || releaseStart >= usageStart || releaseBlock !== usageBlock && !handleGuard || !handleGuard && hasExecutionBoundaryNotSharedWithUsage(child, usage.node, callback)) return;
|
|
14593
14856
|
if (doesReleaseCallMatchUsage(child, usage, context)) {
|
|
14594
14857
|
matchingReleaseAnchors.push(handleGuard ?? child);
|
|
14595
14858
|
return;
|
|
@@ -14597,7 +14860,7 @@ const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
|
|
|
14597
14860
|
const helperFunction = resolveStableValue(child.callee, context);
|
|
14598
14861
|
if (helperFunction && isFunctionLike$1(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context)) matchingReleaseAnchors.push(handleGuard ?? child);
|
|
14599
14862
|
});
|
|
14600
|
-
return
|
|
14863
|
+
return allowUnreleasedPathsWithoutUsage ? doMatchingNodesCoverEveryPathBeforeUsage(usage.node, matchingReleaseAnchors, callback, context) : doNodesCoverEveryPathFromFunctionEntry(callback, matchingReleaseAnchors, context);
|
|
14601
14864
|
};
|
|
14602
14865
|
const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
|
|
14603
14866
|
const componentFunction = findEnclosingFunction$1(callback);
|
|
@@ -14617,7 +14880,7 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
|
|
|
14617
14880
|
});
|
|
14618
14881
|
return didFindUnmountCleanup;
|
|
14619
14882
|
};
|
|
14620
|
-
const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null &&
|
|
14883
|
+
const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasStableUnmountCleanupForUsage(callback, usage, context) && hasRerunReleaseBeforeUsage(callback, usage, context, usage.registrationVerbName === "setTimeout");
|
|
14621
14884
|
const collectBlockingBooleanStates = (expression, blockedExpressionValue, guardNode, context) => {
|
|
14622
14885
|
const unwrappedExpression = stripParenExpression(expression);
|
|
14623
14886
|
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return collectBlockingBooleanStates(unwrappedExpression.argument, !blockedExpressionValue, guardNode, context);
|
|
@@ -14848,7 +15111,7 @@ const hasGuardedRefOwnedNestedCleanup = (callback, usage, cleanupReturns, contex
|
|
|
14848
15111
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14849
15112
|
const usageExpression = findTransparentExpressionRoot(usage.node);
|
|
14850
15113
|
const usageAssignment = usageExpression.parent;
|
|
14851
|
-
if (usage.kind !== "subscribe" && usage.kind !== "timer" || usage.handleKey === null || !usageFunction || !isFunctionLike$1(usageFunction) || usageFunction === callback || usageFunction.async || usageFunction.generator || !isNodeOfType(usageAssignment, "AssignmentExpression") || usageAssignment.operator !== "=" || usageAssignment.right !== usageExpression || !resolveReactRefSymbol(stripParenExpression(usageAssignment.left), context.scopes) || !collectSynchronouslyEffectInvokedFunctions(callback).has(usageFunction) || !cleanupReturnsReleaseUsage(cleanupReturns, usage, context) || !
|
|
15114
|
+
if (usage.kind !== "subscribe" && usage.kind !== "timer" || usage.handleKey === null || !usageFunction || !isFunctionLike$1(usageFunction) || usageFunction === callback || usageFunction.async || usageFunction.generator || !isNodeOfType(usageAssignment, "AssignmentExpression") || usageAssignment.operator !== "=" || usageAssignment.right !== usageExpression || !resolveReactRefSymbol(stripParenExpression(usageAssignment.left), context.scopes) || !collectSynchronouslyEffectInvokedFunctions(callback).has(usageFunction) || !cleanupReturnsReleaseUsage(cleanupReturns, usage, context) || !doNodesCoverEveryPathFromFunctionEntry(callback, cleanupReturns, context)) return false;
|
|
14852
15115
|
const cleanupFunctions = cleanupReturns.flatMap((cleanupReturn) => {
|
|
14853
15116
|
if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return [];
|
|
14854
15117
|
const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
|
|
@@ -14896,7 +15159,7 @@ const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) =>
|
|
|
14896
15159
|
});
|
|
14897
15160
|
}
|
|
14898
15161
|
});
|
|
14899
|
-
if (!
|
|
15162
|
+
if (!doNodesCoverEveryPathFromFunctionEntry(cleanupFunction, globalReleaseProofs.map((releaseProof) => releaseProof.anchor), context)) return false;
|
|
14900
15163
|
globalReleaseProofsByCleanup.set(cleanupFunction, globalReleaseProofs);
|
|
14901
15164
|
}
|
|
14902
15165
|
const handleAssignments = handleSymbol.references.filter((reference) => isWithinAssignmentTarget(reference.identifier));
|
|
@@ -15099,9 +15362,7 @@ const isReactRefListenerReplacementRelease = (releaseCall, usage, context) => {
|
|
|
15099
15362
|
const nodeParameterKey = resolveExpressionKey(usageFunction.params?.[0], context);
|
|
15100
15363
|
const releaseReceiverKey = resolveExpressionKey(releaseCallee.object, context);
|
|
15101
15364
|
if (registrationReceiverKey === null || registrationReceiverKey !== nodeParameterKey || releaseReceiverKey === null || usage.eventKey === null || usage.eventKey !== resolveExpressionKey(releaseCall.arguments?.[0], context) || usage.handlerKey === null || usage.handlerKey !== resolveExpressionKey(releaseCall.arguments?.[1], context)) return false;
|
|
15102
|
-
|
|
15103
|
-
const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15104
|
-
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
15365
|
+
if (!doEventListenerCapturesMatch(usage.node.arguments?.[2], releaseCall.arguments?.[2], context)) return false;
|
|
15105
15366
|
const releaseStart = getRangeStart(releaseCall);
|
|
15106
15367
|
const matchingOwnershipAssignments = [];
|
|
15107
15368
|
const usageFunctionBody = usageFunction.body;
|
|
@@ -15111,7 +15372,127 @@ const isReactRefListenerReplacementRelease = (releaseCall, usage, context) => {
|
|
|
15111
15372
|
});
|
|
15112
15373
|
const releaseAnchor = findLiveExpressionGuardForRelease(releaseCall, usageFunction, releaseReceiverKey, context) ?? releaseCall;
|
|
15113
15374
|
const safeOwnershipAssignments = matchingOwnershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, [releaseAnchor], usageFunction, context));
|
|
15114
|
-
return
|
|
15375
|
+
return doNodesCoverEveryPathFromFunctionEntry(usageFunction, [releaseAnchor], context) && doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
|
|
15376
|
+
};
|
|
15377
|
+
const findDirectExhaustiveForEachCleanupFunction = (releaseNode, requiredCollectionKeys, context) => {
|
|
15378
|
+
let currentNode = findTransparentExpressionRoot(releaseNode);
|
|
15379
|
+
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
15380
|
+
const replayedCollectionKeys = /* @__PURE__ */ new Set();
|
|
15381
|
+
while (true) {
|
|
15382
|
+
const ownerFunction = findEnclosingFunction$1(currentNode);
|
|
15383
|
+
if (!ownerFunction || !isFunctionLike$1(ownerFunction) || visitedFunctions.has(ownerFunction)) return null;
|
|
15384
|
+
visitedFunctions.add(ownerFunction);
|
|
15385
|
+
const isDirectConciseBody = ownerFunction.body === currentNode;
|
|
15386
|
+
const statementNode = currentNode.parent;
|
|
15387
|
+
const isDirectBlockStatement = isNodeOfType(ownerFunction.body, "BlockStatement") && isNodeOfType(statementNode, "ExpressionStatement") && statementNode.parent === ownerFunction.body;
|
|
15388
|
+
if (!isDirectConciseBody && !isDirectBlockStatement || !doNodesCoverEveryPathFromFunctionEntry(ownerFunction, [isDirectBlockStatement ? statementNode : currentNode], context)) return null;
|
|
15389
|
+
const forEachCall = findEnclosingForEachCall(ownerFunction);
|
|
15390
|
+
if (!forEachCall) return replayedCollectionKeys.size === requiredCollectionKeys.size && isReturnedEffectCleanupFunction(ownerFunction, context) ? ownerFunction : null;
|
|
15391
|
+
const forEachCallee = stripParenExpression(forEachCall.callee);
|
|
15392
|
+
if (!isNodeOfType(forEachCallee, "MemberExpression")) return null;
|
|
15393
|
+
const collectionKey = resolveExpressionKey(forEachCallee.object, context);
|
|
15394
|
+
if (!collectionKey || !requiredCollectionKeys.has(collectionKey)) return null;
|
|
15395
|
+
replayedCollectionKeys.add(collectionKey);
|
|
15396
|
+
currentNode = findTransparentExpressionRoot(forEachCall);
|
|
15397
|
+
}
|
|
15398
|
+
};
|
|
15399
|
+
const collectReplayOwnerFunctions = (usageNode) => {
|
|
15400
|
+
const ownerFunctions = /* @__PURE__ */ new Set();
|
|
15401
|
+
let currentNode = usageNode;
|
|
15402
|
+
while (true) {
|
|
15403
|
+
const ownerFunction = findEnclosingFunction$1(currentNode);
|
|
15404
|
+
if (!ownerFunction || !isFunctionLike$1(ownerFunction) || ownerFunctions.has(ownerFunction)) break;
|
|
15405
|
+
ownerFunctions.add(ownerFunction);
|
|
15406
|
+
const forEachCall = findEnclosingForEachCall(ownerFunction);
|
|
15407
|
+
if (!forEachCall) break;
|
|
15408
|
+
currentNode = forEachCall;
|
|
15409
|
+
}
|
|
15410
|
+
return ownerFunctions;
|
|
15411
|
+
};
|
|
15412
|
+
const hasCollectionMutationBeforeRelease = (usageNode, releaseNode, collectionKeys, context) => {
|
|
15413
|
+
const usageStart = getRangeStart(usageNode);
|
|
15414
|
+
const releaseStart = getRangeStart(releaseNode);
|
|
15415
|
+
if (usageStart === null || releaseStart === null) return true;
|
|
15416
|
+
const setupOwnerFunctions = collectReplayOwnerFunctions(usageNode);
|
|
15417
|
+
const cleanupOwnerFunctions = collectReplayOwnerFunctions(releaseNode);
|
|
15418
|
+
let programNode = usageNode;
|
|
15419
|
+
while (programNode.parent) programNode = programNode.parent;
|
|
15420
|
+
let didFindMutation = false;
|
|
15421
|
+
walkAst(programNode, (child) => {
|
|
15422
|
+
if (didFindMutation) return false;
|
|
15423
|
+
const childStart = getRangeStart(child);
|
|
15424
|
+
if (childStart === null) return;
|
|
15425
|
+
const ownerFunction = context.cfg.enclosingFunction(child);
|
|
15426
|
+
if (!ownerFunction) return;
|
|
15427
|
+
const isAfterRegistration = setupOwnerFunctions.has(ownerFunction) && childStart > usageStart;
|
|
15428
|
+
const isBeforeRelease = cleanupOwnerFunctions.has(ownerFunction) && childStart < releaseStart;
|
|
15429
|
+
if (!isAfterRegistration && !isBeforeRelease) return;
|
|
15430
|
+
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
15431
|
+
const assignmentKey = resolveExpressionKey(child.left, context);
|
|
15432
|
+
const assignmentTarget = stripParenExpression(child.left);
|
|
15433
|
+
if (assignmentKey && [...collectionKeys].some((collectionKey) => assignmentKey === collectionKey || assignmentKey === `${collectionKey}.length`) || isNodeOfType(assignmentTarget, "MemberExpression") && assignmentTarget.computed && collectionKeys.has(resolveExpressionKey(assignmentTarget.object, context) ?? "")) {
|
|
15434
|
+
didFindMutation = true;
|
|
15435
|
+
return false;
|
|
15436
|
+
}
|
|
15437
|
+
return;
|
|
15438
|
+
}
|
|
15439
|
+
if (isNodeOfType(child, "UnaryExpression") && child.operator === "delete") {
|
|
15440
|
+
const deletedMember = stripParenExpression(child.argument);
|
|
15441
|
+
if (!isNodeOfType(deletedMember, "MemberExpression")) return;
|
|
15442
|
+
if (collectionKeys.has(resolveExpressionKey(deletedMember.object, context) ?? "")) {
|
|
15443
|
+
didFindMutation = true;
|
|
15444
|
+
return false;
|
|
15445
|
+
}
|
|
15446
|
+
return;
|
|
15447
|
+
}
|
|
15448
|
+
if (isNodeOfType(child, "UpdateExpression")) {
|
|
15449
|
+
const updatedKey = resolveExpressionKey(child.argument, context);
|
|
15450
|
+
if (updatedKey && [...collectionKeys].some((collectionKey) => updatedKey === `${collectionKey}.length`)) {
|
|
15451
|
+
didFindMutation = true;
|
|
15452
|
+
return false;
|
|
15453
|
+
}
|
|
15454
|
+
return;
|
|
15455
|
+
}
|
|
15456
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
15457
|
+
const callee = stripParenExpression(child.callee);
|
|
15458
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !REPLAY_ENTRY_DROPPING_ARRAY_METHOD_NAMES.has(callee.property.name) && !REPLAY_ENTRY_DROPPING_COLLECTION_METHOD_NAMES.has(callee.property.name) || !collectionKeys.has(resolveExpressionKey(callee.object, context) ?? "")) return;
|
|
15459
|
+
didFindMutation = true;
|
|
15460
|
+
return false;
|
|
15461
|
+
});
|
|
15462
|
+
return didFindMutation;
|
|
15463
|
+
};
|
|
15464
|
+
const usesUnaryListenerSignature = (registrationCall, releaseCall) => getCalleeName$1(registrationCall) === "addListener" && registrationCall.arguments?.length === 1 && releaseCall.arguments?.length === 1;
|
|
15465
|
+
const hasSafeForEachProjectionCleanup = (registrationCall, releaseCall, context) => {
|
|
15466
|
+
const registrationCallee = stripParenExpression(registrationCall.callee);
|
|
15467
|
+
const releaseCallee = stripParenExpression(releaseCall.callee);
|
|
15468
|
+
if (!isNodeOfType(registrationCallee, "MemberExpression") || !isNodeOfType(releaseCallee, "MemberExpression")) return true;
|
|
15469
|
+
const registrationVerbName = getCalleeName$1(registrationCall);
|
|
15470
|
+
const releaseVerbName = getCalleeName$1(releaseCall);
|
|
15471
|
+
const releaseHandler = releaseCall.arguments?.[usesUnaryListenerSignature(registrationCall, releaseCall) ? 0 : 1];
|
|
15472
|
+
const releaseFunction = findEnclosingFunction$1(releaseCall);
|
|
15473
|
+
const registrationEventKey = resolveResourceIdentityKey(registrationCall.arguments?.[0], context);
|
|
15474
|
+
const releaseEventKey = resolveResourceIdentityKey(releaseCall.arguments?.[0], context);
|
|
15475
|
+
const doesHandlerlessOffReleaseEveryRegistration = releaseVerbName === "off" && !releaseHandler && (releaseCall.arguments?.length === 0 || registrationEventKey !== null && registrationEventKey === releaseEventKey);
|
|
15476
|
+
if (Boolean(releaseFunction && isFunctionLike$1(releaseFunction) && isReturnedEffectCleanupFunction(releaseFunction, context) && doNodesCoverEveryPathFromFunctionEntry(releaseFunction, [releaseCall], context)) && (releaseVerbName !== null && UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName) || doesHandlerlessOffReleaseEveryRegistration)) return true;
|
|
15477
|
+
const projections = [
|
|
15478
|
+
registrationCallee.object,
|
|
15479
|
+
registrationCall.arguments?.[0],
|
|
15480
|
+
registrationCall.arguments?.[1],
|
|
15481
|
+
releaseCallee.object,
|
|
15482
|
+
releaseCall.arguments?.[0],
|
|
15483
|
+
releaseCall.arguments?.[1]
|
|
15484
|
+
].flatMap((expression) => {
|
|
15485
|
+
const projection = resolveForEachProjection(expression, context);
|
|
15486
|
+
return projection ? [projection] : [];
|
|
15487
|
+
});
|
|
15488
|
+
if (registrationVerbName === "addEventListener" && releaseVerbName === "removeEventListener") for (const optionsNode of [registrationCall.arguments?.[2], releaseCall.arguments?.[2]]) {
|
|
15489
|
+
const captureProjection = resolveEventListenerCaptureProjection(optionsNode, context);
|
|
15490
|
+
if (captureProjection) projections.push(captureProjection);
|
|
15491
|
+
}
|
|
15492
|
+
if (projections.length === 0) return true;
|
|
15493
|
+
const collectionKeys = new Set(projections.map((projection) => projection.collectionKey));
|
|
15494
|
+
if (!findDirectExhaustiveForEachCleanupFunction(releaseCall, collectionKeys, context)) return false;
|
|
15495
|
+
return !hasCollectionMutationBeforeRelease(registrationCall, releaseCall, collectionKeys, context);
|
|
15115
15496
|
};
|
|
15116
15497
|
const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
15117
15498
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
@@ -15128,8 +15509,8 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
15128
15509
|
const releaseVerbName = getReleaseVerbName(callNode);
|
|
15129
15510
|
if (!releaseVerbName) return false;
|
|
15130
15511
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
15131
|
-
const releaseReceiverKey =
|
|
15132
|
-
const releaseEventKey =
|
|
15512
|
+
const releaseReceiverKey = resolveResourceIdentityKey(callee.object, context);
|
|
15513
|
+
const releaseEventKey = resolveResourceIdentityKey(callNode.arguments?.[0], context);
|
|
15133
15514
|
const pairedReleaseVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
15134
15515
|
const pushedResourceCollectionKey = findPushedResourceCollectionKey(usage, context);
|
|
15135
15516
|
const releaseReceiverForOfStatement = findForOfStatementForIteratorExpression(callee.object, context);
|
|
@@ -15139,7 +15520,13 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
15139
15520
|
if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
|
|
15140
15521
|
if (usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName))) return true;
|
|
15141
15522
|
if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
|
|
15523
|
+
if (usage.registrationVerbName === "addListener" && isNodeOfType(usage.node, "CallExpression") && usage.node.arguments?.length === 1) return isProvenLegacyMediaQueryListMethodCall(usage.node, "addListener", context) && releaseVerbName === "removeListener" && isProvenLegacyMediaQueryListMethodCall(callNode, "removeListener", context) && usage.receiverKey !== null && resolveStableMediaQueryListenerIdentityKey(callee.object, context) === usage.receiverKey && usage.handlerKey !== null && resolveStableMediaQueryListenerIdentityKey(callNode.arguments?.[0], context) === usage.handlerKey;
|
|
15142
15524
|
if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
|
|
15525
|
+
if (usage.registrationVerbName === "addEventListener" && releaseVerbName === "removeEventListener" && isNodeOfType(usage.node, "CallExpression")) {
|
|
15526
|
+
if (!isNodeOfType(stripParenExpression(usage.node.callee), "MemberExpression")) return false;
|
|
15527
|
+
if (!doEventListenerCapturesMatch(usage.node.arguments?.[2], callNode.arguments?.[2], context, true)) return false;
|
|
15528
|
+
}
|
|
15529
|
+
if (isNodeOfType(usage.node, "CallExpression") && !hasSafeForEachProjectionCleanup(usage.node, callNode, context)) return false;
|
|
15143
15530
|
if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
|
|
15144
15531
|
if (usage.registrationVerbName === "subscribe" && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub") && usage.handleKey !== null && resolveExpressionKey(callNode.arguments?.[0], context) === usage.handleKey) return true;
|
|
15145
15532
|
const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
@@ -15149,6 +15536,9 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
15149
15536
|
if (isAssignmentFormForOfIteratorReference(usageEventArgument, context) || isAssignmentFormForOfIteratorReference(releaseEventArgument, context)) return false;
|
|
15150
15537
|
if (usage.eventKey !== null && releaseEventKey !== null && usage.eventKey !== releaseEventKey) {
|
|
15151
15538
|
if (!isNodeOfType(usage.node, "CallExpression")) return false;
|
|
15539
|
+
const registrationEventProjectionKey = resolveForEachProjectionKey(usage.node.arguments?.[0], context);
|
|
15540
|
+
const releaseEventProjectionKey = resolveForEachProjectionKey(callNode.arguments?.[0], context);
|
|
15541
|
+
if ((registrationEventProjectionKey !== null || releaseEventProjectionKey !== null) && registrationEventProjectionKey !== releaseEventProjectionKey) return false;
|
|
15152
15542
|
const usageForOfStatement = findForOfStatementForIteratorExpression(usageEventArgument, context);
|
|
15153
15543
|
const releaseForOfStatement = findForOfStatementForIteratorExpression(releaseEventArgument, context);
|
|
15154
15544
|
if (usageForOfStatement === null !== (releaseForOfStatement === null)) return false;
|
|
@@ -15162,9 +15552,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
15162
15552
|
const registrationCallee = stripParenExpression(usage.node.callee);
|
|
15163
15553
|
if (!isNodeOfType(registrationCallee, "MemberExpression")) return false;
|
|
15164
15554
|
if (!isStableLoopReceiver(registrationCallee.object, context) || !isStableLoopReceiver(callee.object, context) || registrationHandlerSymbolId === null || registrationHandlerSymbolId !== releaseHandlerSymbolId) return false;
|
|
15165
|
-
|
|
15166
|
-
const releaseCapture = resolveEventListenerCapture(callNode.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15167
|
-
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
15555
|
+
if (!doEventListenerCapturesMatch(usage.node.arguments?.[2], callNode.arguments?.[2], context)) return false;
|
|
15168
15556
|
if (!isDirectExhaustiveForOfRelease(callNode, releaseForOfStatement)) return false;
|
|
15169
15557
|
}
|
|
15170
15558
|
}
|
|
@@ -15173,27 +15561,28 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
15173
15561
|
return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
|
|
15174
15562
|
}
|
|
15175
15563
|
if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
|
|
15176
|
-
const
|
|
15177
|
-
const releaseHandler =
|
|
15564
|
+
const usesUnaryListenerSignatureForCalls = isNodeOfType(usage.node, "CallExpression") && usesUnaryListenerSignature(usage.node, callNode);
|
|
15565
|
+
const releaseHandler = usesUnaryListenerSignatureForCalls ? callNode.arguments?.[0] : callNode.arguments?.[1];
|
|
15178
15566
|
if (!releaseHandler) return releaseVerbName === "off";
|
|
15179
|
-
const expectedHandlerKey =
|
|
15180
|
-
const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[
|
|
15181
|
-
return expectedHandlerKey !== null &&
|
|
15567
|
+
const expectedHandlerKey = usesUnaryListenerSignatureForCalls ? usage.eventKey : usage.handlerKey;
|
|
15568
|
+
const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[usesUnaryListenerSignatureForCalls ? 0 : 1] : null;
|
|
15569
|
+
return expectedHandlerKey !== null && resolveResourceIdentityKey(releaseHandler, context) === expectedHandlerKey || registrationHandler !== null && resolveStableValue(releaseHandler, context) === resolveStableValue(registrationHandler, context);
|
|
15182
15570
|
}
|
|
15183
15571
|
if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
|
|
15184
15572
|
return true;
|
|
15185
15573
|
};
|
|
15186
15574
|
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
15187
15575
|
const isReturnedEffectCleanupFunction = (functionNode, context) => {
|
|
15188
|
-
|
|
15189
|
-
|
|
15190
|
-
|
|
15191
|
-
|
|
15192
|
-
|
|
15193
|
-
|
|
15194
|
-
|
|
15195
|
-
|
|
15196
|
-
|
|
15576
|
+
const effectCallback = findEnclosingFunction$1(functionNode);
|
|
15577
|
+
if (!effectCallback || !isFunctionLike$1(effectCallback)) return false;
|
|
15578
|
+
const effectCall = effectCallback.parent;
|
|
15579
|
+
if (!isNodeOfType(effectCall, "CallExpression") || !isReactHookCall(effectCall, CLEANUP_EFFECT_HOOK_NAMES, context.scopes)) return false;
|
|
15580
|
+
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return resolveStableValue(effectCallback.body, context) === functionNode;
|
|
15581
|
+
let isReturned = false;
|
|
15582
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
15583
|
+
if (isNodeOfType(child, "ReturnStatement") && child.argument && resolveStableValue(child.argument, context) === functionNode) isReturned = true;
|
|
15584
|
+
});
|
|
15585
|
+
return isReturned;
|
|
15197
15586
|
};
|
|
15198
15587
|
const isPotentiallyReachableFunction = (functionNode, context) => {
|
|
15199
15588
|
if (isInlineRetainedHandlerFunction(functionNode, context) || isReturnedEffectCleanupFunction(functionNode, context)) return true;
|
|
@@ -15283,7 +15672,7 @@ const hasEffectCleanupInvocation = (storage, usage, context) => {
|
|
|
15283
15672
|
if (!cleanupFunction || !cleanupFunctionInvokesRef(cleanupFunction)) return;
|
|
15284
15673
|
matchingReturns.push(child);
|
|
15285
15674
|
});
|
|
15286
|
-
return
|
|
15675
|
+
return doNodesCoverEveryPathFromFunctionEntry(effectCallback, matchingReturns, context);
|
|
15287
15676
|
};
|
|
15288
15677
|
let didFindInvocation = false;
|
|
15289
15678
|
walkAst(componentFunction.body, (child) => {
|
|
@@ -15335,12 +15724,10 @@ const isRetainedDisposerRefRelease = (releaseNode, usage, context) => {
|
|
|
15335
15724
|
return findRetainedDisposerStorages(disposerFunction, usage, context).some((storage) => isRetainedDisposerStorageEstablished(storage, usage, context) && !hasUnsafeRetainedDisposerOverwrite(storage, usage, context) && (hasEffectCleanupInvocation(storage, usage, context) || hasCallbackRefReplacementInvocation(storage, usage, context)));
|
|
15336
15725
|
};
|
|
15337
15726
|
const isSelfReleasingListenerRelease = (releaseNode, releaseFunction, usage, context) => {
|
|
15338
|
-
if (usage.kind !== "subscribe" || usage.registrationVerbName !== "addEventListener" || usage.receiverKey === null || usage.eventKey === null || !isNodeOfType(usage.node, "CallExpression") || !isFunctionLike$1(releaseFunction) || releaseFunction.async || releaseFunction.generator || !isNodeOfType(releaseFunction.body, "BlockStatement") || !
|
|
15339
|
-
const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15727
|
+
if (usage.kind !== "subscribe" || usage.registrationVerbName !== "addEventListener" || usage.receiverKey === null || usage.eventKey === null || !isNodeOfType(usage.node, "CallExpression") || !isFunctionLike$1(releaseFunction) || releaseFunction.async || releaseFunction.generator || !isNodeOfType(releaseFunction.body, "BlockStatement") || !doNodesCoverEveryPathFromFunctionEntry(releaseFunction, [releaseNode], context)) return false;
|
|
15340
15728
|
const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
|
|
15341
15729
|
if (!isNodeOfType(releaseCall, "CallExpression")) return false;
|
|
15342
|
-
|
|
15343
|
-
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
15730
|
+
if (!doEventListenerCapturesMatch(usage.node.arguments?.[2], releaseCall.arguments?.[2], context)) return false;
|
|
15344
15731
|
const ownerFunction = findEnclosingFunction$1(releaseFunction);
|
|
15345
15732
|
if (!ownerFunction || !isFunctionLike$1(ownerFunction)) return false;
|
|
15346
15733
|
const triggerRegistrations = [];
|
|
@@ -15565,7 +15952,7 @@ const effectReturnsRefOwnedCleanup = (effectCallback, componentFunction, retaine
|
|
|
15565
15952
|
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
15566
15953
|
if (isNodeOfType(child, "ReturnStatement") && child.argument && matchesReturnedCleanup(stripParenExpression(child.argument))) matchingReturns.push(child);
|
|
15567
15954
|
});
|
|
15568
|
-
return
|
|
15955
|
+
return doNodesCoverEveryPathFromFunctionEntry(effectCallback, matchingReturns, context);
|
|
15569
15956
|
};
|
|
15570
15957
|
const hasGuaranteedRefOwnedUnmountCleanup = (retainedFunction, usage, context) => {
|
|
15571
15958
|
const componentFunction = findEnclosingFunction$1(retainedFunction);
|
|
@@ -15608,15 +15995,6 @@ const findUnconditionalReturnStatement = (expression, ownerFunction) => {
|
|
|
15608
15995
|
const returnStatement = expressionRoot.parent;
|
|
15609
15996
|
return isNodeOfType(returnStatement, "ReturnStatement") && returnStatement.argument === expressionRoot && findEnclosingFunction$1(returnStatement) === ownerFunction ? returnStatement : null;
|
|
15610
15997
|
};
|
|
15611
|
-
const getFinalSequenceExpressionValue = (expression) => {
|
|
15612
|
-
let finalExpression = stripParenExpression(expression);
|
|
15613
|
-
while (isNodeOfType(finalExpression, "SequenceExpression")) {
|
|
15614
|
-
const sequenceResult = finalExpression.expressions.at(-1);
|
|
15615
|
-
if (!sequenceResult) break;
|
|
15616
|
-
finalExpression = stripParenExpression(sequenceResult);
|
|
15617
|
-
}
|
|
15618
|
-
return finalExpression;
|
|
15619
|
-
};
|
|
15620
15998
|
const doesResourceResultEscape = (resourceNode, allowReturnedResourceEscape, allowConciseReturnEscape, context) => {
|
|
15621
15999
|
if (!allowReturnedResourceEscape) return false;
|
|
15622
16000
|
let currentNode = resourceNode;
|
|
@@ -15865,7 +16243,7 @@ const isReactRefCallbackCleanupOwnedByEffect = (retainedFunction, cleanupFunctio
|
|
|
15865
16243
|
if (child !== returnedCleanupFunction.body && isFunctionLike$1(child)) return false;
|
|
15866
16244
|
if (isNodeOfType(child, "CallExpression") && resolveRefOwnedCleanupFunction(child.callee, context) === cleanupFunction) matchingCalls.push(child);
|
|
15867
16245
|
});
|
|
15868
|
-
return
|
|
16246
|
+
return doNodesCoverEveryPathFromFunctionEntry(returnedCleanupFunction, matchingCalls, context);
|
|
15869
16247
|
};
|
|
15870
16248
|
const matchingReturns = [];
|
|
15871
16249
|
walkInsideStatementBlocks(retainedFunction.body, (child) => {
|
|
@@ -18668,7 +19046,7 @@ const compileGlob = (pattern) => {
|
|
|
18668
19046
|
};
|
|
18669
19047
|
//#endregion
|
|
18670
19048
|
//#region src/plugin/rules/react-builtins/forbid-component-props.ts
|
|
18671
|
-
const resolveSettings$
|
|
19049
|
+
const resolveSettings$43 = (settings) => {
|
|
18672
19050
|
const reactDoctor = settings?.["react-doctor"];
|
|
18673
19051
|
return { forbid: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidComponentProps ?? {} : {}).forbid ?? [] };
|
|
18674
19052
|
};
|
|
@@ -18720,7 +19098,7 @@ const forbidComponentProps = defineRule({
|
|
|
18720
19098
|
recommendation: "Configure blocked component props so callers cannot bypass the component API contract.",
|
|
18721
19099
|
category: "Architecture",
|
|
18722
19100
|
create: (context) => {
|
|
18723
|
-
const entries = resolveSettings$
|
|
19101
|
+
const entries = resolveSettings$43(context.settings).forbid.map(normalizeEntry);
|
|
18724
19102
|
return { JSXOpeningElement(node) {
|
|
18725
19103
|
if (entries.length === 0) return;
|
|
18726
19104
|
if (!isSupportedJsxName(node.name)) return;
|
|
@@ -18749,7 +19127,7 @@ const forbidComponentProps = defineRule({
|
|
|
18749
19127
|
//#endregion
|
|
18750
19128
|
//#region src/plugin/rules/react-builtins/forbid-dom-props.ts
|
|
18751
19129
|
const buildMessage$24 = (propName, customMessage) => customMessage ?? `Your project blocks the \`${propName}\` prop on plain HTML tags, so this bypasses the agreed DOM API contract.`;
|
|
18752
|
-
const resolveSettings$
|
|
19130
|
+
const resolveSettings$42 = (settings) => {
|
|
18753
19131
|
const reactDoctor = settings?.["react-doctor"];
|
|
18754
19132
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
|
|
18755
19133
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -18771,7 +19149,7 @@ const forbidDomProps = defineRule({
|
|
|
18771
19149
|
recommendation: "Configure blocked DOM props so plain HTML tags stay on the agreed DOM API surface.",
|
|
18772
19150
|
category: "Architecture",
|
|
18773
19151
|
create: (context) => {
|
|
18774
|
-
const forbidMap = resolveSettings$
|
|
19152
|
+
const forbidMap = resolveSettings$42(context.settings);
|
|
18775
19153
|
return { JSXOpeningElement(node) {
|
|
18776
19154
|
if (forbidMap.size === 0) return;
|
|
18777
19155
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -18810,11 +19188,12 @@ const forbidDomProps = defineRule({
|
|
|
18810
19188
|
* carrying their own near-identical inlined implementation.
|
|
18811
19189
|
*/
|
|
18812
19190
|
const flattenCalleeName = (callee) => {
|
|
18813
|
-
|
|
18814
|
-
if (isNodeOfType(
|
|
18815
|
-
|
|
19191
|
+
const unwrappedCallee = stripParenExpression(callee);
|
|
19192
|
+
if (isNodeOfType(unwrappedCallee, "Identifier")) return unwrappedCallee.name;
|
|
19193
|
+
if (isNodeOfType(unwrappedCallee, "MemberExpression") && !unwrappedCallee.computed) {
|
|
19194
|
+
const objectName = flattenCalleeName(unwrappedCallee.object);
|
|
18816
19195
|
if (!objectName) return null;
|
|
18817
|
-
if (isNodeOfType(
|
|
19196
|
+
if (isNodeOfType(unwrappedCallee.property, "Identifier")) return `${objectName}.${unwrappedCallee.property.name}`;
|
|
18818
19197
|
}
|
|
18819
19198
|
return null;
|
|
18820
19199
|
};
|
|
@@ -18833,7 +19212,7 @@ const isReactFunctionCall = (node, expectedCall) => {
|
|
|
18833
19212
|
//#endregion
|
|
18834
19213
|
//#region src/plugin/rules/react-builtins/forbid-elements.ts
|
|
18835
19214
|
const buildMessage$23 = (element, customHelp) => customHelp ? `Your project blocks \`<${element}>\` here. ${customHelp}` : `Your project blocks \`<${element}>\` here, so code stays on the approved UI surface.`;
|
|
18836
|
-
const resolveSettings$
|
|
19215
|
+
const resolveSettings$41 = (settings) => {
|
|
18837
19216
|
const reactDoctor = settings?.["react-doctor"];
|
|
18838
19217
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
|
|
18839
19218
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -18849,7 +19228,7 @@ const forbidElements = defineRule({
|
|
|
18849
19228
|
recommendation: "Configure blocked elements so code stays on the approved UI primitives.",
|
|
18850
19229
|
category: "Architecture",
|
|
18851
19230
|
create: (context) => {
|
|
18852
|
-
const forbidMap = resolveSettings$
|
|
19231
|
+
const forbidMap = resolveSettings$41(context.settings);
|
|
18853
19232
|
return {
|
|
18854
19233
|
JSXOpeningElement(node) {
|
|
18855
19234
|
if (forbidMap.size === 0) return;
|
|
@@ -18958,7 +19337,7 @@ const DEFAULT_HEADING_TAGS = [
|
|
|
18958
19337
|
"h5",
|
|
18959
19338
|
"h6"
|
|
18960
19339
|
];
|
|
18961
|
-
const resolveSettings$
|
|
19340
|
+
const resolveSettings$40 = (settings) => {
|
|
18962
19341
|
const reactDoctor = settings?.["react-doctor"];
|
|
18963
19342
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
|
|
18964
19343
|
return { headingTags: new Set([...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []]) };
|
|
@@ -18971,7 +19350,7 @@ const headingHasContent = defineRule({
|
|
|
18971
19350
|
recommendation: "Put readable text in every heading.",
|
|
18972
19351
|
category: "Accessibility",
|
|
18973
19352
|
create: (context) => {
|
|
18974
|
-
const settings = resolveSettings$
|
|
19353
|
+
const settings = resolveSettings$40(context.settings);
|
|
18975
19354
|
return { JSXOpeningElement(node) {
|
|
18976
19355
|
const elementType = getElementType(node, context.settings);
|
|
18977
19356
|
if (!settings.headingTags.has(elementType)) return;
|
|
@@ -18992,7 +19371,7 @@ const headingHasContent = defineRule({
|
|
|
18992
19371
|
//#region src/plugin/rules/react-builtins/hook-use-state.ts
|
|
18993
19372
|
const REQUIRE_DESTRUCTURE_MESSAGE = "`useState` should be destructured as `[value, setValue]` so readers can see the state value and setter together.";
|
|
18994
19373
|
const NAMING_CONVENTION_MESSAGE = "This `useState` setter does not match its value name, so updates are harder to trace.";
|
|
18995
|
-
const resolveSettings$
|
|
19374
|
+
const resolveSettings$39 = (settings) => {
|
|
18996
19375
|
const reactDoctor = settings?.["react-doctor"];
|
|
18997
19376
|
return { allowDestructuredState: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.hookUseState ?? {} : {}).allowDestructuredState ?? false };
|
|
18998
19377
|
};
|
|
@@ -19019,7 +19398,7 @@ const hookUseState = defineRule({
|
|
|
19019
19398
|
recommendation: "Destructure `useState` as `const [thing, setThing] = useState(…)` so state reads and writes stay visible together.",
|
|
19020
19399
|
category: "Architecture",
|
|
19021
19400
|
create: (context) => {
|
|
19022
|
-
const { allowDestructuredState } = resolveSettings$
|
|
19401
|
+
const { allowDestructuredState } = resolveSettings$39(context.settings);
|
|
19023
19402
|
return { CallExpression(node) {
|
|
19024
19403
|
const callExpressionNode = node;
|
|
19025
19404
|
if (!isReactFunctionCall(callExpressionNode, "useState")) return;
|
|
@@ -19152,7 +19531,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
19152
19531
|
//#endregion
|
|
19153
19532
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
19154
19533
|
const MESSAGE$52 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
19155
|
-
const resolveSettings$
|
|
19534
|
+
const resolveSettings$38 = (settings) => {
|
|
19156
19535
|
const reactDoctor = settings?.["react-doctor"];
|
|
19157
19536
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
19158
19537
|
};
|
|
@@ -19189,7 +19568,7 @@ const htmlHasLang = defineRule({
|
|
|
19189
19568
|
recommendation: "Set `<html lang=\"…\">` so screen readers know the page language.",
|
|
19190
19569
|
category: "Accessibility",
|
|
19191
19570
|
create: (context) => {
|
|
19192
|
-
const settings = resolveSettings$
|
|
19571
|
+
const settings = resolveSettings$38(context.settings);
|
|
19193
19572
|
const tagSet = new Set(settings.htmlTags);
|
|
19194
19573
|
return { JSXOpeningElement(node) {
|
|
19195
19574
|
const tag = getElementType(node, context.settings);
|
|
@@ -19705,7 +20084,7 @@ const DEFAULT_REDUNDANT_WORDS = [
|
|
|
19705
20084
|
"photo",
|
|
19706
20085
|
"picture"
|
|
19707
20086
|
];
|
|
19708
|
-
const resolveSettings$
|
|
20087
|
+
const resolveSettings$37 = (settings) => {
|
|
19709
20088
|
const reactDoctor = settings?.["react-doctor"];
|
|
19710
20089
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.imgRedundantAlt ?? {} : {};
|
|
19711
20090
|
return {
|
|
@@ -19753,7 +20132,7 @@ const imgRedundantAlt = defineRule({
|
|
|
19753
20132
|
category: "Accessibility",
|
|
19754
20133
|
create: (context) => {
|
|
19755
20134
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
19756
|
-
const settings = resolveSettings$
|
|
20135
|
+
const settings = resolveSettings$37(context.settings);
|
|
19757
20136
|
return { JSXOpeningElement(node) {
|
|
19758
20137
|
const tag = getElementType(node, context.settings);
|
|
19759
20138
|
if (!settings.components.includes(tag)) return;
|
|
@@ -20061,7 +20440,7 @@ const COMPOSITE_ITEM_ROLES = new Set([
|
|
|
20061
20440
|
"row",
|
|
20062
20441
|
"gridcell"
|
|
20063
20442
|
]);
|
|
20064
|
-
const resolveSettings$
|
|
20443
|
+
const resolveSettings$36 = (settings) => {
|
|
20065
20444
|
const reactDoctor = settings?.["react-doctor"];
|
|
20066
20445
|
return { tabbable: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.interactiveSupportsFocus ?? {} : {}).tabbable ?? DEFAULT_TABBABLE_ROLES };
|
|
20067
20446
|
};
|
|
@@ -20073,7 +20452,7 @@ const interactiveSupportsFocus = defineRule({
|
|
|
20073
20452
|
recommendation: "Add keyboard focus support so users can reach interactive elements without a pointer.",
|
|
20074
20453
|
category: "Accessibility",
|
|
20075
20454
|
create: (context) => {
|
|
20076
|
-
const settings = resolveSettings$
|
|
20455
|
+
const settings = resolveSettings$36(context.settings);
|
|
20077
20456
|
const tabbableSet = new Set(settings.tabbable);
|
|
20078
20457
|
return { JSXOpeningElement(node) {
|
|
20079
20458
|
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
@@ -22214,25 +22593,49 @@ const jsLengthCheckFirst = defineRule({
|
|
|
22214
22593
|
});
|
|
22215
22594
|
//#endregion
|
|
22216
22595
|
//#region src/plugin/utils/is-proven-global-namespace-reference.ts
|
|
22596
|
+
const GLOBAL_SELF_PROPERTY_NAMES = new Set([
|
|
22597
|
+
"global",
|
|
22598
|
+
"globalThis",
|
|
22599
|
+
"self",
|
|
22600
|
+
"window"
|
|
22601
|
+
]);
|
|
22217
22602
|
const isProvenGlobalObjectReference = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
22218
|
-
|
|
22219
|
-
|
|
22220
|
-
|
|
22221
|
-
|
|
22222
|
-
|
|
22223
|
-
|
|
22224
|
-
|
|
22603
|
+
let currentExpression = expression;
|
|
22604
|
+
while (true) {
|
|
22605
|
+
const strippedExpression = stripParenExpression(currentExpression);
|
|
22606
|
+
if (!isNodeOfType(strippedExpression, "Identifier")) return false;
|
|
22607
|
+
if ((strippedExpression.name === "globalThis" || strippedExpression.name === "window" || strippedExpression.name === "self" || strippedExpression.name === "global") && scopes.isGlobalReference(strippedExpression)) return true;
|
|
22608
|
+
const symbol = scopes.symbolFor(strippedExpression);
|
|
22609
|
+
if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbolIds.has(symbol.id) || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
22610
|
+
const declaration = symbol.declarationNode;
|
|
22611
|
+
const initializer = declaration.init;
|
|
22612
|
+
if (!initializer) return false;
|
|
22613
|
+
const isDirectIdentifierAlias = isNodeOfType(declaration.id, "Identifier") && declaration.id === symbol.bindingIdentifier && initializer === symbol.initializer;
|
|
22614
|
+
const bindingProperty = symbol.bindingIdentifier.parent;
|
|
22615
|
+
const destructuredPropertyName = getDestructuredBindingPropertyName(symbol.bindingIdentifier);
|
|
22616
|
+
const isDirectGlobalSelfAlias = isNodeOfType(declaration.id, "ObjectPattern") && isNodeOfType(bindingProperty, "Property") && bindingProperty.value === symbol.bindingIdentifier && bindingProperty.parent === declaration.id && destructuredPropertyName !== null && GLOBAL_SELF_PROPERTY_NAMES.has(destructuredPropertyName);
|
|
22617
|
+
if (!isDirectIdentifierAlias && !isDirectGlobalSelfAlias) return false;
|
|
22618
|
+
visitedSymbolIds.add(symbol.id);
|
|
22619
|
+
currentExpression = initializer;
|
|
22620
|
+
}
|
|
22225
22621
|
};
|
|
22226
22622
|
const isProvenGlobalNamespaceReference = (expression, namespaceName, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
22227
|
-
|
|
22228
|
-
|
|
22623
|
+
let currentExpression = expression;
|
|
22624
|
+
while (true) {
|
|
22625
|
+
const strippedExpression = stripParenExpression(currentExpression);
|
|
22626
|
+
if (!isNodeOfType(strippedExpression, "Identifier")) return isNodeOfType(strippedExpression, "MemberExpression") && getStaticPropertyName(strippedExpression) === namespaceName && isProvenGlobalObjectReference(strippedExpression.object, scopes);
|
|
22229
22627
|
if (strippedExpression.name === namespaceName && scopes.isGlobalReference(strippedExpression)) return true;
|
|
22230
22628
|
const symbol = scopes.symbolFor(strippedExpression);
|
|
22231
|
-
if (!symbol
|
|
22629
|
+
if (!symbol || symbol.kind !== "const" || visitedSymbolIds.has(symbol.id)) return false;
|
|
22232
22630
|
visitedSymbolIds.add(symbol.id);
|
|
22233
|
-
|
|
22631
|
+
const declaration = symbol.declarationNode;
|
|
22632
|
+
const bindingIdentifier = symbol.bindingIdentifier;
|
|
22633
|
+
const bindingProperty = bindingIdentifier.parent;
|
|
22634
|
+
if (getDestructuredBindingPropertyName(bindingIdentifier) === namespaceName && isNodeOfType(declaration, "VariableDeclarator") && isNodeOfType(declaration.id, "ObjectPattern") && isNodeOfType(bindingProperty, "Property") && bindingProperty.value === bindingIdentifier && bindingProperty.parent === declaration.id && declaration.init) return isProvenGlobalObjectReference(declaration.init, scopes);
|
|
22635
|
+
const directInitializer = getDirectConstInitializer(symbol);
|
|
22636
|
+
if (!directInitializer) return false;
|
|
22637
|
+
currentExpression = directInitializer;
|
|
22234
22638
|
}
|
|
22235
|
-
return isNodeOfType(strippedExpression, "MemberExpression") && getStaticPropertyName(strippedExpression) === namespaceName && isProvenGlobalObjectReference(strippedExpression.object, scopes);
|
|
22236
22639
|
};
|
|
22237
22640
|
//#endregion
|
|
22238
22641
|
//#region src/plugin/rules/js-performance/js-min-max-loop.ts
|
|
@@ -23345,7 +23748,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
23345
23748
|
const NEVER_MESSAGE$2 = () => "This boolean prop style disagrees with the project setting, so equivalent true props are harder to scan consistently.";
|
|
23346
23749
|
const ALWAYS_MESSAGE$2 = () => "This boolean prop style disagrees with the project setting, so equivalent true props are harder to scan consistently.";
|
|
23347
23750
|
const FALSE_OMITTED_MESSAGE = (attributeName) => `\`${attributeName}={false}\` does nothing, so the explicit false value adds noise without changing output.`;
|
|
23348
|
-
const resolveSettings$
|
|
23751
|
+
const resolveSettings$35 = (settings) => {
|
|
23349
23752
|
const reactDoctor = settings?.["react-doctor"];
|
|
23350
23753
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxBooleanValue ?? {} : {};
|
|
23351
23754
|
return {
|
|
@@ -23363,7 +23766,7 @@ const jsxBooleanValue = defineRule({
|
|
|
23363
23766
|
recommendation: "Use one boolean-attribute style so equivalent true props scan the same across the codebase.",
|
|
23364
23767
|
category: "Architecture",
|
|
23365
23768
|
create: (context) => {
|
|
23366
|
-
const settings = resolveSettings$
|
|
23769
|
+
const settings = resolveSettings$35(context.settings);
|
|
23367
23770
|
const alwaysSet = new Set(settings.always);
|
|
23368
23771
|
const neverSet = new Set(settings.never);
|
|
23369
23772
|
return { JSXAttribute(node) {
|
|
@@ -23417,7 +23820,7 @@ const jsxBooleanValue = defineRule({
|
|
|
23417
23820
|
const UNNECESSARY_BRACES_MESSAGE = "These curly braces wrap a literal value, so they add JSX noise without changing output.";
|
|
23418
23821
|
const REQUIRED_BRACES_MESSAGE = "This JSX value needs `{ }` so React reads it as an expression instead of text.";
|
|
23419
23822
|
const isAllowedMode = (value) => value === "always" || value === "never" || value === "ignore";
|
|
23420
|
-
const resolveSettings$
|
|
23823
|
+
const resolveSettings$34 = (settings) => {
|
|
23421
23824
|
const reactDoctor = settings?.["react-doctor"];
|
|
23422
23825
|
const ruleSettingsRaw = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxCurlyBracePresence : void 0;
|
|
23423
23826
|
if (isAllowedMode(ruleSettingsRaw)) return {
|
|
@@ -23513,7 +23916,7 @@ const jsxCurlyBracePresence = defineRule({
|
|
|
23513
23916
|
recommendation: "Use one JSX literal style so equivalent markup scans the same without extra JSX noise.",
|
|
23514
23917
|
category: "Architecture",
|
|
23515
23918
|
create: (context) => {
|
|
23516
|
-
const settings = resolveSettings$
|
|
23919
|
+
const settings = resolveSettings$34(context.settings);
|
|
23517
23920
|
return {
|
|
23518
23921
|
JSXAttribute(node) {
|
|
23519
23922
|
const value = node.value;
|
|
@@ -23596,7 +23999,7 @@ const containsJsxElement = (root) => {
|
|
|
23596
23999
|
//#region src/plugin/rules/react-builtins/jsx-filename-extension.ts
|
|
23597
24000
|
const JSX_NOT_ALLOWED = (extension) => `This file contains JSX but uses a \`${extension}\` name, so the filename no longer signals JSX to readers or tooling conventions.`;
|
|
23598
24001
|
const EXTENSION_ONLY_FOR_JSX = (extension) => `\`${extension}\` files are reserved for JSX here, so using that extension without JSX makes file-type conventions less useful.`;
|
|
23599
|
-
const resolveSettings$
|
|
24002
|
+
const resolveSettings$33 = (settings) => {
|
|
23600
24003
|
const reactDoctor = settings?.["react-doctor"];
|
|
23601
24004
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFilenameExtension ?? {} : {};
|
|
23602
24005
|
return {
|
|
@@ -23618,7 +24021,7 @@ const jsxFilenameExtension = defineRule({
|
|
|
23618
24021
|
recommendation: "Name JSX files with the configured extension so file names accurately signal which files contain JSX.",
|
|
23619
24022
|
category: "Architecture",
|
|
23620
24023
|
create: (context) => {
|
|
23621
|
-
const settings = resolveSettings$
|
|
24024
|
+
const settings = resolveSettings$33(context.settings);
|
|
23622
24025
|
const allowedExtensions = normalizeExtensions(settings.extensions);
|
|
23623
24026
|
const filename = normalizeFilename(context.filename ?? "fixture.tsx");
|
|
23624
24027
|
const extensionOnly = path.extname(filename).slice(1);
|
|
@@ -23659,7 +24062,7 @@ const jsxFilenameExtension = defineRule({
|
|
|
23659
24062
|
//#region src/plugin/rules/react-builtins/jsx-fragments.ts
|
|
23660
24063
|
const SYNTAX_MESSAGE = "`<React.Fragment>` is used where shorthand fragments are configured, so similar wrappers look different across the codebase.";
|
|
23661
24064
|
const ELEMENT_MESSAGE = "Fragment shorthand is used where explicit fragments are configured, so similar wrappers look different across the codebase.";
|
|
23662
|
-
const resolveSettings$
|
|
24065
|
+
const resolveSettings$32 = (settings) => {
|
|
23663
24066
|
const reactDoctor = settings?.["react-doctor"];
|
|
23664
24067
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFragments ?? {} : {}).mode ?? "syntax" };
|
|
23665
24068
|
};
|
|
@@ -23671,7 +24074,7 @@ const jsxFragments = defineRule({
|
|
|
23671
24074
|
recommendation: "Use one fragment style so identical wrappers do not look different across files.",
|
|
23672
24075
|
category: "Architecture",
|
|
23673
24076
|
create: (context) => {
|
|
23674
|
-
const { mode } = resolveSettings$
|
|
24077
|
+
const { mode } = resolveSettings$32(context.settings);
|
|
23675
24078
|
return {
|
|
23676
24079
|
JSXElement(node) {
|
|
23677
24080
|
if (mode !== "syntax") return;
|
|
@@ -23715,7 +24118,7 @@ const buildPropRegex = (handlerPropPrefix) => {
|
|
|
23715
24118
|
const escaped = tokens.map((token) => token.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
23716
24119
|
return new RegExp(`^(${escaped})[A-Z].*$`);
|
|
23717
24120
|
};
|
|
23718
|
-
const resolveSettings$
|
|
24121
|
+
const resolveSettings$31 = (settings) => {
|
|
23719
24122
|
const reactDoctor = settings?.["react-doctor"];
|
|
23720
24123
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxHandlerNames ?? {} : {};
|
|
23721
24124
|
let handlerPrefix = DEFAULT_HANDLER_PREFIX;
|
|
@@ -23791,7 +24194,7 @@ const jsxHandlerNames = defineRule({
|
|
|
23791
24194
|
recommendation: "Use the `on…` prefix for event-handler props and `handle…` for handlers so readers can trace event flow.",
|
|
23792
24195
|
category: "Architecture",
|
|
23793
24196
|
create: (context) => {
|
|
23794
|
-
const settings = resolveSettings$
|
|
24197
|
+
const settings = resolveSettings$31(context.settings);
|
|
23795
24198
|
return { JSXAttribute(node) {
|
|
23796
24199
|
if (settings.ignoreComponentNames.length > 0) {
|
|
23797
24200
|
const opening = node.parent;
|
|
@@ -23866,6 +24269,19 @@ const hasJsxKeyAttribute = (openingElement) => {
|
|
|
23866
24269
|
return false;
|
|
23867
24270
|
};
|
|
23868
24271
|
//#endregion
|
|
24272
|
+
//#region src/plugin/utils/is-component-function.ts
|
|
24273
|
+
const isFunctionAssignedToComponent = (functionNode) => {
|
|
24274
|
+
let cursor = functionNode.parent ?? null;
|
|
24275
|
+
while (isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
|
|
24276
|
+
if (isNodeOfType(cursor, "VariableDeclarator") && isNodeOfType(cursor.id, "Identifier") && isUppercaseName(cursor.id.name)) return true;
|
|
24277
|
+
return isNodeOfType(cursor, "ExportDefaultDeclaration");
|
|
24278
|
+
};
|
|
24279
|
+
const isComponentFunction$1 = (node) => {
|
|
24280
|
+
if (!isFunctionLike$1(node)) return false;
|
|
24281
|
+
if (isNodeOfType(node, "FunctionDeclaration")) return !node.id || node.id.name === "default" || isUppercaseName(node.id.name) || isNodeOfType(node.parent, "ExportDefaultDeclaration");
|
|
24282
|
+
return isFunctionAssignedToComponent(node);
|
|
24283
|
+
};
|
|
24284
|
+
//#endregion
|
|
23869
24285
|
//#region src/plugin/utils/is-const-declared-binding.ts
|
|
23870
24286
|
const BINDING_PATTERN_ANCESTOR_TYPES = new Set([
|
|
23871
24287
|
"VariableDeclarator",
|
|
@@ -23910,11 +24326,17 @@ const ITERATOR_METHOD_NAMES$1 = new Set([
|
|
|
23910
24326
|
"flatMap",
|
|
23911
24327
|
"from"
|
|
23912
24328
|
]);
|
|
24329
|
+
const RENDERING_CALL_NAMES = new Set([
|
|
24330
|
+
"createPortal",
|
|
24331
|
+
"hydrate",
|
|
24332
|
+
"hydrateRoot",
|
|
24333
|
+
"render"
|
|
24334
|
+
]);
|
|
23913
24335
|
const MISSING_KEY_ARRAY = "Your users can see the wrong data when this array reorders.";
|
|
23914
24336
|
const MISSING_KEY_ITERATOR = "Your users can see the wrong data when this list reorders.";
|
|
23915
|
-
const
|
|
24337
|
+
const KEY_BEFORE_SPREAD = "Place this `key` after the `{...spread}` so the spread cannot override it.";
|
|
23916
24338
|
const DUPLICATE_KEY = (keyValue) => `Your users can see the wrong data because two elements share the key "${keyValue}".`;
|
|
23917
|
-
const resolveSettings$
|
|
24339
|
+
const resolveSettings$30 = (settings) => {
|
|
23918
24340
|
const reactDoctor = settings?.["react-doctor"];
|
|
23919
24341
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxKey ?? {} : {};
|
|
23920
24342
|
return {
|
|
@@ -23928,6 +24350,33 @@ const findArrayVariableDeclarator = (arrayExpression) => {
|
|
|
23928
24350
|
if (ancestor && isNodeOfType(ancestor, "VariableDeclarator") && ancestor.init === wrapped && isNodeOfType(ancestor.id, "Identifier")) return ancestor;
|
|
23929
24351
|
return null;
|
|
23930
24352
|
};
|
|
24353
|
+
const isArrayNestedInObjectProperty = (arrayExpression) => {
|
|
24354
|
+
let current = findTransparentExpressionRoot(arrayExpression);
|
|
24355
|
+
while (current.parent) {
|
|
24356
|
+
const parent = current.parent;
|
|
24357
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === current || parent.alternate === current) || isNodeOfType(parent, "LogicalExpression") && (parent.left === current || parent.right === current)) {
|
|
24358
|
+
current = findTransparentExpressionRoot(parent);
|
|
24359
|
+
continue;
|
|
24360
|
+
}
|
|
24361
|
+
return isNodeOfType(parent, "Property") && parent.value === current;
|
|
24362
|
+
}
|
|
24363
|
+
return false;
|
|
24364
|
+
};
|
|
24365
|
+
const isArrayPassedToNonRenderingCall = (arrayExpression, scopes) => {
|
|
24366
|
+
const transparentArray = findTransparentExpressionRoot(arrayExpression);
|
|
24367
|
+
const callExpression = transparentArray.parent;
|
|
24368
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression") || !callExpression.arguments.some((argument) => argument === transparentArray)) return false;
|
|
24369
|
+
const argumentIndex = callExpression.arguments.findIndex((argument) => argument === transparentArray);
|
|
24370
|
+
if (isReactApiCall(callExpression, "createElement", scopes, {
|
|
24371
|
+
allowGlobalReactNamespace: true,
|
|
24372
|
+
allowUnboundBareCalls: true,
|
|
24373
|
+
resolveNamedAliases: true
|
|
24374
|
+
})) return argumentIndex < 2;
|
|
24375
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
24376
|
+
if (isNodeOfType(callee, "Identifier")) return !RENDERING_CALL_NAMES.has(callee.name);
|
|
24377
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return !RENDERING_CALL_NAMES.has(callee.property.name);
|
|
24378
|
+
return true;
|
|
24379
|
+
};
|
|
23931
24380
|
const isIdentityIteratorCallback = (callback) => {
|
|
23932
24381
|
const callbackFunction = stripParenExpression(callback);
|
|
23933
24382
|
if (!isNodeOfType(callbackFunction, "ArrowFunctionExpression") && !isNodeOfType(callbackFunction, "FunctionExpression")) return true;
|
|
@@ -24024,7 +24473,7 @@ const findNamedCallbackIteratorCall = (functionNode) => {
|
|
|
24024
24473
|
namedCallbackIteratorCallCache.set(functionNode, iteratorCall);
|
|
24025
24474
|
return iteratorCall;
|
|
24026
24475
|
};
|
|
24027
|
-
const findEnclosingIteratorContext = (jsxNode) => {
|
|
24476
|
+
const findEnclosingIteratorContext = (jsxNode, scopes) => {
|
|
24028
24477
|
let current = jsxNode;
|
|
24029
24478
|
let isOutsideContainingFunction = false;
|
|
24030
24479
|
let didSeeReturnStatement = false;
|
|
@@ -24046,6 +24495,8 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
24046
24495
|
isOutsideContainingFunction = true;
|
|
24047
24496
|
} else if (isNodeOfType(parent, "ArrayExpression")) {
|
|
24048
24497
|
if (isOutsideContainingFunction) return null;
|
|
24498
|
+
if (isArrayNestedInObjectProperty(parent)) return null;
|
|
24499
|
+
if (isArrayPassedToNonRenderingCall(parent, scopes)) return null;
|
|
24049
24500
|
const arrayParent = parent.parent;
|
|
24050
24501
|
if (arrayParent && isNodeOfType(arrayParent, "Property")) return null;
|
|
24051
24502
|
if (arrayParent && isNodeOfType(arrayParent, "ArrayExpression")) return null;
|
|
@@ -24127,9 +24578,15 @@ const isWithinChildrenToArray = (jsxNode) => {
|
|
|
24127
24578
|
const isKeyPropertyName = (propertyKey) => {
|
|
24128
24579
|
if (isNodeOfType(propertyKey, "Identifier")) return propertyKey.name === "key";
|
|
24129
24580
|
if (isNodeOfType(propertyKey, "Literal")) return String(propertyKey.value) === "key";
|
|
24130
|
-
return
|
|
24581
|
+
return false;
|
|
24131
24582
|
};
|
|
24132
24583
|
const isThisPropsMember = (expression) => isNodeOfType(expression, "MemberExpression") && !expression.computed && isNodeOfType(expression.object, "ThisExpression") && isNodeOfType(expression.property, "Identifier") && expression.property.name === "props";
|
|
24584
|
+
const isDirectComponentPropsParameter = (bindingIdentifier, functionNode) => {
|
|
24585
|
+
if (!isFunctionLike$1(functionNode) || !isComponentFunction$1(functionNode)) return false;
|
|
24586
|
+
const firstParameter = functionNode.params?.[0];
|
|
24587
|
+
if (!firstParameter) return false;
|
|
24588
|
+
return (isNodeOfType(firstParameter, "AssignmentPattern") ? firstParameter.left : firstParameter) === bindingIdentifier;
|
|
24589
|
+
};
|
|
24133
24590
|
const isFunctionParameterPropsRest = (bindingIdentifier) => {
|
|
24134
24591
|
const restElement = bindingIdentifier.parent;
|
|
24135
24592
|
if (!restElement || !isNodeOfType(restElement, "RestElement")) return false;
|
|
@@ -24139,7 +24596,7 @@ const isFunctionParameterPropsRest = (bindingIdentifier) => {
|
|
|
24139
24596
|
const parameterNode = patternParent && isNodeOfType(patternParent, "AssignmentPattern") && patternParent.left === objectPattern ? patternParent : objectPattern;
|
|
24140
24597
|
const functionNode = parameterNode.parent;
|
|
24141
24598
|
if (!functionNode || !isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "FunctionDeclaration")) return false;
|
|
24142
|
-
return functionNode
|
|
24599
|
+
return isComponentFunction$1(functionNode) && functionNode.params[0] === parameterNode;
|
|
24143
24600
|
};
|
|
24144
24601
|
const isRestBindingWithKeyExtracted = (bindingIdentifier) => {
|
|
24145
24602
|
const restElement = bindingIdentifier.parent;
|
|
@@ -24152,14 +24609,14 @@ const isRestBindingWithKeyExtracted = (bindingIdentifier) => {
|
|
|
24152
24609
|
}
|
|
24153
24610
|
return false;
|
|
24154
24611
|
};
|
|
24155
|
-
const
|
|
24156
|
-
let
|
|
24612
|
+
const isBindingAssignedKey = (scopeOwner, bindingName) => {
|
|
24613
|
+
let didFindKeyAssignment = false;
|
|
24157
24614
|
walkAst(scopeOwner, (node) => {
|
|
24158
|
-
if (
|
|
24615
|
+
if (didFindKeyAssignment) return false;
|
|
24159
24616
|
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression")) {
|
|
24160
24617
|
const assignedObject = stripParenExpression(node.left.object);
|
|
24161
|
-
if (isNodeOfType(assignedObject, "Identifier") && assignedObject.name === bindingName) {
|
|
24162
|
-
|
|
24618
|
+
if (isNodeOfType(assignedObject, "Identifier") && assignedObject.name === bindingName && isKeyPropertyName(node.left.property)) {
|
|
24619
|
+
didFindKeyAssignment = true;
|
|
24163
24620
|
return false;
|
|
24164
24621
|
}
|
|
24165
24622
|
}
|
|
@@ -24167,59 +24624,63 @@ const isBindingObjectMutated = (scopeOwner, bindingName) => {
|
|
|
24167
24624
|
const callee = node.callee;
|
|
24168
24625
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Object" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "assign") {
|
|
24169
24626
|
const firstArgument = node.arguments[0];
|
|
24170
|
-
if (firstArgument && isNodeOfType(firstArgument, "Identifier") && firstArgument.name === bindingName) {
|
|
24171
|
-
|
|
24172
|
-
|
|
24627
|
+
if (firstArgument && isNodeOfType(firstArgument, "Identifier") && firstArgument.name === bindingName) for (const sourceArgument of node.arguments.slice(1)) {
|
|
24628
|
+
const source = stripParenExpression(sourceArgument);
|
|
24629
|
+
if (!isNodeOfType(source, "ObjectExpression")) continue;
|
|
24630
|
+
if (source.properties.some((property) => isNodeOfType(property, "Property") && isKeyPropertyName(property.key))) {
|
|
24631
|
+
didFindKeyAssignment = true;
|
|
24632
|
+
return false;
|
|
24633
|
+
}
|
|
24173
24634
|
}
|
|
24174
24635
|
}
|
|
24175
24636
|
}
|
|
24176
24637
|
});
|
|
24177
|
-
return
|
|
24638
|
+
return didFindKeyAssignment;
|
|
24178
24639
|
};
|
|
24179
|
-
const
|
|
24640
|
+
const spreadExpressionHasKey = (expression, depth) => {
|
|
24180
24641
|
const inner = stripParenExpression(expression);
|
|
24181
24642
|
if (isNodeOfType(inner, "ObjectExpression")) {
|
|
24182
24643
|
for (const property of inner.properties) {
|
|
24183
24644
|
if (isNodeOfType(property, "SpreadElement")) {
|
|
24184
|
-
if (depth >= 3)
|
|
24185
|
-
if (
|
|
24645
|
+
if (depth >= 3) continue;
|
|
24646
|
+
if (spreadExpressionHasKey(property.argument, depth + 1)) return true;
|
|
24186
24647
|
continue;
|
|
24187
24648
|
}
|
|
24188
|
-
if (!isNodeOfType(property, "Property"))
|
|
24189
|
-
if (property.computed) return true;
|
|
24649
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
24190
24650
|
if (isKeyPropertyName(property.key)) return true;
|
|
24191
24651
|
}
|
|
24192
24652
|
return false;
|
|
24193
24653
|
}
|
|
24194
24654
|
if (isNodeOfType(inner, "ConditionalExpression")) {
|
|
24195
|
-
if (depth >= 3) return
|
|
24196
|
-
return
|
|
24655
|
+
if (depth >= 3) return false;
|
|
24656
|
+
return spreadExpressionHasKey(inner.consequent, depth + 1) || spreadExpressionHasKey(inner.alternate, depth + 1);
|
|
24197
24657
|
}
|
|
24198
24658
|
if (isNodeOfType(inner, "LogicalExpression")) {
|
|
24199
|
-
if (depth >= 3) return
|
|
24200
|
-
if (inner.operator === "&&") return
|
|
24201
|
-
return
|
|
24659
|
+
if (depth >= 3) return false;
|
|
24660
|
+
if (inner.operator === "&&") return spreadExpressionHasKey(inner.right, depth + 1);
|
|
24661
|
+
return spreadExpressionHasKey(inner.left, depth + 1) || spreadExpressionHasKey(inner.right, depth + 1);
|
|
24202
24662
|
}
|
|
24203
24663
|
if (isNodeOfType(inner, "Literal")) return false;
|
|
24204
24664
|
if (isThisPropsMember(inner)) return false;
|
|
24205
24665
|
if (isNodeOfType(inner, "Identifier")) {
|
|
24206
24666
|
if (inner.name === "undefined") return false;
|
|
24207
|
-
if (depth >= 3) return
|
|
24667
|
+
if (depth >= 3) return false;
|
|
24208
24668
|
const binding = findVariableInitializer(inner, inner.name);
|
|
24209
|
-
if (!binding) return
|
|
24669
|
+
if (!binding) return false;
|
|
24210
24670
|
if (isRestBindingWithKeyExtracted(binding.bindingIdentifier)) return false;
|
|
24211
24671
|
if (isFunctionParameterPropsRest(binding.bindingIdentifier)) return false;
|
|
24212
|
-
if (
|
|
24213
|
-
if (
|
|
24214
|
-
|
|
24672
|
+
if (isDirectComponentPropsParameter(binding.bindingIdentifier, binding.scopeOwner)) return false;
|
|
24673
|
+
if (!isConstDeclaredBinding(binding) || !binding.initializer) return false;
|
|
24674
|
+
if (isBindingAssignedKey(binding.scopeOwner, inner.name)) return true;
|
|
24675
|
+
return spreadExpressionHasKey(binding.initializer, depth + 1);
|
|
24215
24676
|
}
|
|
24216
|
-
return
|
|
24677
|
+
return false;
|
|
24217
24678
|
};
|
|
24218
|
-
const spreadCanOverwriteKey = (spreadAttribute) =>
|
|
24679
|
+
const spreadCanOverwriteKey = (spreadAttribute) => spreadExpressionHasKey(spreadAttribute.argument, 0);
|
|
24219
24680
|
const checkKeyBeforeSpread = (context, openingElement) => {
|
|
24220
24681
|
let keyIndex = null;
|
|
24221
24682
|
let keyAttribute = null;
|
|
24222
|
-
let
|
|
24683
|
+
let lastKeyCarryingSpreadIndex = null;
|
|
24223
24684
|
for (let attributeIndex = 0; attributeIndex < openingElement.attributes.length; attributeIndex++) {
|
|
24224
24685
|
const attribute = openingElement.attributes[attributeIndex];
|
|
24225
24686
|
if (isNodeOfType(attribute, "JSXAttribute")) {
|
|
@@ -24227,11 +24688,11 @@ const checkKeyBeforeSpread = (context, openingElement) => {
|
|
|
24227
24688
|
keyIndex = attributeIndex;
|
|
24228
24689
|
keyAttribute = attribute;
|
|
24229
24690
|
}
|
|
24230
|
-
} else if (isNodeOfType(attribute, "JSXSpreadAttribute") &&
|
|
24691
|
+
} else if (isNodeOfType(attribute, "JSXSpreadAttribute") && spreadCanOverwriteKey(attribute)) lastKeyCarryingSpreadIndex = attributeIndex;
|
|
24231
24692
|
}
|
|
24232
|
-
if (keyIndex !== null &&
|
|
24693
|
+
if (keyIndex !== null && lastKeyCarryingSpreadIndex !== null && lastKeyCarryingSpreadIndex > keyIndex && keyAttribute) context.report({
|
|
24233
24694
|
node: keyAttribute,
|
|
24234
|
-
message:
|
|
24695
|
+
message: KEY_BEFORE_SPREAD
|
|
24235
24696
|
});
|
|
24236
24697
|
};
|
|
24237
24698
|
const getKeyAttributeValueString = (openingElement) => {
|
|
@@ -24275,7 +24736,7 @@ const jsxKey = defineRule({
|
|
|
24275
24736
|
severity: "error",
|
|
24276
24737
|
recommendation: "Add a stable `key` prop so React can keep list items matched to the right data when the list changes.",
|
|
24277
24738
|
create: (context) => {
|
|
24278
|
-
const settings = resolveSettings$
|
|
24739
|
+
const settings = resolveSettings$30(context.settings);
|
|
24279
24740
|
return {
|
|
24280
24741
|
JSXElement(node) {
|
|
24281
24742
|
const openingElement = node.openingElement;
|
|
@@ -24293,7 +24754,7 @@ const jsxKey = defineRule({
|
|
|
24293
24754
|
else seenKeys.add(keyValue.keyValue);
|
|
24294
24755
|
}
|
|
24295
24756
|
}
|
|
24296
|
-
const enclosingContext = findEnclosingIteratorContext(node);
|
|
24757
|
+
const enclosingContext = findEnclosingIteratorContext(node, context.scopes);
|
|
24297
24758
|
if (!enclosingContext) return;
|
|
24298
24759
|
if (isWithinChildrenToArray(node)) return;
|
|
24299
24760
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
@@ -24329,7 +24790,7 @@ const jsxKey = defineRule({
|
|
|
24329
24790
|
//#region src/plugin/rules/react-builtins/jsx-max-depth.ts
|
|
24330
24791
|
const buildMessage$19 = (depth, max) => `This JSX is hard to read at ${depth} levels deep, past the limit of ${max}.`;
|
|
24331
24792
|
const DEFAULT_MAX_DEPTH = 14;
|
|
24332
|
-
const resolveSettings$
|
|
24793
|
+
const resolveSettings$29 = (settings) => {
|
|
24333
24794
|
const reactDoctor = settings?.["react-doctor"];
|
|
24334
24795
|
return { max: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxMaxDepth ?? {} : {}).max ?? DEFAULT_MAX_DEPTH };
|
|
24335
24796
|
};
|
|
@@ -24398,7 +24859,7 @@ const jsxMaxDepth = defineRule({
|
|
|
24398
24859
|
recommendation: "Pull deeply nested JSX into smaller components so it's easier to read.",
|
|
24399
24860
|
category: "Architecture",
|
|
24400
24861
|
create: (context) => {
|
|
24401
|
-
const { max } = resolveSettings$
|
|
24862
|
+
const { max } = resolveSettings$29(context.settings);
|
|
24402
24863
|
const deepestLeafPerTree = /* @__PURE__ */ new Map();
|
|
24403
24864
|
const checkNode = (node) => {
|
|
24404
24865
|
if (!isLeafJsxNode(node)) return;
|
|
@@ -24587,9 +25048,14 @@ const collectContextBindings = (programRoot) => {
|
|
|
24587
25048
|
}
|
|
24588
25049
|
return bindings;
|
|
24589
25050
|
};
|
|
24590
|
-
const isCreateContextBindingJsxName = (node, contextBindings) => {
|
|
25051
|
+
const isCreateContextBindingJsxName = (node, contextBindings, scopes) => {
|
|
24591
25052
|
if (!isNodeOfType(node, "JSXIdentifier")) return false;
|
|
24592
|
-
if (!contextBindings.has(node.name))
|
|
25053
|
+
if (!contextBindings.has(node.name)) {
|
|
25054
|
+
const symbol = scopes.symbolFor(node);
|
|
25055
|
+
if (!symbol || symbol.kind !== "import") return false;
|
|
25056
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
25057
|
+
return getImportSourceForName(node, node.name)?.toLowerCase().includes("context") === true && (node.name.endsWith("Context") || importedName?.endsWith("Context") === true);
|
|
25058
|
+
}
|
|
24593
25059
|
const binding = findVariableInitializer(node, node.name);
|
|
24594
25060
|
if (!binding) return false;
|
|
24595
25061
|
return binding.scopeOwner.type === "Program";
|
|
@@ -24613,7 +25079,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
24613
25079
|
if (isTestlikeFile) return;
|
|
24614
25080
|
const nameNode = node.name;
|
|
24615
25081
|
const isLegacyProvider = isProviderMemberName(nameNode);
|
|
24616
|
-
const isReact19Shorthand = isCreateContextBindingJsxName(nameNode, contextBindings);
|
|
25082
|
+
const isReact19Shorthand = isCreateContextBindingJsxName(nameNode, contextBindings, context.scopes);
|
|
24617
25083
|
if (!isLegacyProvider && !isReact19Shorthand) return;
|
|
24618
25084
|
if (!isInsideFunctionScope(node)) return;
|
|
24619
25085
|
for (const attribute of node.attributes) {
|
|
@@ -24884,6 +25350,29 @@ const buildSameFileJsxSlotPropRegistry = (program, memoRegistry, scopes) => {
|
|
|
24884
25350
|
return registry;
|
|
24885
25351
|
};
|
|
24886
25352
|
//#endregion
|
|
25353
|
+
//#region src/plugin/utils/has-custom-memo-comparator.ts
|
|
25354
|
+
const MEMO_CALLEE_NAMES$1 = new Set(["memo", "React.memo"]);
|
|
25355
|
+
const isIdentitySensitiveMemoComparator = (comparatorNode, scopes) => {
|
|
25356
|
+
if (isNodeOfType(comparatorNode, "Identifier")) {
|
|
25357
|
+
if (comparatorNode.name === "undefined") return scopes.isGlobalReference(comparatorNode);
|
|
25358
|
+
return getImportedNameFromModule(comparatorNode, comparatorNode.name, "react-redux") === "shallowEqual";
|
|
25359
|
+
}
|
|
25360
|
+
if (!isNodeOfType(comparatorNode, "MemberExpression") || comparatorNode.computed || !isNodeOfType(comparatorNode.object, "Identifier") || !isNodeOfType(comparatorNode.property, "Identifier") || comparatorNode.property.name !== "shallowEqual") return false;
|
|
25361
|
+
return isNamespaceImportFromModule$1(comparatorNode, comparatorNode.object.name, "react-redux");
|
|
25362
|
+
};
|
|
25363
|
+
const hasCustomMemoComparator = (openingName, scopes) => {
|
|
25364
|
+
if (!openingName || !isNodeOfType(openingName, "JSXIdentifier")) return false;
|
|
25365
|
+
const binding = findVariableInitializer(openingName, openingName.name);
|
|
25366
|
+
if (!binding || !binding.initializer) return false;
|
|
25367
|
+
const initializer = binding.initializer;
|
|
25368
|
+
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
25369
|
+
const calleeName = flattenCalleeName(initializer.callee);
|
|
25370
|
+
if (calleeName === null || !MEMO_CALLEE_NAMES$1.has(calleeName)) return false;
|
|
25371
|
+
const comparatorNode = (initializer.arguments ?? [])[1];
|
|
25372
|
+
if (!comparatorNode) return false;
|
|
25373
|
+
return !isIdentitySensitiveMemoComparator(comparatorNode, scopes);
|
|
25374
|
+
};
|
|
25375
|
+
//#endregion
|
|
24887
25376
|
//#region src/plugin/utils/is-on-intrinsic-html-element.ts
|
|
24888
25377
|
const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
24889
25378
|
const openingElement = attribute.parent;
|
|
@@ -25169,6 +25658,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
25169
25658
|
const parentJsxOpening = node.parent;
|
|
25170
25659
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
25171
25660
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
25661
|
+
if (hasCustomMemoComparator(openingName, context.scopes)) return;
|
|
25172
25662
|
const openingSymbol = openingName && isNodeOfType(openingName, "JSXIdentifier") ? context.scopes.symbolFor(openingName) : null;
|
|
25173
25663
|
if (openingSymbol && isNodeOfType(node.name, "JSXIdentifier") && jsxSlotPropRegistry?.get(openingSymbol.id)?.has(node.name.name)) return;
|
|
25174
25664
|
if (isNodeOfType(node.name, "JSXIdentifier") && isSlotPropName(node.name.name)) return;
|
|
@@ -25188,19 +25678,6 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
25188
25678
|
}
|
|
25189
25679
|
});
|
|
25190
25680
|
//#endregion
|
|
25191
|
-
//#region src/plugin/utils/has-custom-memo-comparator.ts
|
|
25192
|
-
const MEMO_CALLEE_NAMES = new Set(["memo", "React.memo"]);
|
|
25193
|
-
const hasCustomMemoComparator = (openingName) => {
|
|
25194
|
-
if (!openingName || !isNodeOfType(openingName, "JSXIdentifier")) return false;
|
|
25195
|
-
const binding = findVariableInitializer(openingName, openingName.name);
|
|
25196
|
-
if (!binding || !binding.initializer) return false;
|
|
25197
|
-
const initializer = binding.initializer;
|
|
25198
|
-
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
25199
|
-
const calleeName = flattenCalleeName(initializer.callee);
|
|
25200
|
-
if (calleeName === null || !MEMO_CALLEE_NAMES.has(calleeName)) return false;
|
|
25201
|
-
return (initializer.arguments ?? []).length >= 2;
|
|
25202
|
-
};
|
|
25203
|
-
//#endregion
|
|
25204
25681
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop-tables.ts
|
|
25205
25682
|
const DATA_ARRAY_PROP_NAMES = new Set([
|
|
25206
25683
|
"data",
|
|
@@ -25559,7 +26036,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
25559
26036
|
const parentJsxOpening = node.parent;
|
|
25560
26037
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
25561
26038
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
25562
|
-
if (hasCustomMemoComparator(openingName)) return;
|
|
26039
|
+
if (hasCustomMemoComparator(openingName, context.scopes)) return;
|
|
25563
26040
|
if (isNodeOfType(node.name, "JSXIdentifier") && isDataArrayPropName(node.name.name)) return;
|
|
25564
26041
|
if (!isInsideFunctionScope(node)) return;
|
|
25565
26042
|
const value = node.value;
|
|
@@ -26023,6 +26500,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
26023
26500
|
const parentJsxOpening = node.parent;
|
|
26024
26501
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
26025
26502
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
26503
|
+
if (hasCustomMemoComparator(openingName, context.scopes)) return;
|
|
26026
26504
|
if (isNodeOfType(node.name, "JSXIdentifier") && isOneShotHandlerName(node.name.name)) return;
|
|
26027
26505
|
if (!isInsideFunctionScope(node)) return;
|
|
26028
26506
|
const value = node.value;
|
|
@@ -26330,7 +26808,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
26330
26808
|
const parentJsxOpening = node.parent;
|
|
26331
26809
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
26332
26810
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
26333
|
-
if (hasCustomMemoComparator(openingName)) return;
|
|
26811
|
+
if (hasCustomMemoComparator(openingName, context.scopes)) return;
|
|
26334
26812
|
if (!isInsideFunctionScope(node)) return;
|
|
26335
26813
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
26336
26814
|
if (ALWAYS_FRESH_OBJECT_PROPS.has(node.name.name)) return;
|
|
@@ -26353,7 +26831,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
26353
26831
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
26354
26832
|
const MESSAGE$43 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
26355
26833
|
const JAVASCRIPT_URL_PATTERN = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
|
26356
|
-
const resolveSettings$
|
|
26834
|
+
const resolveSettings$28 = (settings) => {
|
|
26357
26835
|
const reactDoctor = settings?.["react-doctor"];
|
|
26358
26836
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
26359
26837
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
@@ -26375,7 +26853,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
26375
26853
|
recommendation: "Use real event handlers instead of `javascript:` URLs so injected URL text cannot execute as code.",
|
|
26376
26854
|
category: "Security",
|
|
26377
26855
|
create: (context) => {
|
|
26378
|
-
const options = resolveSettings$
|
|
26856
|
+
const options = resolveSettings$28(context.settings);
|
|
26379
26857
|
return { JSXOpeningElement(node) {
|
|
26380
26858
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
26381
26859
|
const elementName = resolveJsxElementType(node);
|
|
@@ -26396,283 +26874,6 @@ const jsxNoScriptUrl = defineRule({
|
|
|
26396
26874
|
}
|
|
26397
26875
|
});
|
|
26398
26876
|
//#endregion
|
|
26399
|
-
//#region src/plugin/rules/react-builtins/jsx-no-target-blank.ts
|
|
26400
|
-
const NOREFERRER_MESSAGE = "`target=\"_blank\"` without `rel=\"noreferrer\"` lets the linked page hijack your tab to a phishing site.";
|
|
26401
|
-
const NOOPENER_MESSAGE = "`target=\"_blank\"` without `rel` lets the linked page hijack your tab to a phishing site.";
|
|
26402
|
-
const SPREAD_MESSAGE = "A spread here can add `target=\"_blank\"`, letting the linked page hijack your tab to a phishing site.";
|
|
26403
|
-
const resolveSettings$28 = (settings) => {
|
|
26404
|
-
const reactDoctor = settings?.["react-doctor"];
|
|
26405
|
-
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxNoTargetBlank ?? {} : {};
|
|
26406
|
-
const reactSettings = typeof settings?.react === "object" && settings.react !== null ? settings.react : {};
|
|
26407
|
-
const linkComponents = /* @__PURE__ */ new Map();
|
|
26408
|
-
for (const entry of reactSettings.linkComponents ?? []) if (typeof entry === "string") linkComponents.set(entry, ["href"]);
|
|
26409
|
-
else if (typeof entry === "object" && entry !== null) {
|
|
26410
|
-
const linkAttribute = entry.linkAttribute ?? "href";
|
|
26411
|
-
linkComponents.set(entry.name, Array.isArray(linkAttribute) ? linkAttribute : [linkAttribute]);
|
|
26412
|
-
}
|
|
26413
|
-
const formComponents = /* @__PURE__ */ new Map();
|
|
26414
|
-
for (const entry of reactSettings.formComponents ?? []) if (typeof entry === "string") formComponents.set(entry, ["action"]);
|
|
26415
|
-
else if (typeof entry === "object" && entry !== null) {
|
|
26416
|
-
const formAttribute = entry.formAttribute ?? "action";
|
|
26417
|
-
formComponents.set(entry.name, Array.isArray(formAttribute) ? formAttribute : [formAttribute]);
|
|
26418
|
-
}
|
|
26419
|
-
return {
|
|
26420
|
-
enforceDynamicLinks: ruleSettings.enforceDynamicLinks ?? "always",
|
|
26421
|
-
warnOnSpreadAttributes: ruleSettings.warnOnSpreadAttributes ?? false,
|
|
26422
|
-
allowReferrer: ruleSettings.allowReferrer ?? false,
|
|
26423
|
-
links: ruleSettings.links ?? true,
|
|
26424
|
-
forms: ruleSettings.forms ?? false,
|
|
26425
|
-
linkComponents,
|
|
26426
|
-
formComponents
|
|
26427
|
-
};
|
|
26428
|
-
};
|
|
26429
|
-
const isExternalLink = (href) => href.includes("//");
|
|
26430
|
-
const matchHrefExpression = (expression, state) => {
|
|
26431
|
-
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") {
|
|
26432
|
-
if (isExternalLink(expression.value)) state.isExternal = true;
|
|
26433
|
-
return;
|
|
26434
|
-
}
|
|
26435
|
-
if (isNodeOfType(expression, "Identifier")) {
|
|
26436
|
-
state.isDynamic = true;
|
|
26437
|
-
return;
|
|
26438
|
-
}
|
|
26439
|
-
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
26440
|
-
matchHrefExpression(expression.consequent, state);
|
|
26441
|
-
matchHrefExpression(expression.alternate, state);
|
|
26442
|
-
}
|
|
26443
|
-
};
|
|
26444
|
-
const checkHref = (attributeValue, enforceDynamicLinks) => {
|
|
26445
|
-
const state = {
|
|
26446
|
-
isExternal: false,
|
|
26447
|
-
isDynamic: false
|
|
26448
|
-
};
|
|
26449
|
-
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") state.isExternal = isExternalLink(attributeValue.value);
|
|
26450
|
-
else if (isNodeOfType(attributeValue, "JSXExpressionContainer")) matchHrefExpression(attributeValue.expression, state);
|
|
26451
|
-
if (enforceDynamicLinks === "never") return !state.isExternal || state.isDynamic;
|
|
26452
|
-
return !(state.isExternal || state.isDynamic);
|
|
26453
|
-
};
|
|
26454
|
-
const checkRelValue = (text, allowReferrer) => {
|
|
26455
|
-
const tokens = text.split(/\s+/);
|
|
26456
|
-
if (allowReferrer) return tokens.includes("noopener") || tokens.includes("noreferrer");
|
|
26457
|
-
return tokens.some((token) => token.toLowerCase() === "noreferrer");
|
|
26458
|
-
};
|
|
26459
|
-
const matchRelExpression = (expression, allowReferrer) => {
|
|
26460
|
-
const empty = {
|
|
26461
|
-
combined: false,
|
|
26462
|
-
testName: "",
|
|
26463
|
-
consequent: false,
|
|
26464
|
-
alternate: false
|
|
26465
|
-
};
|
|
26466
|
-
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return {
|
|
26467
|
-
combined: checkRelValue(expression.value, allowReferrer),
|
|
26468
|
-
testName: "",
|
|
26469
|
-
consequent: false,
|
|
26470
|
-
alternate: false
|
|
26471
|
-
};
|
|
26472
|
-
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
26473
|
-
const consequent = matchRelExpression(expression.consequent, allowReferrer);
|
|
26474
|
-
const alternate = matchRelExpression(expression.alternate, allowReferrer);
|
|
26475
|
-
const test = expression.test;
|
|
26476
|
-
if (isNodeOfType(test, "Identifier")) return {
|
|
26477
|
-
combined: consequent.combined && alternate.combined,
|
|
26478
|
-
testName: test.name,
|
|
26479
|
-
consequent: consequent.combined,
|
|
26480
|
-
alternate: alternate.combined
|
|
26481
|
-
};
|
|
26482
|
-
return {
|
|
26483
|
-
combined: consequent.combined && alternate.combined,
|
|
26484
|
-
testName: "",
|
|
26485
|
-
consequent: consequent.combined,
|
|
26486
|
-
alternate: alternate.combined
|
|
26487
|
-
};
|
|
26488
|
-
}
|
|
26489
|
-
return empty;
|
|
26490
|
-
};
|
|
26491
|
-
const checkRel = (attributeValue, allowReferrer) => {
|
|
26492
|
-
const empty = {
|
|
26493
|
-
combined: false,
|
|
26494
|
-
testName: "",
|
|
26495
|
-
consequent: false,
|
|
26496
|
-
alternate: false
|
|
26497
|
-
};
|
|
26498
|
-
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return {
|
|
26499
|
-
combined: checkRelValue(attributeValue.value, allowReferrer),
|
|
26500
|
-
testName: "",
|
|
26501
|
-
consequent: false,
|
|
26502
|
-
alternate: false
|
|
26503
|
-
};
|
|
26504
|
-
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
26505
|
-
const expression = attributeValue.expression;
|
|
26506
|
-
if (expression.type === "JSXEmptyExpression") return empty;
|
|
26507
|
-
return matchRelExpression(expression, allowReferrer);
|
|
26508
|
-
}
|
|
26509
|
-
return empty;
|
|
26510
|
-
};
|
|
26511
|
-
const matchTargetExpression = (expression) => {
|
|
26512
|
-
const empty = {
|
|
26513
|
-
combined: false,
|
|
26514
|
-
testName: "",
|
|
26515
|
-
consequent: false,
|
|
26516
|
-
alternate: false
|
|
26517
|
-
};
|
|
26518
|
-
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return {
|
|
26519
|
-
combined: expression.value.toLowerCase() === "_blank",
|
|
26520
|
-
testName: "",
|
|
26521
|
-
consequent: false,
|
|
26522
|
-
alternate: false
|
|
26523
|
-
};
|
|
26524
|
-
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
26525
|
-
const consequent = matchTargetExpression(expression.consequent);
|
|
26526
|
-
const alternate = matchTargetExpression(expression.alternate);
|
|
26527
|
-
const test = expression.test;
|
|
26528
|
-
const combined = consequent.combined || alternate.combined;
|
|
26529
|
-
if (isNodeOfType(test, "Identifier")) return {
|
|
26530
|
-
combined,
|
|
26531
|
-
testName: test.name,
|
|
26532
|
-
consequent: consequent.combined,
|
|
26533
|
-
alternate: alternate.combined
|
|
26534
|
-
};
|
|
26535
|
-
return {
|
|
26536
|
-
combined,
|
|
26537
|
-
testName: "",
|
|
26538
|
-
consequent: consequent.combined,
|
|
26539
|
-
alternate: alternate.combined
|
|
26540
|
-
};
|
|
26541
|
-
}
|
|
26542
|
-
return empty;
|
|
26543
|
-
};
|
|
26544
|
-
const checkTarget = (attributeValue) => {
|
|
26545
|
-
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return {
|
|
26546
|
-
combined: attributeValue.value.toLowerCase() === "_blank",
|
|
26547
|
-
testName: "",
|
|
26548
|
-
consequent: false,
|
|
26549
|
-
alternate: false
|
|
26550
|
-
};
|
|
26551
|
-
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
26552
|
-
const expression = attributeValue.expression;
|
|
26553
|
-
if (expression.type === "JSXEmptyExpression") return {
|
|
26554
|
-
combined: false,
|
|
26555
|
-
testName: "",
|
|
26556
|
-
consequent: false,
|
|
26557
|
-
alternate: false
|
|
26558
|
-
};
|
|
26559
|
-
return matchTargetExpression(expression);
|
|
26560
|
-
}
|
|
26561
|
-
return {
|
|
26562
|
-
combined: false,
|
|
26563
|
-
testName: "",
|
|
26564
|
-
consequent: false,
|
|
26565
|
-
alternate: false
|
|
26566
|
-
};
|
|
26567
|
-
};
|
|
26568
|
-
const jsxNoTargetBlank = defineRule({
|
|
26569
|
-
id: "jsx-no-target-blank",
|
|
26570
|
-
title: "Unsafe target=_blank link",
|
|
26571
|
-
severity: "warn",
|
|
26572
|
-
recommendation: "Add `rel=\"noreferrer\"` (or `\"noopener\"`) when using `target=\"_blank\"`.",
|
|
26573
|
-
category: "Security",
|
|
26574
|
-
create: (context) => {
|
|
26575
|
-
const settings = resolveSettings$28(context.settings);
|
|
26576
|
-
const isLink = (tagName) => {
|
|
26577
|
-
if (!settings.links) return false;
|
|
26578
|
-
if (tagName === "a") return true;
|
|
26579
|
-
return settings.linkComponents.has(tagName);
|
|
26580
|
-
};
|
|
26581
|
-
const isForm = (tagName) => {
|
|
26582
|
-
if (!settings.forms) return false;
|
|
26583
|
-
if (tagName === "form") return true;
|
|
26584
|
-
return settings.formComponents.has(tagName);
|
|
26585
|
-
};
|
|
26586
|
-
return { JSXOpeningElement(node) {
|
|
26587
|
-
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
26588
|
-
const tagName = resolveJsxElementType(node);
|
|
26589
|
-
if (!tagName) return;
|
|
26590
|
-
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
26591
|
-
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
26592
|
-
const formAttributeNames = settings.formComponents.get(tagName) ?? ["action"];
|
|
26593
|
-
let targetTuple = {
|
|
26594
|
-
combined: false,
|
|
26595
|
-
testName: "",
|
|
26596
|
-
consequent: false,
|
|
26597
|
-
alternate: false
|
|
26598
|
-
};
|
|
26599
|
-
let relTuple = {
|
|
26600
|
-
combined: false,
|
|
26601
|
-
testName: "",
|
|
26602
|
-
consequent: false,
|
|
26603
|
-
alternate: false
|
|
26604
|
-
};
|
|
26605
|
-
let isHrefValid = true;
|
|
26606
|
-
let hasHrefValue = false;
|
|
26607
|
-
let warnSpread = false;
|
|
26608
|
-
let targetReportNode = node.name;
|
|
26609
|
-
let spreadReportNode = null;
|
|
26610
|
-
for (const attribute of node.attributes) {
|
|
26611
|
-
if (isNodeOfType(attribute, "JSXSpreadAttribute")) {
|
|
26612
|
-
if (settings.warnOnSpreadAttributes) {
|
|
26613
|
-
warnSpread = true;
|
|
26614
|
-
spreadReportNode = attribute;
|
|
26615
|
-
targetTuple = {
|
|
26616
|
-
combined: false,
|
|
26617
|
-
testName: "",
|
|
26618
|
-
consequent: false,
|
|
26619
|
-
alternate: false
|
|
26620
|
-
};
|
|
26621
|
-
relTuple = {
|
|
26622
|
-
combined: false,
|
|
26623
|
-
testName: "",
|
|
26624
|
-
consequent: false,
|
|
26625
|
-
alternate: false
|
|
26626
|
-
};
|
|
26627
|
-
isHrefValid = false;
|
|
26628
|
-
hasHrefValue = true;
|
|
26629
|
-
}
|
|
26630
|
-
continue;
|
|
26631
|
-
}
|
|
26632
|
-
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
26633
|
-
const attributeName = attribute.name;
|
|
26634
|
-
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
26635
|
-
const propName = attributeName.name;
|
|
26636
|
-
const value = attribute.value;
|
|
26637
|
-
if (propName === "target") {
|
|
26638
|
-
if (value) {
|
|
26639
|
-
targetTuple = checkTarget(value);
|
|
26640
|
-
targetReportNode = value;
|
|
26641
|
-
}
|
|
26642
|
-
} else if (propName === "href" || propName === "action" || linkAttributeNames.includes(propName) || formAttributeNames.includes(propName)) {
|
|
26643
|
-
if (value) {
|
|
26644
|
-
hasHrefValue = true;
|
|
26645
|
-
isHrefValid = checkHref(value, settings.enforceDynamicLinks);
|
|
26646
|
-
}
|
|
26647
|
-
} else if (propName === "rel" && value) relTuple = checkRel(value, settings.allowReferrer);
|
|
26648
|
-
}
|
|
26649
|
-
if (warnSpread) {
|
|
26650
|
-
if (hasHrefValue && isHrefValid || relTuple.combined) return;
|
|
26651
|
-
context.report({
|
|
26652
|
-
node: spreadReportNode ?? node,
|
|
26653
|
-
message: SPREAD_MESSAGE
|
|
26654
|
-
});
|
|
26655
|
-
return;
|
|
26656
|
-
}
|
|
26657
|
-
if (!isHrefValid) {
|
|
26658
|
-
if (targetTuple.testName !== "" && targetTuple.testName === relTuple.testName) {
|
|
26659
|
-
const consequentBad = targetTuple.consequent && !relTuple.consequent;
|
|
26660
|
-
const alternateBad = targetTuple.alternate && !relTuple.alternate;
|
|
26661
|
-
if (consequentBad || alternateBad) context.report({
|
|
26662
|
-
node: targetReportNode,
|
|
26663
|
-
message: settings.allowReferrer ? NOOPENER_MESSAGE : NOREFERRER_MESSAGE
|
|
26664
|
-
});
|
|
26665
|
-
return;
|
|
26666
|
-
}
|
|
26667
|
-
if (targetTuple.combined && !relTuple.combined) context.report({
|
|
26668
|
-
node: targetReportNode,
|
|
26669
|
-
message: settings.allowReferrer ? NOOPENER_MESSAGE : NOREFERRER_MESSAGE
|
|
26670
|
-
});
|
|
26671
|
-
}
|
|
26672
|
-
} };
|
|
26673
|
-
}
|
|
26674
|
-
});
|
|
26675
|
-
//#endregion
|
|
26676
26877
|
//#region src/plugin/rules/react-builtins/jsx-no-undef.ts
|
|
26677
26878
|
const buildMessage$18 = (name) => `\`${name}\` crashes at runtime because it isn't defined here.`;
|
|
26678
26879
|
const KNOWN_GLOBALS = new Set([
|
|
@@ -28330,6 +28531,30 @@ const hasAncestorLayoutMatching = (pageFilename, matchesLayout) => {
|
|
|
28330
28531
|
const hasAncestorMetadataLayout = (pageFilename) => hasAncestorLayoutMatching(pageFilename, (layoutPath) => METADATA_EXPORT_NAMES.some((exportName) => doesModuleExportName(layoutPath, exportName)));
|
|
28331
28532
|
//#endregion
|
|
28332
28533
|
//#region src/plugin/rules/nextjs/nextjs-missing-metadata.ts
|
|
28534
|
+
const unwrapAwaitedExpression = (node) => {
|
|
28535
|
+
let expression = stripParenExpression(node);
|
|
28536
|
+
while (isNodeOfType(expression, "AwaitExpression")) expression = stripParenExpression(expression.argument);
|
|
28537
|
+
return expression;
|
|
28538
|
+
};
|
|
28539
|
+
const isNextNavigationRedirectCall = (node, context) => {
|
|
28540
|
+
const expression = unwrapAwaitedExpression(node);
|
|
28541
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
28542
|
+
const callee = stripParenExpression(expression.callee);
|
|
28543
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
28544
|
+
if (context.scopes.symbolFor(callee)?.kind !== "import") return false;
|
|
28545
|
+
const importedName = getImportedNameFromModule(expression, callee.name, "next/navigation");
|
|
28546
|
+
return importedName === "redirect" || importedName === "permanentRedirect";
|
|
28547
|
+
};
|
|
28548
|
+
const isRedirectOnlyDefaultExport = (programNode, context) => {
|
|
28549
|
+
const defaultExport = findExportedValue(programNode, "default");
|
|
28550
|
+
if (!defaultExport || !isFunctionLike$1(defaultExport)) return false;
|
|
28551
|
+
if (!isNodeOfType(defaultExport.body, "BlockStatement")) return isNextNavigationRedirectCall(defaultExport.body, context);
|
|
28552
|
+
if (defaultExport.body.body.length !== 1) return false;
|
|
28553
|
+
const onlyStatement = defaultExport.body.body[0];
|
|
28554
|
+
if (isNodeOfType(onlyStatement, "ExpressionStatement")) return isNextNavigationRedirectCall(onlyStatement.expression, context);
|
|
28555
|
+
if (isNodeOfType(onlyStatement, "ReturnStatement") && onlyStatement.argument) return isNextNavigationRedirectCall(onlyStatement.argument, context);
|
|
28556
|
+
return false;
|
|
28557
|
+
};
|
|
28333
28558
|
const nextjsMissingMetadata = defineRule({
|
|
28334
28559
|
id: "nextjs-missing-metadata",
|
|
28335
28560
|
title: "Page missing metadata for search previews",
|
|
@@ -28349,6 +28574,7 @@ const nextjsMissingMetadata = defineRule({
|
|
|
28349
28574
|
if (isNodeOfType(declaration, "FunctionDeclaration")) return declaration.id?.name === "generateMetadata";
|
|
28350
28575
|
return (statement.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ExportSpecifier") && isNodeOfType(specifier.exported, "Identifier") && METADATA_EXPORT_NAMES.includes(specifier.exported.name));
|
|
28351
28576
|
})) return;
|
|
28577
|
+
if (isRedirectOnlyDefaultExport(programNode, context)) return;
|
|
28352
28578
|
if (hasAncestorMetadataLayout(context.filename ?? "")) return;
|
|
28353
28579
|
context.report({
|
|
28354
28580
|
node: programNode,
|
|
@@ -29362,6 +29588,14 @@ const nextjsNoImgElement = defineRule({
|
|
|
29362
29588
|
});
|
|
29363
29589
|
//#endregion
|
|
29364
29590
|
//#region src/plugin/rules/nextjs/nextjs-no-native-script.ts
|
|
29591
|
+
const hasEnabledBooleanAttribute = (attributes, attributeName) => {
|
|
29592
|
+
const attribute = findJsxAttribute(attributes, attributeName);
|
|
29593
|
+
if (!attribute) return false;
|
|
29594
|
+
if (attribute.value === null) return true;
|
|
29595
|
+
if (isNodeOfType(attribute.value, "Literal")) return true;
|
|
29596
|
+
if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return false;
|
|
29597
|
+
return isNodeOfType(attribute.value.expression, "Literal") && Boolean(attribute.value.expression.value);
|
|
29598
|
+
};
|
|
29365
29599
|
const nextjsNoNativeScript = defineRule({
|
|
29366
29600
|
id: "nextjs-no-native-script",
|
|
29367
29601
|
title: "Plain script can block Next.js rendering",
|
|
@@ -29374,6 +29608,9 @@ const nextjsNoNativeScript = defineRule({
|
|
|
29374
29608
|
const typeAttribute = findJsxAttribute(node.attributes ?? [], "type");
|
|
29375
29609
|
const typeValue = isNodeOfType(typeAttribute?.value, "Literal") ? typeAttribute.value.value : null;
|
|
29376
29610
|
if (typeof typeValue === "string" && !EXECUTABLE_SCRIPT_TYPES.has(typeValue)) return;
|
|
29611
|
+
if (hasEnabledBooleanAttribute(node.attributes ?? [], "async")) return;
|
|
29612
|
+
if (hasEnabledBooleanAttribute(node.attributes ?? [], "defer")) return;
|
|
29613
|
+
if (typeValue === "module") return;
|
|
29377
29614
|
const hasSrcAttribute = Boolean(findJsxAttribute(node.attributes ?? [], "src"));
|
|
29378
29615
|
if (Boolean(findJsxAttribute(node.attributes ?? [], "dangerouslySetInnerHTML")) && !hasSrcAttribute) return;
|
|
29379
29616
|
context.report({
|
|
@@ -34466,6 +34703,19 @@ const noCascadingSetState = defineRetiredRule({
|
|
|
34466
34703
|
recommendation: "Retired: React batches synchronous state updates from one effect into the same follow-up commit, so setter count does not prove repeated redraws."
|
|
34467
34704
|
});
|
|
34468
34705
|
//#endregion
|
|
34706
|
+
//#region src/plugin/utils/get-react-use-callback-call.ts
|
|
34707
|
+
const getReactUseCallbackCall = (expression, scopes) => {
|
|
34708
|
+
const identifier = stripParenExpression(expression);
|
|
34709
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
34710
|
+
const symbol = resolveConstIdentifierAlias(identifier, scopes);
|
|
34711
|
+
if (symbol?.kind !== "const" || !symbol.initializer) return null;
|
|
34712
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
34713
|
+
return isNodeOfType(initializer, "CallExpression") && isReactApiCall(initializer, "useCallback", scopes, {
|
|
34714
|
+
allowGlobalReactNamespace: true,
|
|
34715
|
+
resolveNamedAliases: true
|
|
34716
|
+
}) ? initializer : null;
|
|
34717
|
+
};
|
|
34718
|
+
//#endregion
|
|
34469
34719
|
//#region src/plugin/rules/state-and-effects/utils/create-state-trigger-reachability.ts
|
|
34470
34720
|
const getRefCurrentSymbol = (node, context) => {
|
|
34471
34721
|
const expression = stripParenExpression(node);
|
|
@@ -34994,19 +35244,8 @@ const isBuiltinNamespaceCallee = (callee) => {
|
|
|
34994
35244
|
}
|
|
34995
35245
|
return false;
|
|
34996
35246
|
};
|
|
34997
|
-
const getReactUseCallbackSource = (reference, context) => {
|
|
34998
|
-
const identifier = reference.identifier;
|
|
34999
|
-
const symbol = resolveConstIdentifierAlias(identifier, context.scopes);
|
|
35000
|
-
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
|
|
35001
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
35002
|
-
if (isNodeOfType(initializer, "CallExpression") && isReactApiCall(initializer, "useCallback", context.scopes, {
|
|
35003
|
-
allowGlobalReactNamespace: true,
|
|
35004
|
-
resolveNamedAliases: true
|
|
35005
|
-
})) return initializer;
|
|
35006
|
-
return null;
|
|
35007
|
-
};
|
|
35008
35247
|
const getDependencyStateRefs = (analysis, context, dependencyReference) => {
|
|
35009
|
-
const useCallbackCall =
|
|
35248
|
+
const useCallbackCall = getReactUseCallbackCall(dependencyReference.identifier, context.scopes);
|
|
35010
35249
|
if (!useCallbackCall) return getUpstreamRefs(analysis, dependencyReference).filter((reference) => isState(analysis, reference));
|
|
35011
35250
|
const dependencyList = useCallbackCall.arguments?.[1];
|
|
35012
35251
|
if (!dependencyList || !isNodeOfType(dependencyList, "ArrayExpression")) return getUpstreamRefs(analysis, dependencyReference).filter((reference) => isState(analysis, reference));
|
|
@@ -36594,19 +36833,6 @@ const noDefaultProps = defineRule({
|
|
|
36594
36833
|
} })
|
|
36595
36834
|
});
|
|
36596
36835
|
//#endregion
|
|
36597
|
-
//#region src/plugin/utils/is-component-function.ts
|
|
36598
|
-
const isFunctionAssignedToComponent = (functionNode) => {
|
|
36599
|
-
let cursor = functionNode.parent ?? null;
|
|
36600
|
-
while (isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
|
|
36601
|
-
if (isNodeOfType(cursor, "VariableDeclarator") && isNodeOfType(cursor.id, "Identifier") && isUppercaseName(cursor.id.name)) return true;
|
|
36602
|
-
return isNodeOfType(cursor, "ExportDefaultDeclaration");
|
|
36603
|
-
};
|
|
36604
|
-
const isComponentFunction$1 = (node) => {
|
|
36605
|
-
if (!isFunctionLike$1(node)) return false;
|
|
36606
|
-
if (isNodeOfType(node, "FunctionDeclaration")) return !node.id || node.id.name === "default" || isUppercaseName(node.id.name) || isNodeOfType(node.parent, "ExportDefaultDeclaration");
|
|
36607
|
-
return isFunctionAssignedToComponent(node);
|
|
36608
|
-
};
|
|
36609
|
-
//#endregion
|
|
36610
36836
|
//#region src/plugin/utils/create-component-prop-stack-tracker.ts
|
|
36611
36837
|
const extractDestructuredPropNames = (params) => {
|
|
36612
36838
|
const propNames = /* @__PURE__ */ new Set();
|
|
@@ -38520,6 +38746,17 @@ const noDynamicImportPath = defineRule({
|
|
|
38520
38746
|
})
|
|
38521
38747
|
});
|
|
38522
38748
|
//#endregion
|
|
38749
|
+
//#region src/plugin/utils/get-require-call-source.ts
|
|
38750
|
+
const getRequireCallSource = (expression) => {
|
|
38751
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
38752
|
+
if (isNodeOfType(unwrappedExpression, "MemberExpression")) return getRequireCallSource(unwrappedExpression.object);
|
|
38753
|
+
if (!isNodeOfType(unwrappedExpression, "CallExpression")) return null;
|
|
38754
|
+
if (!isNodeOfType(unwrappedExpression.callee, "Identifier") || unwrappedExpression.callee.name !== "require") return null;
|
|
38755
|
+
const [firstArgument] = unwrappedExpression.arguments ?? [];
|
|
38756
|
+
if (!firstArgument || !isNodeOfType(firstArgument, "Literal")) return null;
|
|
38757
|
+
return typeof firstArgument.value === "string" ? firstArgument.value : null;
|
|
38758
|
+
};
|
|
38759
|
+
//#endregion
|
|
38523
38760
|
//#region src/plugin/rules/state-and-effects/utils/is-cleanup-return.ts
|
|
38524
38761
|
const ITERATOR_CALLBACK_METHOD_NAMES = new Set([
|
|
38525
38762
|
"each",
|
|
@@ -38664,7 +38901,16 @@ const collectDependencyStateSymbolIds = (effectNode, stateSymbolIds, scopes) =>
|
|
|
38664
38901
|
}
|
|
38665
38902
|
return dependencyStateSymbolIds;
|
|
38666
38903
|
};
|
|
38667
|
-
const
|
|
38904
|
+
const resolveSynchronouslyInvokedFunction = (expression, scopes) => {
|
|
38905
|
+
const localFunction = resolveExactLocalFunction(expression, scopes);
|
|
38906
|
+
if (localFunction) return localFunction;
|
|
38907
|
+
const useCallbackCall = getReactUseCallbackCall(expression, scopes);
|
|
38908
|
+
if (!useCallbackCall) return null;
|
|
38909
|
+
const callback = useCallbackCall.arguments[0];
|
|
38910
|
+
if (!callback || isNodeOfType(callback, "SpreadElement")) return null;
|
|
38911
|
+
return resolveExactLocalFunction(callback, scopes);
|
|
38912
|
+
};
|
|
38913
|
+
const collectSynchronouslyInvokedFunctions = (effectCallback, scopes, includeStableCallbacks = true) => {
|
|
38668
38914
|
const analysisFunctions = new Set([effectCallback]);
|
|
38669
38915
|
const pendingFunctions = [effectCallback];
|
|
38670
38916
|
while (pendingFunctions.length > 0) {
|
|
@@ -38672,9 +38918,9 @@ const collectSynchronouslyInvokedFunctions = (effectCallback, scopes) => {
|
|
|
38672
38918
|
if (!currentFunction || !isFunctionLike$1(currentFunction)) continue;
|
|
38673
38919
|
walkInsideStatementBlocks(currentFunction.body, (child) => {
|
|
38674
38920
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
38675
|
-
const invokedFunction = resolveExactLocalFunction(child.callee, scopes);
|
|
38921
|
+
const invokedFunction = includeStableCallbacks ? resolveSynchronouslyInvokedFunction(child.callee, scopes) : resolveExactLocalFunction(child.callee, scopes);
|
|
38676
38922
|
if (!invokedFunction || analysisFunctions.has(invokedFunction)) return;
|
|
38677
|
-
if (isFunctionLike$1(invokedFunction) && invokedFunction.async) return;
|
|
38923
|
+
if (isFunctionLike$1(invokedFunction) && (invokedFunction.async || invokedFunction.generator)) return;
|
|
38678
38924
|
analysisFunctions.add(invokedFunction);
|
|
38679
38925
|
pendingFunctions.push(invokedFunction);
|
|
38680
38926
|
});
|
|
@@ -38687,7 +38933,51 @@ const visitSynchronousFunctionBodies = (analysisFunctions, visitor) => {
|
|
|
38687
38933
|
walkInsideStatementBlocks(analysisFunction.body, visitor);
|
|
38688
38934
|
}
|
|
38689
38935
|
};
|
|
38690
|
-
const
|
|
38936
|
+
const getStateNameForSetterCall = (node, setterSymbolIdToStateName, scopes) => {
|
|
38937
|
+
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "Identifier")) return null;
|
|
38938
|
+
const setterSymbol = resolveConstIdentifierAlias(node.callee, scopes, true);
|
|
38939
|
+
return setterSymbol ? setterSymbolIdToStateName.get(setterSymbol.id) ?? null : null;
|
|
38940
|
+
};
|
|
38941
|
+
const collectStateWriteAnalysisFunctions = (effectCallback, scopes, setterSymbolIdToStateName) => {
|
|
38942
|
+
const ordinaryAnalysisFunctions = collectSynchronouslyInvokedFunctions(effectCallback, scopes, false);
|
|
38943
|
+
const fullAnalysisFunctions = collectSynchronouslyInvokedFunctions(effectCallback, scopes);
|
|
38944
|
+
const stableAnalysisFunctions = [...fullAnalysisFunctions].filter((analysisFunction) => !ordinaryAnalysisFunctions.has(analysisFunction));
|
|
38945
|
+
if (stableAnalysisFunctions.length === 0) return ordinaryAnalysisFunctions;
|
|
38946
|
+
let hasUnprovenObservableWork = stableAnalysisFunctions.some((analysisFunction) => {
|
|
38947
|
+
if (!isFunctionLike$1(analysisFunction)) return false;
|
|
38948
|
+
let hasExecutableDefault = false;
|
|
38949
|
+
for (const parameter of analysisFunction.params) {
|
|
38950
|
+
walkInsideStatementBlocks(parameter, (child) => {
|
|
38951
|
+
if (isNodeOfType(child, "CallExpression") || isNodeOfType(child, "AssignmentExpression") || isNodeOfType(child, "UpdateExpression") || isNodeOfType(child, "ImportExpression") || isNodeOfType(child, "NewExpression") || isNodeOfType(child, "TaggedTemplateExpression") || isNodeOfType(child, "ThrowStatement") || isNodeOfType(child, "UnaryExpression") && child.operator === "delete") hasExecutableDefault = true;
|
|
38952
|
+
});
|
|
38953
|
+
if (hasExecutableDefault) return true;
|
|
38954
|
+
}
|
|
38955
|
+
return false;
|
|
38956
|
+
});
|
|
38957
|
+
const stableWrittenStateNames = /* @__PURE__ */ new Set();
|
|
38958
|
+
const allWrittenStateNames = /* @__PURE__ */ new Set();
|
|
38959
|
+
visitSynchronousFunctionBodies(fullAnalysisFunctions, (child) => {
|
|
38960
|
+
const writtenStateName = getStateNameForSetterCall(child, setterSymbolIdToStateName, scopes);
|
|
38961
|
+
if (writtenStateName) allWrittenStateNames.add(writtenStateName);
|
|
38962
|
+
});
|
|
38963
|
+
visitSynchronousFunctionBodies(new Set(stableAnalysisFunctions), (child) => {
|
|
38964
|
+
if (hasUnprovenObservableWork) return;
|
|
38965
|
+
if (isNodeOfType(child, "CallExpression")) {
|
|
38966
|
+
const writtenStateName = getStateNameForSetterCall(child, setterSymbolIdToStateName, scopes);
|
|
38967
|
+
if (writtenStateName) {
|
|
38968
|
+
stableWrittenStateNames.add(writtenStateName);
|
|
38969
|
+
return;
|
|
38970
|
+
}
|
|
38971
|
+
const invokedFunction = resolveSynchronouslyInvokedFunction(child.callee, scopes);
|
|
38972
|
+
if (invokedFunction && fullAnalysisFunctions.has(invokedFunction)) return;
|
|
38973
|
+
hasUnprovenObservableWork = true;
|
|
38974
|
+
return;
|
|
38975
|
+
}
|
|
38976
|
+
if (isNodeOfType(child, "AssignmentExpression") || isNodeOfType(child, "UpdateExpression") || isNodeOfType(child, "ImportExpression") || isNodeOfType(child, "NewExpression") || isNodeOfType(child, "TaggedTemplateExpression") || isNodeOfType(child, "ThrowStatement") || isNodeOfType(child, "UnaryExpression") && child.operator === "delete") hasUnprovenObservableWork = true;
|
|
38977
|
+
});
|
|
38978
|
+
return !hasUnprovenObservableWork && stableWrittenStateNames.size === 1 && allWrittenStateNames.size === 1 ? fullAnalysisFunctions : ordinaryAnalysisFunctions;
|
|
38979
|
+
};
|
|
38980
|
+
const readStaticEffectValue = (expression, scopes, stateSymbolId, stateValue, visitedSymbolIds = /* @__PURE__ */ new Set(), additionalStateValues = /* @__PURE__ */ new Map()) => {
|
|
38691
38981
|
const unwrappedExpression = stripParenExpression(expression);
|
|
38692
38982
|
if (isNodeOfType(unwrappedExpression, "Literal")) {
|
|
38693
38983
|
const literalValue = unwrappedExpression.value;
|
|
@@ -38695,46 +38985,49 @@ const readStaticEffectValue = (expression, scopes, stateSymbolId, stateValue, vi
|
|
|
38695
38985
|
return null;
|
|
38696
38986
|
}
|
|
38697
38987
|
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
38698
|
-
|
|
38988
|
+
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
38989
|
+
if (symbol?.id === stateSymbolId) return stateValue;
|
|
38990
|
+
if (symbol && additionalStateValues.has(symbol.id)) return additionalStateValues.get(symbol.id) ?? null;
|
|
38699
38991
|
if (unwrappedExpression.name === "undefined" && scopes.isGlobalReference(unwrappedExpression)) return { value: void 0 };
|
|
38992
|
+
if (unwrappedExpression.name === "NaN" && scopes.isGlobalReference(unwrappedExpression)) return { value: NaN };
|
|
38700
38993
|
const immutableSymbol = scopes.symbolFor(unwrappedExpression);
|
|
38701
38994
|
if (immutableSymbol?.kind !== "const" || !immutableSymbol.initializer || !isNodeOfType(immutableSymbol.declarationNode, "VariableDeclarator") || immutableSymbol.declarationNode.id !== immutableSymbol.bindingIdentifier || immutableSymbol.declarationNode.init !== immutableSymbol.initializer || immutableSymbol.references.some((reference) => reference.flag !== "read") || visitedSymbolIds.has(immutableSymbol.id)) return null;
|
|
38702
|
-
return readStaticEffectValue(immutableSymbol.initializer, scopes, stateSymbolId, stateValue, new Set(visitedSymbolIds).add(immutableSymbol.id));
|
|
38995
|
+
return readStaticEffectValue(immutableSymbol.initializer, scopes, stateSymbolId, stateValue, new Set(visitedSymbolIds).add(immutableSymbol.id), additionalStateValues);
|
|
38703
38996
|
}
|
|
38704
38997
|
if (isNodeOfType(unwrappedExpression, "UnaryExpression")) {
|
|
38705
38998
|
if (unwrappedExpression.operator === "void") return { value: void 0 };
|
|
38706
38999
|
if (unwrappedExpression.operator !== "!") return null;
|
|
38707
|
-
const argumentValue = readStaticEffectValue(unwrappedExpression.argument, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
39000
|
+
const argumentValue = readStaticEffectValue(unwrappedExpression.argument, scopes, stateSymbolId, stateValue, visitedSymbolIds, additionalStateValues);
|
|
38708
39001
|
return argumentValue ? { value: !argumentValue.value } : null;
|
|
38709
39002
|
}
|
|
38710
39003
|
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
38711
39004
|
if (isNodeOfType(unwrappedExpression.callee, "Identifier") && unwrappedExpression.callee.name === "Boolean" && scopes.isGlobalReference(unwrappedExpression.callee) && unwrappedExpression.arguments.length === 1 && unwrappedExpression.arguments[0] && !isNodeOfType(unwrappedExpression.arguments[0], "SpreadElement")) {
|
|
38712
|
-
const argumentValue = readStaticEffectValue(unwrappedExpression.arguments[0], scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
39005
|
+
const argumentValue = readStaticEffectValue(unwrappedExpression.arguments[0], scopes, stateSymbolId, stateValue, visitedSymbolIds, additionalStateValues);
|
|
38713
39006
|
return argumentValue ? { value: Boolean(argumentValue.value) } : null;
|
|
38714
39007
|
}
|
|
38715
39008
|
return null;
|
|
38716
39009
|
}
|
|
38717
39010
|
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
38718
|
-
const leftValue = readStaticEffectValue(unwrappedExpression.left, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
39011
|
+
const leftValue = readStaticEffectValue(unwrappedExpression.left, scopes, stateSymbolId, stateValue, visitedSymbolIds, additionalStateValues);
|
|
38719
39012
|
if (!leftValue) return null;
|
|
38720
39013
|
if (unwrappedExpression.operator === "&&" && !leftValue.value) return leftValue;
|
|
38721
39014
|
if (unwrappedExpression.operator === "||" && leftValue.value) return leftValue;
|
|
38722
39015
|
if (unwrappedExpression.operator === "??" && leftValue.value !== null && leftValue.value !== void 0) return leftValue;
|
|
38723
|
-
return readStaticEffectValue(unwrappedExpression.right, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
39016
|
+
return readStaticEffectValue(unwrappedExpression.right, scopes, stateSymbolId, stateValue, visitedSymbolIds, additionalStateValues);
|
|
38724
39017
|
}
|
|
38725
39018
|
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) {
|
|
38726
|
-
const testValue = readStaticEffectValue(unwrappedExpression.test, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
39019
|
+
const testValue = readStaticEffectValue(unwrappedExpression.test, scopes, stateSymbolId, stateValue, visitedSymbolIds, additionalStateValues);
|
|
38727
39020
|
if (!testValue) return null;
|
|
38728
|
-
return readStaticEffectValue(testValue.value ? unwrappedExpression.consequent : unwrappedExpression.alternate, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
39021
|
+
return readStaticEffectValue(testValue.value ? unwrappedExpression.consequent : unwrappedExpression.alternate, scopes, stateSymbolId, stateValue, visitedSymbolIds, additionalStateValues);
|
|
38729
39022
|
}
|
|
38730
39023
|
if (isNodeOfType(unwrappedExpression, "MemberExpression") && unwrappedExpression.optional) {
|
|
38731
|
-
const objectValue = readStaticEffectValue(unwrappedExpression.object, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
39024
|
+
const objectValue = readStaticEffectValue(unwrappedExpression.object, scopes, stateSymbolId, stateValue, visitedSymbolIds, additionalStateValues);
|
|
38732
39025
|
if (objectValue?.value === null || objectValue?.value === void 0) return { value: void 0 };
|
|
38733
39026
|
return null;
|
|
38734
39027
|
}
|
|
38735
39028
|
if (isNodeOfType(unwrappedExpression, "BinaryExpression")) {
|
|
38736
|
-
const leftValue = readStaticEffectValue(unwrappedExpression.left, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
38737
|
-
const rightValue = readStaticEffectValue(unwrappedExpression.right, scopes, stateSymbolId, stateValue, visitedSymbolIds);
|
|
39029
|
+
const leftValue = readStaticEffectValue(unwrappedExpression.left, scopes, stateSymbolId, stateValue, visitedSymbolIds, additionalStateValues);
|
|
39030
|
+
const rightValue = readStaticEffectValue(unwrappedExpression.right, scopes, stateSymbolId, stateValue, visitedSymbolIds, additionalStateValues);
|
|
38738
39031
|
if (!leftValue || !rightValue) return null;
|
|
38739
39032
|
if (unwrappedExpression.operator === "===" || unwrappedExpression.operator === "!==") {
|
|
38740
39033
|
const areEqual = leftValue.value === rightValue.value;
|
|
@@ -38773,8 +39066,7 @@ const collectStateWritesInEffect = (analysisFunctions, setterSymbolIdToStateName
|
|
|
38773
39066
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
38774
39067
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
38775
39068
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
38776
|
-
const
|
|
38777
|
-
const stateName = setterSymbol ? setterSymbolIdToStateName.get(setterSymbol.id) : void 0;
|
|
39069
|
+
const stateName = getStateNameForSetterCall(child, setterSymbolIdToStateName, scopes);
|
|
38778
39070
|
if (!stateName) return;
|
|
38779
39071
|
const writeInfo = stateWrites.get(stateName) ?? {
|
|
38780
39072
|
values: /* @__PURE__ */ new Set(),
|
|
@@ -38790,27 +39082,27 @@ const collectStateWritesInEffect = (analysisFunctions, setterSymbolIdToStateName
|
|
|
38790
39082
|
const isGlobalBooleanCall = (node, scopes) => {
|
|
38791
39083
|
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Boolean" && scopes.isGlobalReference(node.callee);
|
|
38792
39084
|
};
|
|
38793
|
-
const isWorkNodeReachableForStateValue = (workNode, stateSymbolId, stateValue, scopes) => {
|
|
39085
|
+
const isWorkNodeReachableForStateValue = (workNode, stateSymbolId, stateValue, scopes, additionalStateValues = /* @__PURE__ */ new Map()) => {
|
|
38794
39086
|
let currentNode = workNode;
|
|
38795
39087
|
while (currentNode.parent) {
|
|
38796
39088
|
const parentNode = currentNode.parent;
|
|
38797
39089
|
if (isFunctionLike$1(parentNode)) break;
|
|
38798
39090
|
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
38799
|
-
const testValue = readStaticEffectValue(parentNode.test, scopes, stateSymbolId, stateValue);
|
|
39091
|
+
const testValue = readStaticEffectValue(parentNode.test, scopes, stateSymbolId, stateValue, /* @__PURE__ */ new Set(), additionalStateValues);
|
|
38800
39092
|
if (testValue) {
|
|
38801
39093
|
if (currentNode === parentNode.consequent && !testValue.value) return false;
|
|
38802
39094
|
if (currentNode === parentNode.alternate && testValue.value) return false;
|
|
38803
39095
|
}
|
|
38804
39096
|
}
|
|
38805
39097
|
if (isNodeOfType(parentNode, "ConditionalExpression")) {
|
|
38806
|
-
const testValue = readStaticEffectValue(parentNode.test, scopes, stateSymbolId, stateValue);
|
|
39098
|
+
const testValue = readStaticEffectValue(parentNode.test, scopes, stateSymbolId, stateValue, /* @__PURE__ */ new Set(), additionalStateValues);
|
|
38807
39099
|
if (testValue) {
|
|
38808
39100
|
if (currentNode === parentNode.consequent && !testValue.value) return false;
|
|
38809
39101
|
if (currentNode === parentNode.alternate && testValue.value) return false;
|
|
38810
39102
|
}
|
|
38811
39103
|
}
|
|
38812
39104
|
if (isNodeOfType(parentNode, "LogicalExpression") && currentNode === parentNode.right) {
|
|
38813
|
-
const leftValue = readStaticEffectValue(parentNode.left, scopes, stateSymbolId, stateValue);
|
|
39105
|
+
const leftValue = readStaticEffectValue(parentNode.left, scopes, stateSymbolId, stateValue, /* @__PURE__ */ new Set(), additionalStateValues);
|
|
38814
39106
|
if (leftValue) {
|
|
38815
39107
|
if (parentNode.operator === "&&" && !leftValue.value) return false;
|
|
38816
39108
|
if (parentNode.operator === "||" && leftValue.value) return false;
|
|
@@ -38822,7 +39114,7 @@ const isWorkNodeReachableForStateValue = (workNode, stateSymbolId, stateValue, s
|
|
|
38822
39114
|
if (statementIndex >= 0) for (let index = 0; index < statementIndex; index += 1) {
|
|
38823
39115
|
const earlierStatement = parentNode.body[index];
|
|
38824
39116
|
if (!isNodeOfType(earlierStatement, "IfStatement") || earlierStatement.alternate || !statementAlwaysExits(earlierStatement.consequent)) continue;
|
|
38825
|
-
if (readStaticEffectValue(earlierStatement.test, scopes, stateSymbolId, stateValue)?.value) return false;
|
|
39117
|
+
if (readStaticEffectValue(earlierStatement.test, scopes, stateSymbolId, stateValue, /* @__PURE__ */ new Set(), additionalStateValues)?.value) return false;
|
|
38826
39118
|
}
|
|
38827
39119
|
}
|
|
38828
39120
|
currentNode = parentNode;
|
|
@@ -38832,24 +39124,93 @@ const isWorkNodeReachableForStateValue = (workNode, stateSymbolId, stateValue, s
|
|
|
38832
39124
|
const isReaderWorkNode = (node, analysisFunctions, scopes) => {
|
|
38833
39125
|
if (isNodeOfType(node, "CallExpression")) {
|
|
38834
39126
|
if (isGlobalBooleanCall(node, scopes)) return false;
|
|
38835
|
-
const invokedFunction =
|
|
39127
|
+
const invokedFunction = resolveSynchronouslyInvokedFunction(node.callee, scopes);
|
|
38836
39128
|
return !invokedFunction || !analysisFunctions.has(invokedFunction);
|
|
38837
39129
|
}
|
|
38838
39130
|
return isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "UpdateExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "TaggedTemplateExpression") || isNodeOfType(node, "ThrowStatement") || isNodeOfType(node, "UnaryExpression") && node.operator === "delete";
|
|
38839
39131
|
};
|
|
38840
|
-
const
|
|
38841
|
-
if (
|
|
38842
|
-
|
|
38843
|
-
|
|
39132
|
+
const mergeKnownStateValues = (additionalStateValues, parameterStateValues) => {
|
|
39133
|
+
if (parameterStateValues.size === 0) return additionalStateValues;
|
|
39134
|
+
const knownStateValues = new Map(additionalStateValues);
|
|
39135
|
+
for (const [symbolId, value] of parameterStateValues) knownStateValues.set(symbolId, value);
|
|
39136
|
+
return knownStateValues;
|
|
39137
|
+
};
|
|
39138
|
+
const buildInvokedFunctionParameterStateValues = (invokedFunction, invocation, stateSymbolId, stateValue, scopes, additionalStateValues = /* @__PURE__ */ new Map(), callerParameterStateValues = /* @__PURE__ */ new Map()) => {
|
|
39139
|
+
if (!isFunctionLike$1(invokedFunction)) return /* @__PURE__ */ new Map();
|
|
39140
|
+
const parameterStateValues = /* @__PURE__ */ new Map();
|
|
39141
|
+
const callerKnownStateValues = mergeKnownStateValues(additionalStateValues, callerParameterStateValues);
|
|
39142
|
+
for (let parameterIndex = 0; parameterIndex < invokedFunction.params.length; parameterIndex += 1) {
|
|
39143
|
+
const rawParameter = invokedFunction.params[parameterIndex];
|
|
39144
|
+
const rawArgument = invocation.arguments[parameterIndex];
|
|
39145
|
+
if (rawArgument && isNodeOfType(rawArgument, "SpreadElement")) break;
|
|
39146
|
+
const parameter = isNodeOfType(rawParameter, "AssignmentPattern") ? rawParameter.left : rawParameter;
|
|
39147
|
+
if (!isNodeOfType(parameter, "Identifier")) continue;
|
|
39148
|
+
const parameterSymbol = scopes.symbolFor(parameter);
|
|
39149
|
+
if (!parameterSymbol) continue;
|
|
39150
|
+
let argumentValue = null;
|
|
39151
|
+
if (rawArgument) {
|
|
39152
|
+
argumentValue = readStaticEffectValue(rawArgument, scopes, stateSymbolId, stateValue, /* @__PURE__ */ new Set(), callerKnownStateValues);
|
|
39153
|
+
if (argumentValue !== null && argumentValue.value === void 0 && isNodeOfType(rawParameter, "AssignmentPattern")) {
|
|
39154
|
+
const defaultKnownStateValues = mergeKnownStateValues(callerKnownStateValues, parameterStateValues);
|
|
39155
|
+
argumentValue = readStaticEffectValue(rawParameter.right, scopes, stateSymbolId, stateValue, /* @__PURE__ */ new Set(), defaultKnownStateValues);
|
|
39156
|
+
}
|
|
39157
|
+
} else if (isNodeOfType(rawParameter, "AssignmentPattern")) {
|
|
39158
|
+
const defaultKnownStateValues = mergeKnownStateValues(callerKnownStateValues, parameterStateValues);
|
|
39159
|
+
argumentValue = readStaticEffectValue(rawParameter.right, scopes, stateSymbolId, stateValue, /* @__PURE__ */ new Set(), defaultKnownStateValues);
|
|
39160
|
+
} else argumentValue = { value: void 0 };
|
|
39161
|
+
if (argumentValue) parameterStateValues.set(parameterSymbol.id, argumentValue);
|
|
39162
|
+
}
|
|
39163
|
+
return parameterStateValues;
|
|
39164
|
+
};
|
|
39165
|
+
const haveEqualParameterStateValues = (leftValues, rightValues) => {
|
|
39166
|
+
if (leftValues.size !== rightValues.size) return false;
|
|
39167
|
+
for (const [symbolId, leftValue] of leftValues) {
|
|
39168
|
+
const rightValue = rightValues.get(symbolId);
|
|
39169
|
+
if (!rightValue || !Object.is(leftValue.value, rightValue.value)) return false;
|
|
39170
|
+
}
|
|
39171
|
+
return true;
|
|
39172
|
+
};
|
|
39173
|
+
const canReaderWorkRunForStateValues = (readerEffect, stateSymbolId, stateValue, scopes, additionalStateValues = /* @__PURE__ */ new Map()) => {
|
|
39174
|
+
const pendingFrames = [{
|
|
39175
|
+
functionNode: readerEffect.callback,
|
|
39176
|
+
parameterStateValues: /* @__PURE__ */ new Map()
|
|
39177
|
+
}];
|
|
39178
|
+
const visitedParameterStateValues = /* @__PURE__ */ new Map();
|
|
39179
|
+
while (pendingFrames.length > 0) {
|
|
39180
|
+
const frame = pendingFrames.pop();
|
|
39181
|
+
if (!frame || !isFunctionLike$1(frame.functionNode)) continue;
|
|
39182
|
+
const functionParameterStateValues = visitedParameterStateValues.get(frame.functionNode) ?? [];
|
|
39183
|
+
if (functionParameterStateValues.some((previousValues) => haveEqualParameterStateValues(previousValues, frame.parameterStateValues))) continue;
|
|
39184
|
+
functionParameterStateValues.push(frame.parameterStateValues);
|
|
39185
|
+
visitedParameterStateValues.set(frame.functionNode, functionParameterStateValues);
|
|
39186
|
+
const knownStateValues = mergeKnownStateValues(additionalStateValues, frame.parameterStateValues);
|
|
38844
39187
|
let didFindReachableWork = false;
|
|
38845
|
-
|
|
38846
|
-
if (didFindReachableWork
|
|
38847
|
-
if (
|
|
39188
|
+
walkInsideStatementBlocks(frame.functionNode.body, (child) => {
|
|
39189
|
+
if (didFindReachableWork) return;
|
|
39190
|
+
if (isNodeOfType(child, "CallExpression")) {
|
|
39191
|
+
const invokedFunction = resolveSynchronouslyInvokedFunction(child.callee, scopes);
|
|
39192
|
+
if (invokedFunction && readerEffect.analysisFunctions.has(invokedFunction)) {
|
|
39193
|
+
if (isWorkNodeReachableForStateValue(child, stateSymbolId, stateValue, scopes, knownStateValues)) pendingFrames.push({
|
|
39194
|
+
functionNode: invokedFunction,
|
|
39195
|
+
parameterStateValues: buildInvokedFunctionParameterStateValues(invokedFunction, child, stateSymbolId, stateValue, scopes, additionalStateValues, frame.parameterStateValues)
|
|
39196
|
+
});
|
|
39197
|
+
return;
|
|
39198
|
+
}
|
|
39199
|
+
}
|
|
39200
|
+
if (isReaderWorkNode(child, readerEffect.analysisFunctions, scopes) && isWorkNodeReachableForStateValue(child, stateSymbolId, stateValue, scopes, knownStateValues)) didFindReachableWork = true;
|
|
38848
39201
|
});
|
|
38849
39202
|
if (didFindReachableWork) return true;
|
|
38850
39203
|
}
|
|
38851
39204
|
return false;
|
|
38852
39205
|
};
|
|
39206
|
+
const canStateWriteReachReaderWork = (writtenStateName, writerEffect, readerEffect, stateSymbolIds, scopes) => {
|
|
39207
|
+
const writeInfo = writerEffect.stateWrites.get(writtenStateName);
|
|
39208
|
+
const stateSymbolId = stateSymbolIds.get(writtenStateName);
|
|
39209
|
+
if (!writeInfo || stateSymbolId === void 0) return true;
|
|
39210
|
+
if (writeInfo.hasUnknownValue) return true;
|
|
39211
|
+
for (const writtenValue of writeInfo.values) if (canReaderWorkRunForStateValues(readerEffect, stateSymbolId, { value: writtenValue }, scopes)) return true;
|
|
39212
|
+
return false;
|
|
39213
|
+
};
|
|
38853
39214
|
const EMPTY_CLEANUP_NAME_SET = /* @__PURE__ */ new Set();
|
|
38854
39215
|
const NON_CONTAMINATING_MAP_METHOD_NAMES = new Set([
|
|
38855
39216
|
"clear",
|
|
@@ -38860,26 +39221,499 @@ const NON_CONTAMINATING_MAP_METHOD_NAMES = new Set([
|
|
|
38860
39221
|
"keys",
|
|
38861
39222
|
"values"
|
|
38862
39223
|
]);
|
|
38863
|
-
const
|
|
38864
|
-
|
|
38865
|
-
|
|
38866
|
-
|
|
38867
|
-
|
|
38868
|
-
|
|
38869
|
-
|
|
39224
|
+
const EXTERNAL_SYNC_DIRECT_IMPORT_SOURCES = new Map([
|
|
39225
|
+
["fetch", new Set([
|
|
39226
|
+
"cross-fetch",
|
|
39227
|
+
"node-fetch",
|
|
39228
|
+
"undici"
|
|
39229
|
+
])],
|
|
39230
|
+
["ky", new Set(["ky"])],
|
|
39231
|
+
["got", new Set(["got"])],
|
|
39232
|
+
["wretch", new Set(["wretch"])],
|
|
39233
|
+
["ofetch", new Set(["ofetch"])],
|
|
39234
|
+
["setTimeout", new Set([
|
|
39235
|
+
"node:timers",
|
|
39236
|
+
"node:timers/promises",
|
|
39237
|
+
"timers",
|
|
39238
|
+
"timers/promises"
|
|
39239
|
+
])],
|
|
39240
|
+
["setInterval", new Set([
|
|
39241
|
+
"node:timers",
|
|
39242
|
+
"node:timers/promises",
|
|
39243
|
+
"timers",
|
|
39244
|
+
"timers/promises"
|
|
39245
|
+
])]
|
|
39246
|
+
]);
|
|
39247
|
+
const EXTERNAL_SYNC_DEFAULT_IMPORT_NAMES = new Map([
|
|
39248
|
+
["axios", "axios"],
|
|
39249
|
+
["cross-fetch", "fetch"],
|
|
39250
|
+
["got", "got"],
|
|
39251
|
+
["ky", "ky"],
|
|
39252
|
+
["node-fetch", "fetch"],
|
|
39253
|
+
["wretch", "wretch"]
|
|
39254
|
+
]);
|
|
39255
|
+
const EXTERNAL_SYNC_HTTP_CLIENT_MODULE_SOURCES = new Set([
|
|
39256
|
+
"axios",
|
|
39257
|
+
"cross-fetch",
|
|
39258
|
+
"got",
|
|
39259
|
+
"ky",
|
|
39260
|
+
"node-fetch",
|
|
39261
|
+
"ofetch",
|
|
39262
|
+
"undici",
|
|
39263
|
+
"wretch"
|
|
39264
|
+
]);
|
|
39265
|
+
const EXTERNAL_SYNC_GLOBAL_HTTP_CLIENT_NAMES = new Set([
|
|
39266
|
+
"axios",
|
|
39267
|
+
"got",
|
|
39268
|
+
"ky",
|
|
39269
|
+
"ofetch",
|
|
39270
|
+
"wretch"
|
|
39271
|
+
]);
|
|
39272
|
+
const EXTERNAL_SYNC_HTTP_METHOD_NAMES = new Set([
|
|
39273
|
+
...EXTERNAL_SYNC_AMBIGUOUS_HTTP_METHOD_NAMES,
|
|
39274
|
+
"fetch",
|
|
39275
|
+
"patch",
|
|
39276
|
+
"post",
|
|
39277
|
+
"put",
|
|
39278
|
+
"request"
|
|
39279
|
+
]);
|
|
39280
|
+
const TANSTACK_QUERY_CLIENT_MODULE_SOURCES = new Set(["@tanstack/query-core", "@tanstack/react-query"]);
|
|
39281
|
+
const TANSTACK_QUERY_EXTERNAL_SYNC_METHOD_NAMES = new Set(["fetchQuery", "prefetchQuery"]);
|
|
39282
|
+
const EXTERNAL_SYNC_RESOURCE_CONSTRUCTOR_NAMES = new Set([
|
|
39283
|
+
...EXTERNAL_SYNC_OBSERVER_CONSTRUCTORS,
|
|
39284
|
+
...SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP,
|
|
39285
|
+
"EventTarget",
|
|
39286
|
+
"XMLHttpRequest"
|
|
39287
|
+
]);
|
|
39288
|
+
const getDestructuredBindingDepth = (bindingIdentifier) => {
|
|
39289
|
+
let bindingNode = bindingIdentifier;
|
|
39290
|
+
let depth = 0;
|
|
39291
|
+
while (true) {
|
|
39292
|
+
if (isNodeOfType(bindingNode.parent, "AssignmentPattern") && bindingNode.parent.left === bindingNode) bindingNode = bindingNode.parent;
|
|
39293
|
+
const property = bindingNode.parent;
|
|
39294
|
+
if (!property || !isNodeOfType(property, "Property") || property.value !== bindingNode || !property.parent || !isNodeOfType(property.parent, "ObjectPattern")) return depth;
|
|
39295
|
+
depth += 1;
|
|
39296
|
+
bindingNode = property.parent;
|
|
39297
|
+
}
|
|
39298
|
+
};
|
|
39299
|
+
const isMutatedMemberExpression = (rawMemberExpression) => {
|
|
39300
|
+
let expression = findTransparentExpressionRoot(rawMemberExpression);
|
|
39301
|
+
while (expression.parent) {
|
|
39302
|
+
const parent = expression.parent;
|
|
39303
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === expression) {
|
|
39304
|
+
expression = findTransparentExpressionRoot(parent);
|
|
39305
|
+
continue;
|
|
39306
|
+
}
|
|
39307
|
+
if (isNodeOfType(parent, "Property") && parent.value === expression && isNodeOfType(parent.parent, "ObjectPattern")) {
|
|
39308
|
+
expression = findTransparentExpressionRoot(parent);
|
|
39309
|
+
continue;
|
|
38870
39310
|
}
|
|
38871
|
-
|
|
39311
|
+
if (isNodeOfType(parent, "ObjectPattern") && parent.properties.some((property) => property === expression)) {
|
|
39312
|
+
expression = findTransparentExpressionRoot(parent);
|
|
39313
|
+
continue;
|
|
39314
|
+
}
|
|
39315
|
+
if (isNodeOfType(parent, "ArrayPattern") && parent.elements.some((element) => element === expression)) {
|
|
39316
|
+
expression = findTransparentExpressionRoot(parent);
|
|
39317
|
+
continue;
|
|
39318
|
+
}
|
|
39319
|
+
if (isNodeOfType(parent, "RestElement") && parent.argument === expression || isNodeOfType(parent, "AssignmentPattern") && parent.left === expression) {
|
|
39320
|
+
expression = findTransparentExpressionRoot(parent);
|
|
39321
|
+
continue;
|
|
39322
|
+
}
|
|
39323
|
+
break;
|
|
38872
39324
|
}
|
|
38873
|
-
|
|
39325
|
+
const parent = expression.parent;
|
|
39326
|
+
return Boolean(isNodeOfType(parent, "AssignmentExpression") && parent.left === expression || isNodeOfType(parent, "UpdateExpression") && parent.argument === expression || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === expression || (isNodeOfType(parent, "ForInStatement") || isNodeOfType(parent, "ForOfStatement")) && parent.left === expression);
|
|
39327
|
+
};
|
|
39328
|
+
const isReactHookDependencyArgument = (expression, scopes) => {
|
|
39329
|
+
const call = expression.parent;
|
|
39330
|
+
return Boolean(isNodeOfType(call, "CallExpression") && call.arguments[1] === expression && isReactApiCall(call, HOOKS_WITH_DEPS, scopes, {
|
|
39331
|
+
allowGlobalReactNamespace: true,
|
|
39332
|
+
allowUnboundBareCalls: true,
|
|
39333
|
+
resolveNamedAliases: true
|
|
39334
|
+
}));
|
|
39335
|
+
};
|
|
39336
|
+
const isPlainDependencyObjectExpression = (expression) => isNodeOfType(expression, "ObjectExpression") && expression.properties.every((property) => isNodeOfType(property, "Property") && property.kind === "init" && !property.computed && !property.method);
|
|
39337
|
+
const getContainingReactDependencyValue = (rawExpression, containerKind) => {
|
|
39338
|
+
const expression = findTransparentExpressionRoot(rawExpression);
|
|
39339
|
+
const parent = expression.parent;
|
|
39340
|
+
if (isNodeOfType(parent, "ArrayExpression")) return {
|
|
39341
|
+
expression: parent,
|
|
39342
|
+
containerKind: "array"
|
|
39343
|
+
};
|
|
39344
|
+
if (containerKind === "array" && isNodeOfType(parent, "SpreadElement") && parent.argument === expression && isNodeOfType(parent.parent, "ArrayExpression")) return {
|
|
39345
|
+
expression: parent.parent,
|
|
39346
|
+
containerKind: "array"
|
|
39347
|
+
};
|
|
39348
|
+
if (isNodeOfType(parent, "Property") && (parent.value === expression || parent.shorthand && parent.key === expression) && parent.parent && isPlainDependencyObjectExpression(parent.parent)) return {
|
|
39349
|
+
expression: parent.parent,
|
|
39350
|
+
containerKind: "object"
|
|
39351
|
+
};
|
|
39352
|
+
return null;
|
|
39353
|
+
};
|
|
39354
|
+
const getFullArrayRestCopySymbol = (initializer, scopes) => {
|
|
39355
|
+
const declaration = initializer.parent;
|
|
39356
|
+
if (!isNodeOfType(declaration, "VariableDeclarator") || declaration.init !== initializer || !isNodeOfType(declaration.id, "ArrayPattern") || declaration.id.elements.length !== 1) return null;
|
|
39357
|
+
const restElement = declaration.id.elements[0];
|
|
39358
|
+
if (!isNodeOfType(restElement, "RestElement") || !isNodeOfType(restElement.argument, "Identifier")) return null;
|
|
39359
|
+
const symbol = scopes.symbolFor(restElement.argument);
|
|
39360
|
+
return symbol?.kind === "const" && symbol.declarationNode === declaration && symbol.references.every((reference) => reference.flag === "read") ? symbol : null;
|
|
39361
|
+
};
|
|
39362
|
+
const isReactDependencyArrayReference = (initialExpression, scopes) => {
|
|
39363
|
+
const pendingPaths = [{
|
|
39364
|
+
expression: initialExpression,
|
|
39365
|
+
containerKind: null
|
|
39366
|
+
}];
|
|
39367
|
+
const visitedExpressions = /* @__PURE__ */ new Set();
|
|
39368
|
+
let didFindReactDependencyArgument = false;
|
|
39369
|
+
while (pendingPaths.length > 0) {
|
|
39370
|
+
const pendingPath = pendingPaths.pop();
|
|
39371
|
+
if (!pendingPath) continue;
|
|
39372
|
+
const expression = findTransparentExpressionRoot(pendingPath.expression);
|
|
39373
|
+
if (visitedExpressions.has(expression)) continue;
|
|
39374
|
+
visitedExpressions.add(expression);
|
|
39375
|
+
if (pendingPath.containerKind === "array" && isReactHookDependencyArgument(expression, scopes)) {
|
|
39376
|
+
didFindReactDependencyArgument = true;
|
|
39377
|
+
continue;
|
|
39378
|
+
}
|
|
39379
|
+
const containingValue = getContainingReactDependencyValue(expression, pendingPath.containerKind);
|
|
39380
|
+
if (containingValue) {
|
|
39381
|
+
pendingPaths.push(containingValue);
|
|
39382
|
+
continue;
|
|
39383
|
+
}
|
|
39384
|
+
const declaration = expression.parent;
|
|
39385
|
+
if (isNodeOfType(declaration, "VariableDeclarator") && declaration.init === expression && isNodeOfType(declaration.id, "Identifier")) {
|
|
39386
|
+
const symbol = scopes.symbolFor(declaration.id);
|
|
39387
|
+
if (!symbol || getDirectUnreassignedInitializer(symbol) !== expression) return false;
|
|
39388
|
+
if (symbol.references.length === 0) return false;
|
|
39389
|
+
for (const reference of symbol.references) pendingPaths.push({
|
|
39390
|
+
expression: reference.identifier,
|
|
39391
|
+
containerKind: pendingPath.containerKind
|
|
39392
|
+
});
|
|
39393
|
+
continue;
|
|
39394
|
+
}
|
|
39395
|
+
if (pendingPath.containerKind === "array") {
|
|
39396
|
+
const restCopySymbol = getFullArrayRestCopySymbol(expression, scopes);
|
|
39397
|
+
if (restCopySymbol) {
|
|
39398
|
+
if (restCopySymbol.references.length === 0) return false;
|
|
39399
|
+
for (const reference of restCopySymbol.references) pendingPaths.push({
|
|
39400
|
+
expression: reference.identifier,
|
|
39401
|
+
containerKind: "array"
|
|
39402
|
+
});
|
|
39403
|
+
continue;
|
|
39404
|
+
}
|
|
39405
|
+
}
|
|
39406
|
+
return false;
|
|
39407
|
+
}
|
|
39408
|
+
return didFindReactDependencyArgument;
|
|
39409
|
+
};
|
|
39410
|
+
const hasSafeReceiverAliases = (rootSymbols, scopes) => {
|
|
39411
|
+
const pendingSymbols = [...rootSymbols];
|
|
39412
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
39413
|
+
while (pendingSymbols.length > 0) {
|
|
39414
|
+
const symbol = pendingSymbols.pop();
|
|
39415
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) continue;
|
|
39416
|
+
visitedSymbolIds.add(symbol.id);
|
|
39417
|
+
for (const reference of symbol.references) {
|
|
39418
|
+
const expression = findTransparentExpressionRoot(reference.identifier);
|
|
39419
|
+
const container = expression.parent;
|
|
39420
|
+
if (isReactDependencyArrayReference(expression, scopes)) continue;
|
|
39421
|
+
if (isNodeOfType(container, "MemberExpression") && container.object === expression) {
|
|
39422
|
+
if (isMutatedMemberExpression(container)) return false;
|
|
39423
|
+
continue;
|
|
39424
|
+
}
|
|
39425
|
+
if (isNodeOfType(container, "CallExpression") && container.callee === expression) continue;
|
|
39426
|
+
if (isNodeOfType(container, "VariableDeclarator") && container.init === expression && isNodeOfType(container.id, "ObjectPattern")) continue;
|
|
39427
|
+
if (isNodeOfType(container, "AssignmentExpression") && container.right === expression && isNodeOfType(container.left, "ObjectPattern")) continue;
|
|
39428
|
+
if (isNodeOfType(container, "VariableDeclarator") && container.init === expression && isNodeOfType(container.id, "Identifier")) {
|
|
39429
|
+
const aliasSymbol = scopes.symbolFor(container.id);
|
|
39430
|
+
const aliasInitializer = aliasSymbol && getDirectUnreassignedInitializer(aliasSymbol);
|
|
39431
|
+
if (!aliasSymbol || aliasInitializer !== expression) return false;
|
|
39432
|
+
pendingSymbols.push(aliasSymbol);
|
|
39433
|
+
continue;
|
|
39434
|
+
}
|
|
39435
|
+
return false;
|
|
39436
|
+
}
|
|
39437
|
+
}
|
|
39438
|
+
return true;
|
|
39439
|
+
};
|
|
39440
|
+
const hasProvenGlobalNamespaceReference = (expression, namespaceNames, scopes) => {
|
|
39441
|
+
for (const namespaceName of namespaceNames) if (isProvenGlobalNamespaceReference(expression, namespaceName, scopes)) return true;
|
|
38874
39442
|
return false;
|
|
38875
39443
|
};
|
|
38876
|
-
const
|
|
38877
|
-
|
|
38878
|
-
|
|
38879
|
-
if (isNodeOfType(
|
|
38880
|
-
|
|
39444
|
+
const getTypeScriptImportEqualsSource = (symbol) => {
|
|
39445
|
+
if (symbol.kind !== "ts-import-equals" || !isNodeOfType(symbol.declarationNode, "TSImportEqualsDeclaration")) return null;
|
|
39446
|
+
const moduleReference = symbol.declarationNode.moduleReference;
|
|
39447
|
+
if (!isNodeOfType(moduleReference, "TSExternalModuleReference") || !isNodeOfType(moduleReference.expression, "Literal")) return null;
|
|
39448
|
+
return typeof moduleReference.expression.value === "string" ? moduleReference.expression.value : null;
|
|
39449
|
+
};
|
|
39450
|
+
const getUnshadowedRequireSource = (rawExpression, scopes) => {
|
|
39451
|
+
let expression = stripParenExpression(rawExpression);
|
|
39452
|
+
while (isNodeOfType(expression, "MemberExpression")) expression = stripParenExpression(expression.object);
|
|
39453
|
+
if (!isNodeOfType(expression, "CallExpression") || !isNodeOfType(expression.callee, "Identifier") || expression.callee.name !== "require" || !scopes.isGlobalReference(expression.callee)) return null;
|
|
39454
|
+
return getRequireCallSource(rawExpression);
|
|
39455
|
+
};
|
|
39456
|
+
const resolveExternalModuleBinding = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set(), ignoreCommonJsMutation = false) => {
|
|
39457
|
+
let expression = stripParenExpression(rawExpression);
|
|
39458
|
+
let outermostExportedName = null;
|
|
39459
|
+
let exportedPropertyDepth = 0;
|
|
39460
|
+
let resolvedBinding = null;
|
|
39461
|
+
let isCommonJsModuleObject = false;
|
|
39462
|
+
const traversedSymbolIds = new Set(visitedSymbolIds);
|
|
39463
|
+
const commonJsModuleObjectSymbols = /* @__PURE__ */ new Set();
|
|
39464
|
+
while (true) {
|
|
39465
|
+
if (isNodeOfType(expression, "MemberExpression")) {
|
|
39466
|
+
const propertyName = getStaticPropertyName(expression);
|
|
39467
|
+
if (propertyName === null) return null;
|
|
39468
|
+
outermostExportedName ??= propertyName;
|
|
39469
|
+
exportedPropertyDepth += 1;
|
|
39470
|
+
expression = stripParenExpression(expression.object);
|
|
39471
|
+
continue;
|
|
39472
|
+
}
|
|
39473
|
+
const requireSource = getUnshadowedRequireSource(expression, scopes);
|
|
39474
|
+
if (requireSource !== null) {
|
|
39475
|
+
resolvedBinding = {
|
|
39476
|
+
source: requireSource,
|
|
39477
|
+
exportedName: null,
|
|
39478
|
+
isModuleObject: true
|
|
39479
|
+
};
|
|
39480
|
+
isCommonJsModuleObject = true;
|
|
39481
|
+
break;
|
|
39482
|
+
}
|
|
39483
|
+
if (!isNodeOfType(expression, "Identifier")) return null;
|
|
39484
|
+
const symbol = scopes.symbolFor(expression);
|
|
39485
|
+
if (!symbol || traversedSymbolIds.has(symbol.id)) return null;
|
|
39486
|
+
if (symbol.kind === "import") {
|
|
39487
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
39488
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration") || typeof importDeclaration.source.value !== "string") return null;
|
|
39489
|
+
if (isNodeOfType(symbol.declarationNode, "ImportSpecifier")) resolvedBinding = {
|
|
39490
|
+
source: importDeclaration.source.value,
|
|
39491
|
+
exportedName: getImportedName(symbol.declarationNode) ?? null,
|
|
39492
|
+
isModuleObject: false
|
|
39493
|
+
};
|
|
39494
|
+
else if (isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier")) resolvedBinding = {
|
|
39495
|
+
source: importDeclaration.source.value,
|
|
39496
|
+
exportedName: "default",
|
|
39497
|
+
isModuleObject: false
|
|
39498
|
+
};
|
|
39499
|
+
else if (isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")) resolvedBinding = {
|
|
39500
|
+
source: importDeclaration.source.value,
|
|
39501
|
+
exportedName: null,
|
|
39502
|
+
isModuleObject: true
|
|
39503
|
+
};
|
|
39504
|
+
break;
|
|
39505
|
+
}
|
|
39506
|
+
const importEqualsSource = getTypeScriptImportEqualsSource(symbol);
|
|
39507
|
+
if (importEqualsSource !== null) {
|
|
39508
|
+
resolvedBinding = {
|
|
39509
|
+
source: importEqualsSource,
|
|
39510
|
+
exportedName: null,
|
|
39511
|
+
isModuleObject: true
|
|
39512
|
+
};
|
|
39513
|
+
isCommonJsModuleObject = true;
|
|
39514
|
+
commonJsModuleObjectSymbols.add(symbol);
|
|
39515
|
+
break;
|
|
39516
|
+
}
|
|
39517
|
+
const directInitializer = getDirectUnreassignedInitializer(symbol);
|
|
39518
|
+
const destructuredPropertyName = getDestructuredBindingPropertyName(symbol.bindingIdentifier);
|
|
39519
|
+
const initializer = directInitializer ?? (destructuredPropertyName !== null && symbol.kind === "const" && isNodeOfType(symbol.declarationNode, "VariableDeclarator") ? symbol.declarationNode.init : null);
|
|
39520
|
+
if (!initializer) return null;
|
|
39521
|
+
traversedSymbolIds.add(symbol.id);
|
|
39522
|
+
outermostExportedName ??= destructuredPropertyName;
|
|
39523
|
+
if (destructuredPropertyName !== null) exportedPropertyDepth += getDestructuredBindingDepth(symbol.bindingIdentifier);
|
|
39524
|
+
const unwrappedInitializer = stripParenExpression(initializer);
|
|
39525
|
+
if (destructuredPropertyName === null && !isNodeOfType(unwrappedInitializer, "MemberExpression")) commonJsModuleObjectSymbols.add(symbol);
|
|
39526
|
+
expression = unwrappedInitializer;
|
|
39527
|
+
}
|
|
39528
|
+
if (!resolvedBinding) return null;
|
|
39529
|
+
if (!ignoreCommonJsMutation && isCommonJsModuleObject && !hasSafeReceiverAliases(commonJsModuleObjectSymbols, scopes)) return null;
|
|
39530
|
+
if (outermostExportedName === null) return resolvedBinding;
|
|
39531
|
+
if (!resolvedBinding.isModuleObject || exportedPropertyDepth !== 1) return null;
|
|
39532
|
+
return {
|
|
39533
|
+
source: resolvedBinding.source,
|
|
39534
|
+
exportedName: outermostExportedName,
|
|
39535
|
+
isModuleObject: false
|
|
39536
|
+
};
|
|
39537
|
+
};
|
|
39538
|
+
const getCanonicalExternalSyncExportName = (binding) => {
|
|
39539
|
+
if (binding.exportedName && binding.exportedName !== "default") return binding.exportedName;
|
|
39540
|
+
return EXTERNAL_SYNC_DEFAULT_IMPORT_NAMES.get(binding.source) ?? null;
|
|
39541
|
+
};
|
|
39542
|
+
const isKnownExternalSyncDirectModuleBinding = (binding) => {
|
|
39543
|
+
const exportedName = getCanonicalExternalSyncExportName(binding);
|
|
39544
|
+
return Boolean(exportedName && EXTERNAL_SYNC_DIRECT_IMPORT_SOURCES.get(exportedName)?.has(binding.source));
|
|
39545
|
+
};
|
|
39546
|
+
const DEFINITELY_NON_FUNCTION_GLOBAL_CALL_NAMES = new Set([
|
|
39547
|
+
"Array",
|
|
39548
|
+
"BigInt",
|
|
39549
|
+
"Boolean",
|
|
39550
|
+
"Date",
|
|
39551
|
+
"Number",
|
|
39552
|
+
"String",
|
|
39553
|
+
"Symbol"
|
|
39554
|
+
]);
|
|
39555
|
+
const DEFINITELY_NON_FUNCTION_GLOBAL_CONSTRUCTOR_NAMES = new Set([
|
|
39556
|
+
"Array",
|
|
39557
|
+
"Boolean",
|
|
39558
|
+
"Date",
|
|
39559
|
+
"Map",
|
|
39560
|
+
"Number",
|
|
39561
|
+
"Promise",
|
|
39562
|
+
"RegExp",
|
|
39563
|
+
"Set",
|
|
39564
|
+
"String",
|
|
39565
|
+
"URL",
|
|
39566
|
+
"URLSearchParams",
|
|
39567
|
+
"WeakMap",
|
|
39568
|
+
"WeakSet"
|
|
39569
|
+
]);
|
|
39570
|
+
const PROMISE_STATIC_RESULT_METHOD_NAMES = new Set([
|
|
39571
|
+
"all",
|
|
39572
|
+
"allSettled",
|
|
39573
|
+
"any",
|
|
39574
|
+
"race",
|
|
39575
|
+
"reject",
|
|
39576
|
+
"resolve",
|
|
39577
|
+
"withResolvers"
|
|
39578
|
+
]);
|
|
39579
|
+
const returnsOnlyNonCleanupValues = (functionNode, setterToStateName, scopes, visitedFunctions = /* @__PURE__ */ new Set()) => {
|
|
39580
|
+
if (!isFunctionLike$1(functionNode) || functionNode.async || functionNode.generator || visitedFunctions.has(functionNode)) return false;
|
|
39581
|
+
const nextVisitedFunctions = new Set(visitedFunctions).add(functionNode);
|
|
39582
|
+
const isNonCleanupValue = (returnValue) => {
|
|
39583
|
+
const expression = stripParenExpression(returnValue);
|
|
39584
|
+
if (isNodeOfType(expression, "Identifier") && expression.name === "undefined" && scopes.isGlobalReference(expression)) return true;
|
|
39585
|
+
if (isNodeOfType(expression, "Literal") || isNodeOfType(expression, "ArrayExpression") || isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "JSXElement") || isNodeOfType(expression, "JSXFragment") || isNodeOfType(expression, "TemplateLiteral") || isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return true;
|
|
39586
|
+
if (isNodeOfType(expression, "ConditionalExpression")) return isNonCleanupValue(expression.consequent) && isNonCleanupValue(expression.alternate);
|
|
39587
|
+
if (isNodeOfType(expression, "LogicalExpression")) return isNonCleanupValue(expression.left) && isNonCleanupValue(expression.right);
|
|
39588
|
+
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
39589
|
+
const finalExpression = expression.expressions.at(-1);
|
|
39590
|
+
return Boolean(finalExpression && isNonCleanupValue(finalExpression));
|
|
39591
|
+
}
|
|
39592
|
+
if (isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && DEFINITELY_NON_FUNCTION_GLOBAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && scopes.isGlobalReference(expression.callee)) return true;
|
|
39593
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
39594
|
+
if (isNodeOfType(expression.callee, "Identifier") && DEFINITELY_NON_FUNCTION_GLOBAL_CALL_NAMES.has(expression.callee.name) && scopes.isGlobalReference(expression.callee)) return true;
|
|
39595
|
+
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.object, "Identifier") && expression.callee.object.name === "Promise" && scopes.isGlobalReference(expression.callee.object) && PROMISE_STATIC_RESULT_METHOD_NAMES.has(getStaticPropertyName(expression.callee) ?? "")) return true;
|
|
39596
|
+
if (isNodeOfType(expression.callee, "Identifier") && setterToStateName.has(expression.callee.name)) return true;
|
|
39597
|
+
const invokedFunction = resolveSynchronouslyInvokedFunction(expression.callee, scopes);
|
|
39598
|
+
return Boolean(invokedFunction && returnsOnlyNonCleanupValues(invokedFunction, setterToStateName, scopes, nextVisitedFunctions));
|
|
39599
|
+
};
|
|
39600
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) return isNonCleanupValue(functionNode.body);
|
|
39601
|
+
let returnsOnlyNonCleanup = true;
|
|
39602
|
+
walkAst(functionNode.body, (child) => {
|
|
39603
|
+
if (!returnsOnlyNonCleanup) return false;
|
|
39604
|
+
if (child !== functionNode.body && isFunctionLike$1(child)) return false;
|
|
39605
|
+
if (!isNodeOfType(child, "ReturnStatement")) return;
|
|
39606
|
+
if (child.argument && !isNonCleanupValue(child.argument)) {
|
|
39607
|
+
returnsOnlyNonCleanup = false;
|
|
39608
|
+
return false;
|
|
39609
|
+
}
|
|
39610
|
+
});
|
|
39611
|
+
return returnsOnlyNonCleanup;
|
|
39612
|
+
};
|
|
39613
|
+
const getResolvedFunctionCleanupReturnProof = (functionNode, scopes, visitedFunctions = /* @__PURE__ */ new Set()) => {
|
|
39614
|
+
if (!isFunctionLike$1(functionNode) || functionNode.async || functionNode.generator || visitedFunctions.has(functionNode)) return {
|
|
39615
|
+
hasCleanup: false,
|
|
39616
|
+
isValid: false
|
|
39617
|
+
};
|
|
39618
|
+
const nextVisitedFunctions = new Set(visitedFunctions).add(functionNode);
|
|
39619
|
+
const getCleanupReturnProof = (returnValue) => {
|
|
39620
|
+
const expression = stripParenExpression(returnValue);
|
|
39621
|
+
if (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression")) return {
|
|
39622
|
+
hasCleanup: true,
|
|
39623
|
+
isValid: true
|
|
39624
|
+
};
|
|
39625
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
39626
|
+
if (expression.name === "undefined" && scopes.isGlobalReference(expression)) return {
|
|
39627
|
+
hasCleanup: false,
|
|
39628
|
+
isValid: true
|
|
39629
|
+
};
|
|
39630
|
+
const localFunction = resolveExactLocalFunction(expression, scopes);
|
|
39631
|
+
return {
|
|
39632
|
+
hasCleanup: Boolean(localFunction),
|
|
39633
|
+
isValid: Boolean(localFunction)
|
|
39634
|
+
};
|
|
39635
|
+
}
|
|
39636
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return {
|
|
39637
|
+
hasCleanup: false,
|
|
39638
|
+
isValid: true
|
|
39639
|
+
};
|
|
39640
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
39641
|
+
const consequentProof = getCleanupReturnProof(expression.consequent);
|
|
39642
|
+
const alternateProof = getCleanupReturnProof(expression.alternate);
|
|
39643
|
+
return {
|
|
39644
|
+
hasCleanup: consequentProof.hasCleanup || alternateProof.hasCleanup,
|
|
39645
|
+
isValid: consequentProof.isValid && alternateProof.isValid
|
|
39646
|
+
};
|
|
39647
|
+
}
|
|
39648
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
39649
|
+
const leftProof = getCleanupReturnProof(expression.left);
|
|
39650
|
+
const rightProof = getCleanupReturnProof(expression.right);
|
|
39651
|
+
return {
|
|
39652
|
+
hasCleanup: leftProof.hasCleanup || rightProof.hasCleanup,
|
|
39653
|
+
isValid: leftProof.isValid && rightProof.isValid
|
|
39654
|
+
};
|
|
39655
|
+
}
|
|
39656
|
+
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
39657
|
+
const finalExpression = expression.expressions.at(-1);
|
|
39658
|
+
return finalExpression ? getCleanupReturnProof(finalExpression) : {
|
|
39659
|
+
hasCleanup: false,
|
|
39660
|
+
isValid: false
|
|
39661
|
+
};
|
|
39662
|
+
}
|
|
39663
|
+
if (!isNodeOfType(expression, "CallExpression")) return {
|
|
39664
|
+
hasCleanup: false,
|
|
39665
|
+
isValid: false
|
|
39666
|
+
};
|
|
39667
|
+
const invokedFunction = resolveSynchronouslyInvokedFunction(expression.callee, scopes);
|
|
39668
|
+
return invokedFunction ? getResolvedFunctionCleanupReturnProof(invokedFunction, scopes, nextVisitedFunctions) : {
|
|
39669
|
+
hasCleanup: false,
|
|
39670
|
+
isValid: false
|
|
39671
|
+
};
|
|
39672
|
+
};
|
|
39673
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) return getCleanupReturnProof(functionNode.body);
|
|
39674
|
+
let cleanupReturnProof = {
|
|
39675
|
+
hasCleanup: false,
|
|
39676
|
+
isValid: true
|
|
39677
|
+
};
|
|
39678
|
+
walkAst(functionNode.body, (child) => {
|
|
39679
|
+
if (!cleanupReturnProof.isValid) return false;
|
|
39680
|
+
if (child !== functionNode.body && isFunctionLike$1(child)) return false;
|
|
39681
|
+
if (!isNodeOfType(child, "ReturnStatement")) return;
|
|
39682
|
+
if (!child.argument) return;
|
|
39683
|
+
const returnProof = getCleanupReturnProof(child.argument);
|
|
39684
|
+
if (!returnProof.isValid) {
|
|
39685
|
+
cleanupReturnProof = {
|
|
39686
|
+
hasCleanup: false,
|
|
39687
|
+
isValid: false
|
|
39688
|
+
};
|
|
39689
|
+
return false;
|
|
39690
|
+
}
|
|
39691
|
+
cleanupReturnProof.hasCleanup ||= returnProof.hasCleanup;
|
|
39692
|
+
});
|
|
39693
|
+
return cleanupReturnProof;
|
|
39694
|
+
};
|
|
39695
|
+
const isFunctionShapedReturn = (returnedValue, setterToStateName, setterSymbolIdToStateName, scopes, isExplicitReturnStatement) => {
|
|
39696
|
+
const unwrappedReturnedValue = stripParenExpression(returnedValue);
|
|
39697
|
+
if (isNodeOfType(unwrappedReturnedValue, "ArrowFunctionExpression") || isNodeOfType(unwrappedReturnedValue, "FunctionExpression")) return true;
|
|
39698
|
+
if (isNodeOfType(unwrappedReturnedValue, "CallExpression")) {
|
|
39699
|
+
if (isNodeOfType(unwrappedReturnedValue.callee, "Identifier")) {
|
|
39700
|
+
const setterSymbol = resolveConstIdentifierAlias(unwrappedReturnedValue.callee, scopes, true);
|
|
39701
|
+
if (setterToStateName.has(unwrappedReturnedValue.callee.name) || setterSymbol && setterSymbolIdToStateName.has(setterSymbol.id)) return false;
|
|
39702
|
+
}
|
|
39703
|
+
const invokedFunction = resolveSynchronouslyInvokedFunction(unwrappedReturnedValue.callee, scopes);
|
|
39704
|
+
if (invokedFunction && returnsOnlyNonCleanupValues(invokedFunction, setterToStateName, scopes)) return false;
|
|
39705
|
+
if (invokedFunction) {
|
|
39706
|
+
const cleanupReturnProof = getResolvedFunctionCleanupReturnProof(invokedFunction, scopes);
|
|
39707
|
+
if (cleanupReturnProof.isValid) return cleanupReturnProof.hasCleanup;
|
|
39708
|
+
}
|
|
39709
|
+
if (!invokedFunction && isNodeOfType(unwrappedReturnedValue.callee, "Identifier") && isSetterIdentifier(unwrappedReturnedValue.callee.name)) return true;
|
|
39710
|
+
return isCleanupReturn(unwrappedReturnedValue, EMPTY_CLEANUP_NAME_SET, EMPTY_CLEANUP_NAME_SET, { allowOpaqueReturn: isExplicitReturnStatement });
|
|
39711
|
+
}
|
|
39712
|
+
if (isNodeOfType(unwrappedReturnedValue, "Identifier")) return true;
|
|
38881
39713
|
return false;
|
|
38882
39714
|
};
|
|
39715
|
+
const STORAGE_GLOBAL_NAMES = ["localStorage", "sessionStorage"];
|
|
39716
|
+
const isBrowserStorageReceiver = (receiver, scopes) => Boolean(receiver && STORAGE_GLOBAL_NAMES.some((storageName) => isProvenGlobalNamespaceReference(receiver, storageName, scopes)));
|
|
38883
39717
|
const STORAGE_HOOK_PATTERN = /^use\w*Storage/i;
|
|
38884
39718
|
const collectStorageHookSetterNames = (componentBody) => {
|
|
38885
39719
|
const setterNames = /* @__PURE__ */ new Set();
|
|
@@ -38911,23 +39745,16 @@ const callsOpaqueExternalSetter = (analysisFunctions, setterToStateName) => {
|
|
|
38911
39745
|
});
|
|
38912
39746
|
return didFindOpaqueSetterCall;
|
|
38913
39747
|
};
|
|
38914
|
-
const
|
|
39748
|
+
const isReactUseRefCall = (expression, scopes) => isNodeOfType(expression, "CallExpression") && isReactApiCall(expression, "useRef", scopes, {
|
|
38915
39749
|
allowGlobalReactNamespace: true,
|
|
38916
39750
|
allowUnboundBareCalls: true,
|
|
38917
39751
|
resolveNamedAliases: true
|
|
38918
|
-
})
|
|
39752
|
+
});
|
|
39753
|
+
const isReactRefCall = (expression, scopes) => isReactUseRefCall(expression, scopes) || isNodeOfType(expression, "CallExpression") && isReactApiCall(expression, "createRef", scopes, {
|
|
38919
39754
|
allowGlobalReactNamespace: true,
|
|
38920
39755
|
allowUnboundBareCalls: true,
|
|
38921
39756
|
resolveNamedAliases: true
|
|
38922
|
-
})
|
|
38923
|
-
const getDirectReactRefSymbol = (rawExpression, scopes) => {
|
|
38924
|
-
const expression = stripParenExpression(rawExpression);
|
|
38925
|
-
if (!isNodeOfType(expression, "Identifier")) return null;
|
|
38926
|
-
const symbol = scopes.symbolFor(expression);
|
|
38927
|
-
if (!symbol) return null;
|
|
38928
|
-
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
38929
|
-
return initializer && isReactRefCall(stripParenExpression(initializer), scopes) ? symbol : null;
|
|
38930
|
-
};
|
|
39757
|
+
});
|
|
38931
39758
|
const isReactNativeJsxElement = (openingElement, scopes) => {
|
|
38932
39759
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
38933
39760
|
const symbol = scopes.symbolFor(openingElement.name);
|
|
@@ -38935,19 +39762,33 @@ const isReactNativeJsxElement = (openingElement, scopes) => {
|
|
|
38935
39762
|
return Boolean(symbol?.kind === "import" && importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react-native");
|
|
38936
39763
|
};
|
|
38937
39764
|
const isDirectHostJsxRef = (symbol, scopes) => {
|
|
38938
|
-
let
|
|
38939
|
-
|
|
38940
|
-
|
|
38941
|
-
|
|
38942
|
-
|
|
38943
|
-
if (!
|
|
38944
|
-
|
|
38945
|
-
|
|
38946
|
-
|
|
38947
|
-
|
|
38948
|
-
|
|
38949
|
-
|
|
38950
|
-
|
|
39765
|
+
let didFindHostRef = false;
|
|
39766
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
39767
|
+
const pendingSymbols = [symbol];
|
|
39768
|
+
while (pendingSymbols.length > 0) {
|
|
39769
|
+
const currentSymbol = pendingSymbols.pop();
|
|
39770
|
+
if (!currentSymbol || visitedSymbolIds.has(currentSymbol.id)) continue;
|
|
39771
|
+
visitedSymbolIds.add(currentSymbol.id);
|
|
39772
|
+
for (const reference of currentSymbol.references) {
|
|
39773
|
+
const expression = findTransparentExpressionRoot(reference.identifier);
|
|
39774
|
+
const container = expression.parent;
|
|
39775
|
+
if (isNodeOfType(container, "MemberExpression") && container.object === expression && getStaticPropertyName(container) === "current") continue;
|
|
39776
|
+
if (isNodeOfType(container, "VariableDeclarator") && container.init === expression && isNodeOfType(container.id, "Identifier")) {
|
|
39777
|
+
const aliasSymbol = scopes.symbolFor(container.id);
|
|
39778
|
+
const aliasInitializer = aliasSymbol && getDirectUnreassignedInitializer(aliasSymbol);
|
|
39779
|
+
if (!aliasSymbol || aliasInitializer !== expression) return false;
|
|
39780
|
+
pendingSymbols.push(aliasSymbol);
|
|
39781
|
+
continue;
|
|
39782
|
+
}
|
|
39783
|
+
if (!container || !isNodeOfType(container, "JSXExpressionContainer") || container.expression !== expression) return false;
|
|
39784
|
+
const attribute = container.parent;
|
|
39785
|
+
if (!attribute || !isNodeOfType(attribute, "JSXAttribute") || getJsxAttributeName(attribute.name) !== "ref") return false;
|
|
39786
|
+
const openingElement = attribute.parent;
|
|
39787
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement") || !isProvenIntrinsicJsxElement(openingElement, scopes) && !isReactNativeJsxElement(openingElement, scopes)) return false;
|
|
39788
|
+
didFindHostRef = true;
|
|
39789
|
+
}
|
|
39790
|
+
}
|
|
39791
|
+
return didFindHostRef;
|
|
38951
39792
|
};
|
|
38952
39793
|
const isIntrinsicRefCallbackParameter = (expression, scopes) => {
|
|
38953
39794
|
const identifier = stripParenExpression(expression);
|
|
@@ -38957,7 +39798,18 @@ const isIntrinsicRefCallbackParameter = (expression, scopes) => {
|
|
|
38957
39798
|
const rawFirstParameter = callback.params?.[0];
|
|
38958
39799
|
const firstParameter = isNodeOfType(rawFirstParameter, "AssignmentPattern") ? rawFirstParameter.left : rawFirstParameter;
|
|
38959
39800
|
const symbol = scopes.symbolFor(identifier);
|
|
38960
|
-
return Boolean(firstParameter && symbol?.bindingIdentifier === firstParameter)
|
|
39801
|
+
return Boolean(firstParameter && symbol?.bindingIdentifier === firstParameter && symbol.references.every((reference) => {
|
|
39802
|
+
if (reference.flag !== "read") return false;
|
|
39803
|
+
let referenceRoot = reference.identifier;
|
|
39804
|
+
while (referenceRoot.parent) {
|
|
39805
|
+
const parent = referenceRoot.parent;
|
|
39806
|
+
if (isNodeOfType(parent, "AssignmentExpression")) return parent.left !== referenceRoot;
|
|
39807
|
+
if (isNodeOfType(parent, "UpdateExpression")) return parent.argument !== referenceRoot;
|
|
39808
|
+
if (isNodeOfType(parent, "ForInStatement") || isNodeOfType(parent, "ForOfStatement")) return parent.left !== referenceRoot;
|
|
39809
|
+
referenceRoot = parent;
|
|
39810
|
+
}
|
|
39811
|
+
return true;
|
|
39812
|
+
}));
|
|
38961
39813
|
};
|
|
38962
39814
|
const getDirectReactRefCall = (symbol, scopes) => {
|
|
38963
39815
|
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
@@ -38965,15 +39817,38 @@ const getDirectReactRefCall = (symbol, scopes) => {
|
|
|
38965
39817
|
const expression = stripParenExpression(initializer);
|
|
38966
39818
|
return isNodeOfType(expression, "CallExpression") && isReactRefCall(expression, scopes) ? expression : null;
|
|
38967
39819
|
};
|
|
39820
|
+
const isEmptyGlobalMapConstruction = (rawExpression, scopes) => {
|
|
39821
|
+
const expression = stripParenExpression(rawExpression);
|
|
39822
|
+
return isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "Map" && scopes.isGlobalReference(expression.callee) && expression.arguments.length === 0;
|
|
39823
|
+
};
|
|
39824
|
+
const hasEmptyRefSentinelInitializer = (refCall, scopes) => {
|
|
39825
|
+
if (!isReactUseRefCall(refCall, scopes) || refCall.arguments.length > 1) return false;
|
|
39826
|
+
const [initialValue] = refCall.arguments;
|
|
39827
|
+
return !initialValue || isNodeOfType(initialValue, "Literal") && initialValue.value === null || isNodeOfType(initialValue, "Identifier") && initialValue.name === "undefined" && scopes.isGlobalReference(initialValue);
|
|
39828
|
+
};
|
|
39829
|
+
const isDirectLazyEmptyMapInitialization = (currentExpression, symbol, scopes) => {
|
|
39830
|
+
const assignment = currentExpression.parent;
|
|
39831
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== currentExpression || assignment.operator !== "??=" || !isEmptyGlobalMapConstruction(assignment.right, scopes)) return false;
|
|
39832
|
+
const refOwner = findEnclosingFunction$1(symbol.bindingIdentifier);
|
|
39833
|
+
return refOwner !== null && findEnclosingFunction$1(assignment) === refOwner;
|
|
39834
|
+
};
|
|
38968
39835
|
const storesOnlyIntrinsicRefCallbackValues = (symbol, scopes) => {
|
|
38969
|
-
const
|
|
38970
|
-
|
|
39836
|
+
const refCall = getDirectReactRefCall(symbol, scopes);
|
|
39837
|
+
const initialValue = refCall?.arguments[0];
|
|
39838
|
+
const hasDirectEmptyMapInitializer = Boolean(initialValue && isEmptyGlobalMapConstruction(initialValue, scopes));
|
|
39839
|
+
const hasLazyEmptyMapInitializer = Boolean(refCall && hasEmptyRefSentinelInitializer(refCall, scopes));
|
|
39840
|
+
if (!hasDirectEmptyMapInitializer && !hasLazyEmptyMapInitializer) return false;
|
|
38971
39841
|
let intrinsicValueWriteCount = 0;
|
|
39842
|
+
let lazyEmptyMapInitializationCount = 0;
|
|
38972
39843
|
for (const reference of symbol.references) {
|
|
38973
39844
|
const identifier = findTransparentExpressionRoot(reference.identifier);
|
|
38974
39845
|
const currentMember = identifier.parent;
|
|
38975
39846
|
if (!isNodeOfType(currentMember, "MemberExpression") || currentMember.object !== identifier || getStaticPropertyName(currentMember) !== "current") return false;
|
|
38976
39847
|
const currentExpression = findTransparentExpressionRoot(currentMember);
|
|
39848
|
+
if (hasLazyEmptyMapInitializer && isDirectLazyEmptyMapInitialization(currentExpression, symbol, scopes)) {
|
|
39849
|
+
lazyEmptyMapInitializationCount += 1;
|
|
39850
|
+
continue;
|
|
39851
|
+
}
|
|
38977
39852
|
const methodMember = currentExpression.parent;
|
|
38978
39853
|
if (!isNodeOfType(methodMember, "MemberExpression") || methodMember.object !== currentExpression) return false;
|
|
38979
39854
|
const methodName = getStaticPropertyName(methodMember);
|
|
@@ -38986,7 +39861,7 @@ const storesOnlyIntrinsicRefCallbackValues = (symbol, scopes) => {
|
|
|
38986
39861
|
if (!storedValue || isNodeOfType(storedValue, "SpreadElement") || !isIntrinsicRefCallbackParameter(storedValue, scopes)) return false;
|
|
38987
39862
|
intrinsicValueWriteCount += 1;
|
|
38988
39863
|
}
|
|
38989
|
-
return intrinsicValueWriteCount > 0;
|
|
39864
|
+
return intrinsicValueWriteCount > 0 && (hasDirectEmptyMapInitializer || lazyEmptyMapInitializationCount === 1);
|
|
38990
39865
|
};
|
|
38991
39866
|
const isDerivedFromProvenDomRefCurrent = (rawExpression, scopes, didReadCollectionValue = false, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
38992
39867
|
const expression = stripParenExpression(rawExpression);
|
|
@@ -39000,7 +39875,11 @@ const isDerivedFromProvenDomRefCurrent = (rawExpression, scopes, didReadCollecti
|
|
|
39000
39875
|
}
|
|
39001
39876
|
if (isNodeOfType(expression, "MemberExpression")) {
|
|
39002
39877
|
if (getStaticPropertyName(expression) === "current") {
|
|
39003
|
-
const symbol =
|
|
39878
|
+
const symbol = resolveReactRefSymbol(expression, scopes, {
|
|
39879
|
+
allowUnboundBareCalls: true,
|
|
39880
|
+
includeCreateRef: true,
|
|
39881
|
+
resolveNamedAliases: true
|
|
39882
|
+
});
|
|
39004
39883
|
return Boolean(symbol && (isDirectHostJsxRef(symbol, scopes) || didReadCollectionValue && storesOnlyIntrinsicRefCallbackValues(symbol, scopes)));
|
|
39005
39884
|
}
|
|
39006
39885
|
return isDerivedFromProvenDomRefCurrent(expression.object, scopes, didReadCollectionValue, visitedSymbolIds);
|
|
@@ -39018,19 +39897,80 @@ const isCommittedDomSyncNode = (node, scopes) => {
|
|
|
39018
39897
|
if (propertyName === null || !EXTERNAL_SYNC_DOM_MEMBER_METHOD_NAMES.has(propertyName)) return false;
|
|
39019
39898
|
return isDerivedFromProvenDomRefCurrent(callee.object, scopes) || isProvenBrowserApiReceiver(callee.object, "dom-event-target", scopes);
|
|
39020
39899
|
};
|
|
39021
|
-
const
|
|
39022
|
-
|
|
39023
|
-
if (isNodeOfType(
|
|
39900
|
+
const isProvenHttpClientReceiver = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
39901
|
+
const expression = stripParenExpression(rawExpression);
|
|
39902
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
39903
|
+
const receiverSymbol = scopes.symbolFor(expression);
|
|
39904
|
+
if (receiverSymbol && !hasSafeReceiverAliases(new Set([receiverSymbol]), scopes)) return false;
|
|
39905
|
+
}
|
|
39906
|
+
if (hasProvenGlobalNamespaceReference(expression, EXTERNAL_SYNC_GLOBAL_HTTP_CLIENT_NAMES, scopes)) return true;
|
|
39907
|
+
const moduleBinding = resolveExternalModuleBinding(expression, scopes);
|
|
39908
|
+
if (moduleBinding && EXTERNAL_SYNC_HTTP_CLIENT_MODULE_SOURCES.has(moduleBinding.source) && (moduleBinding.isModuleObject || moduleBinding.exportedName === "default" && EXTERNAL_SYNC_DEFAULT_IMPORT_NAMES.has(moduleBinding.source) || isKnownExternalSyncDirectModuleBinding(moduleBinding))) return true;
|
|
39909
|
+
if (isNodeOfType(expression, "CallExpression")) {
|
|
39910
|
+
const callee = stripParenExpression(expression.callee);
|
|
39911
|
+
return Boolean(isNodeOfType(callee, "MemberExpression") && getStaticPropertyName(callee) === "create" && isProvenHttpClientReceiver(callee.object, scopes, visitedSymbolIds));
|
|
39912
|
+
}
|
|
39913
|
+
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
39914
|
+
const symbol = scopes.symbolFor(expression);
|
|
39915
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
39916
|
+
if (resolveExternalModuleBinding(expression, scopes, /* @__PURE__ */ new Set(), true)) return false;
|
|
39917
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
39918
|
+
return Boolean(initializer && isProvenHttpClientReceiver(initializer, scopes, new Set(visitedSymbolIds).add(symbol.id)));
|
|
39919
|
+
};
|
|
39920
|
+
const isProvenTanStackQueryClientReceiver = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
39921
|
+
const expression = stripParenExpression(rawExpression);
|
|
39922
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
39923
|
+
const receiverSymbol = scopes.symbolFor(expression);
|
|
39924
|
+
if (receiverSymbol && !hasSafeReceiverAliases(new Set([receiverSymbol]), scopes)) return false;
|
|
39925
|
+
}
|
|
39926
|
+
if (isNodeOfType(expression, "CallExpression")) {
|
|
39927
|
+
const binding = resolveExternalModuleBinding(expression.callee, scopes);
|
|
39928
|
+
return Boolean(binding && binding.source === "@tanstack/react-query" && binding.exportedName === "useQueryClient");
|
|
39929
|
+
}
|
|
39930
|
+
if (isNodeOfType(expression, "NewExpression")) {
|
|
39931
|
+
const binding = resolveExternalModuleBinding(expression.callee, scopes);
|
|
39932
|
+
return Boolean(binding && TANSTACK_QUERY_CLIENT_MODULE_SOURCES.has(binding.source) && binding.exportedName === "QueryClient");
|
|
39933
|
+
}
|
|
39934
|
+
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
39935
|
+
const symbol = scopes.symbolFor(expression);
|
|
39936
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
39937
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
39938
|
+
return Boolean(initializer && isProvenTanStackQueryClientReceiver(initializer, scopes, new Set(visitedSymbolIds).add(symbol.id)));
|
|
39939
|
+
};
|
|
39940
|
+
const isProvenExternalResourceReceiver = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
39941
|
+
const expression = stripParenExpression(rawExpression);
|
|
39942
|
+
if (isDerivedFromProvenDomRefCurrent(expression, scopes) || isProvenBrowserApiReceiver(expression, "dom-event-target", scopes) || isProvenBrowserApiReceiver(expression, "xml-http-request", scopes) || isProvenHttpClientReceiver(expression, scopes)) return true;
|
|
39943
|
+
if (isNodeOfType(expression, "NewExpression")) return hasProvenGlobalNamespaceReference(expression.callee, EXTERNAL_SYNC_RESOURCE_CONSTRUCTOR_NAMES, scopes);
|
|
39944
|
+
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
39945
|
+
const symbol = scopes.symbolFor(expression);
|
|
39946
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
39947
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
39948
|
+
return Boolean(initializer && isProvenExternalResourceReceiver(initializer, scopes, new Set(visitedSymbolIds).add(symbol.id)));
|
|
39949
|
+
};
|
|
39950
|
+
const isProvenExternalSyncDirectCallee = (callee, scopes) => {
|
|
39951
|
+
if (hasProvenGlobalNamespaceReference(callee, EXTERNAL_SYNC_DIRECT_CALLEE_NAMES, scopes)) return true;
|
|
39952
|
+
const moduleBinding = resolveExternalModuleBinding(callee, scopes);
|
|
39953
|
+
return Boolean(moduleBinding && isKnownExternalSyncDirectModuleBinding(moduleBinding));
|
|
39954
|
+
};
|
|
39955
|
+
const isExternalSyncNode = (node, scopes) => {
|
|
39956
|
+
if (isNodeOfType(node, "NewExpression")) return hasProvenGlobalNamespaceReference(node.callee, EXTERNAL_SYNC_OBSERVER_CONSTRUCTORS, scopes);
|
|
39957
|
+
if (isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "UpdateExpression")) {
|
|
39958
|
+
const mutationTarget = isNodeOfType(node, "AssignmentExpression") ? node.left : node.argument;
|
|
39959
|
+
return isNodeOfType(mutationTarget, "MemberExpression") && getStaticPropertyName(mutationTarget) === "current" && Boolean(resolveReactRefSymbol(mutationTarget, scopes, {
|
|
39960
|
+
allowUnboundBareCalls: true,
|
|
39961
|
+
includeCreateRef: true,
|
|
39962
|
+
resolveNamedAliases: true
|
|
39963
|
+
}));
|
|
39964
|
+
}
|
|
39024
39965
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
39025
|
-
if (
|
|
39966
|
+
if (isProvenExternalSyncDirectCallee(node.callee, scopes)) return true;
|
|
39026
39967
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
39027
39968
|
const propertyName = getStaticPropertyName(node.callee);
|
|
39028
39969
|
if (propertyName === null) return false;
|
|
39029
|
-
if (
|
|
39030
|
-
if (
|
|
39031
|
-
if (
|
|
39032
|
-
|
|
39033
|
-
return receiverRootName !== null && EXTERNAL_SYNC_HTTP_CLIENT_RECEIVERS.has(receiverRootName);
|
|
39970
|
+
if (isBrowserStorageReceiver(node.callee.object, scopes)) return true;
|
|
39971
|
+
if (TANSTACK_QUERY_EXTERNAL_SYNC_METHOD_NAMES.has(propertyName)) return isProvenTanStackQueryClientReceiver(node.callee.object, scopes);
|
|
39972
|
+
if (EXTERNAL_SYNC_HTTP_METHOD_NAMES.has(propertyName)) return isProvenHttpClientReceiver(node.callee.object, scopes);
|
|
39973
|
+
return EXTERNAL_SYNC_MEMBER_METHOD_NAMES.has(propertyName) && isProvenExternalResourceReceiver(node.callee.object, scopes);
|
|
39034
39974
|
};
|
|
39035
39975
|
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, setterSymbolIdToStateName, scopes, allowCommittedDomSync) => {
|
|
39036
39976
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
@@ -39039,7 +39979,7 @@ const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateNa
|
|
|
39039
39979
|
} else for (const statement of effectCallback.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument && isFunctionShapedReturn(statement.argument, setterToStateName, setterSymbolIdToStateName, scopes, true)) return true;
|
|
39040
39980
|
let didFindExternalCall = false;
|
|
39041
39981
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
39042
|
-
if (isExternalSyncNode(child) || allowCommittedDomSync && isCommittedDomSyncNode(child, scopes)) didFindExternalCall = true;
|
|
39982
|
+
if (isExternalSyncNode(child, scopes) || allowCommittedDomSync && isCommittedDomSyncNode(child, scopes)) didFindExternalCall = true;
|
|
39043
39983
|
});
|
|
39044
39984
|
return didFindExternalCall;
|
|
39045
39985
|
};
|
|
@@ -39061,11 +40001,11 @@ const noEffectChain = defineRule({
|
|
|
39061
40001
|
setterToStateName.set(binding.setterName, binding.valueName);
|
|
39062
40002
|
if (!isNodeOfType(binding.declarator.id, "ArrayPattern")) continue;
|
|
39063
40003
|
const stateIdentifier = binding.declarator.id.elements[0];
|
|
40004
|
+
const setterIdentifier = binding.declarator.id.elements[1];
|
|
39064
40005
|
if (isNodeOfType(stateIdentifier, "Identifier")) {
|
|
39065
40006
|
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
39066
40007
|
if (stateSymbol) stateSymbolIds.set(binding.valueName, stateSymbol.id);
|
|
39067
40008
|
}
|
|
39068
|
-
const setterIdentifier = binding.declarator.id.elements[1];
|
|
39069
40009
|
if (isNodeOfType(setterIdentifier, "Identifier")) {
|
|
39070
40010
|
const setterSymbol = context.scopes.symbolFor(setterIdentifier);
|
|
39071
40011
|
if (setterSymbol) setterSymbolIdToStateName.set(setterSymbol.id, binding.valueName);
|
|
@@ -39078,10 +40018,11 @@ const noEffectChain = defineRule({
|
|
|
39078
40018
|
const callback = getEffectCallback(effectCall, context.scopes);
|
|
39079
40019
|
if (!callback || !isFunctionLike$1(callback) || callback.async) continue;
|
|
39080
40020
|
const analysisFunctions = collectSynchronouslyInvokedFunctions(callback, context.scopes);
|
|
39081
|
-
const stateWrites = collectStateWritesInEffect(
|
|
40021
|
+
const stateWrites = collectStateWritesInEffect(collectStateWriteAnalysisFunctions(callback, context.scopes, setterSymbolIdToStateName), setterSymbolIdToStateName, context.scopes);
|
|
39082
40022
|
const writtenStateNames = new Set(stateWrites.keys());
|
|
39083
40023
|
effectInfos.push({
|
|
39084
40024
|
node: effectCall,
|
|
40025
|
+
callback,
|
|
39085
40026
|
dependencyStateSymbolIds: collectDependencyStateSymbolIds(effectCall, stateSymbolIdSet, context.scopes),
|
|
39086
40027
|
stateWrites,
|
|
39087
40028
|
analysisFunctions,
|
|
@@ -39098,10 +40039,10 @@ const noEffectChain = defineRule({
|
|
|
39098
40039
|
if (readerEffect.isExternalSync) continue;
|
|
39099
40040
|
if (readerEffect.dependencyStateSymbolIds.size === 0) continue;
|
|
39100
40041
|
let chainedStateName = null;
|
|
39101
|
-
for (const
|
|
40042
|
+
for (const writtenName of writerEffect.stateWrites.keys()) {
|
|
39102
40043
|
const writtenStateSymbolId = stateSymbolIds.get(writtenName);
|
|
39103
40044
|
if (writtenStateSymbolId === void 0 || !readerEffect.dependencyStateSymbolIds.has(writtenStateSymbolId)) continue;
|
|
39104
|
-
if (!canStateWriteReachReaderWork(
|
|
40045
|
+
if (!canStateWriteReachReaderWork(writtenName, writerEffect, readerEffect, stateSymbolIds, context.scopes)) continue;
|
|
39105
40046
|
chainedStateName = writtenName;
|
|
39106
40047
|
break;
|
|
39107
40048
|
}
|
|
@@ -39490,7 +40431,7 @@ const noEffectEventInDeps = defineRule({
|
|
|
39490
40431
|
title: "Effect Event listed in effect deps",
|
|
39491
40432
|
tags: ["test-noise"],
|
|
39492
40433
|
severity: "error",
|
|
39493
|
-
recommendation: "Call the useEffectEvent function inside the effect body and don't list it in the deps. It
|
|
40434
|
+
recommendation: "Call the useEffectEvent function inside the effect body and don't list it in the deps. It's non-reactive on purpose, so it must be omitted.",
|
|
39494
40435
|
create: (context) => {
|
|
39495
40436
|
const componentBindings = createComponentBindingStackTracker({ onVariableDeclarator: (declaratorNode) => {
|
|
39496
40437
|
if (!isNodeOfType(declaratorNode, "VariableDeclarator")) return;
|
|
@@ -39512,7 +40453,7 @@ const noEffectEventInDeps = defineRule({
|
|
|
39512
40453
|
if (!isNodeOfType(element, "Identifier")) continue;
|
|
39513
40454
|
if (componentBindings.isBoundName(element.name)) context.report({
|
|
39514
40455
|
node: element,
|
|
39515
|
-
message: `Listing "${element.name}" in the deps
|
|
40456
|
+
message: `Listing "${element.name}" in the deps defeats useEffectEvent — Effect Events are non-reactive and must be omitted from deps.`
|
|
39516
40457
|
});
|
|
39517
40458
|
}
|
|
39518
40459
|
}
|
|
@@ -39555,6 +40496,7 @@ const noEffectWithFreshDeps = defineRule({
|
|
|
39555
40496
|
title: "Effect dependency recreated every render",
|
|
39556
40497
|
severity: "error",
|
|
39557
40498
|
category: "State & Effects",
|
|
40499
|
+
disabledWhen: ["react-compiler"],
|
|
39558
40500
|
recommendation: "Move the value inside the hook body and depend on its simple inputs instead, or wrap it in useMemo / useCallback so it stays the same between renders.",
|
|
39559
40501
|
create: (context) => ({ CallExpression(node) {
|
|
39560
40502
|
for (const finding of findForwardedFreshHookDependencies(node, context, EFFECT_HOOK_NAMES$1)) context.report({
|
|
@@ -42495,14 +43437,13 @@ const noInlineExhaustiveStyle = defineRule({
|
|
|
42495
43437
|
});
|
|
42496
43438
|
//#endregion
|
|
42497
43439
|
//#region src/plugin/rules/performance/no-inline-prop-on-memo-component.ts
|
|
42498
|
-
const
|
|
43440
|
+
const MEMO_CALLEE_NAMES = new Set(["memo", "React.memo"]);
|
|
43441
|
+
const isMemoCall = (node) => isNodeOfType(node, "CallExpression") && MEMO_CALLEE_NAMES.has(flattenCalleeName(node.callee) ?? "");
|
|
43442
|
+
const hasCustomComparator = (node, scopes) => {
|
|
42499
43443
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
42500
|
-
|
|
42501
|
-
|
|
42502
|
-
return false;
|
|
43444
|
+
const comparator = node.arguments?.[1];
|
|
43445
|
+
return comparator ? !isIdentitySensitiveMemoComparator(comparator, scopes) : false;
|
|
42503
43446
|
};
|
|
42504
|
-
const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
|
|
42505
|
-
const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
|
|
42506
43447
|
const isInlineReference = (node, scopes) => {
|
|
42507
43448
|
const referenceNode = unwrapObjectIntegrityExpression(node, scopes);
|
|
42508
43449
|
if (isNodeOfType(referenceNode, "ArrowFunctionExpression") || isNodeOfType(referenceNode, "FunctionExpression") || isNodeOfType(referenceNode, "CallExpression") && isNodeOfType(referenceNode.callee, "MemberExpression") && isNodeOfType(referenceNode.callee.property, "Identifier") && referenceNode.callee.property.name === "bind") return "functions";
|
|
@@ -42516,16 +43457,17 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
42516
43457
|
title: "Inline prop defeats memo()",
|
|
42517
43458
|
tags: ["test-noise"],
|
|
42518
43459
|
severity: "warn",
|
|
43460
|
+
disabledWhen: ["react-compiler"],
|
|
42519
43461
|
recommendation: "Move the inline `() => ...` / `[]` / `{}` to a stable value with useMemo, useCallback, or module scope, so the memoized child stops redrawing on every parent render",
|
|
42520
43462
|
create: (context) => {
|
|
42521
43463
|
const memoizedComponentNames = /* @__PURE__ */ new Set();
|
|
42522
43464
|
return {
|
|
42523
43465
|
VariableDeclarator(node) {
|
|
42524
43466
|
if (!isNodeOfType(node.id, "Identifier") || !node.init) return;
|
|
42525
|
-
if (isMemoCall(node.init) && !hasCustomComparator(node.init)) memoizedComponentNames.add(node.id.name);
|
|
43467
|
+
if (isMemoCall(node.init) && !hasCustomComparator(node.init, context.scopes)) memoizedComponentNames.add(node.id.name);
|
|
42526
43468
|
},
|
|
42527
43469
|
ExportDefaultDeclaration(node) {
|
|
42528
|
-
if (node.declaration && isNodeOfType(node.declaration, "CallExpression") && isMemoCall(node.declaration) && !hasCustomComparator(node.declaration)) {
|
|
43470
|
+
if (node.declaration && isNodeOfType(node.declaration, "CallExpression") && isMemoCall(node.declaration) && !hasCustomComparator(node.declaration, context.scopes)) {
|
|
42529
43471
|
const innerArgument = node.declaration.arguments?.[0];
|
|
42530
43472
|
if (isNodeOfType(innerArgument, "Identifier")) memoizedComponentNames.add(innerArgument.name);
|
|
42531
43473
|
}
|
|
@@ -43885,16 +44827,6 @@ const noManyBooleanProps = defineRule({
|
|
|
43885
44827
|
}
|
|
43886
44828
|
});
|
|
43887
44829
|
//#endregion
|
|
43888
|
-
//#region src/plugin/utils/is-global-match-media-call.ts
|
|
43889
|
-
const isGlobalMatchMediaCall = (node, scopes) => {
|
|
43890
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
43891
|
-
const callee = stripParenExpression(node.callee);
|
|
43892
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name === "matchMedia" && scopes.isGlobalReference(callee);
|
|
43893
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "matchMedia") return false;
|
|
43894
|
-
const receiver = stripParenExpression(callee.object);
|
|
43895
|
-
return isNodeOfType(receiver, "Identifier") && (receiver.name === "window" || receiver.name === "globalThis") && scopes.isGlobalReference(receiver);
|
|
43896
|
-
};
|
|
43897
|
-
//#endregion
|
|
43898
44830
|
//#region src/plugin/rules/performance/no-match-media-in-state-initializer.ts
|
|
43899
44831
|
const REACT_USE_STATE_OPTIONS = {
|
|
43900
44832
|
allowGlobalReactNamespace: true,
|
|
@@ -46284,6 +47216,103 @@ const EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1 = new Set([
|
|
|
46284
47216
|
"useVisibility",
|
|
46285
47217
|
"useWindowSize"
|
|
46286
47218
|
]);
|
|
47219
|
+
const EXTERNAL_SUBSCRIPTION_PRIMITIVE_RESULT_HOOK_NAMES = new Set([
|
|
47220
|
+
"useMatchMedia",
|
|
47221
|
+
"useMediaQuery",
|
|
47222
|
+
"useVisibility"
|
|
47223
|
+
]);
|
|
47224
|
+
const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
|
|
47225
|
+
const getImportedExternalSubscriptionHookName = (analysis, rawCallee) => {
|
|
47226
|
+
const callee = stripParenExpression(rawCallee);
|
|
47227
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
47228
|
+
const calleeRef = getRef(analysis, callee);
|
|
47229
|
+
if (!calleeRef || !isImportBindingRef(calleeRef)) return null;
|
|
47230
|
+
const importBinding = getImportBindingForName(callee, callee.name);
|
|
47231
|
+
if (!importBinding || importBinding.isNamespace) return null;
|
|
47232
|
+
if (importBinding.exportedName && EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(importBinding.exportedName)) return importBinding.exportedName;
|
|
47233
|
+
return importBinding.exportedName === "default" && EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(callee.name) ? callee.name : null;
|
|
47234
|
+
}
|
|
47235
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
47236
|
+
const hookName = getStaticMemberPropertyName(callee);
|
|
47237
|
+
const namespaceIdentifier = stripParenExpression(callee.object);
|
|
47238
|
+
if (!hookName || !EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(hookName) || !isNodeOfType(namespaceIdentifier, "Identifier")) return null;
|
|
47239
|
+
const namespaceRef = getRef(analysis, namespaceIdentifier);
|
|
47240
|
+
if (!namespaceRef || !isImportBindingRef(namespaceRef)) return null;
|
|
47241
|
+
return getImportBindingForName(namespaceIdentifier, namespaceIdentifier.name)?.isNamespace ? hookName : null;
|
|
47242
|
+
};
|
|
47243
|
+
const isSafeExternalSubscriptionResultBinding = (bindingIdentifier, bindingPattern) => {
|
|
47244
|
+
let current = bindingIdentifier;
|
|
47245
|
+
let didCrossDestructuringBoundary = false;
|
|
47246
|
+
while (current !== bindingPattern) {
|
|
47247
|
+
const parent = current.parent;
|
|
47248
|
+
if (!parent || isNodeOfType(parent, "AssignmentPattern") || isNodeOfType(parent, "RestElement")) return false;
|
|
47249
|
+
if (isNodeOfType(parent, "Property")) {
|
|
47250
|
+
if (parent.value !== current) return false;
|
|
47251
|
+
didCrossDestructuringBoundary = true;
|
|
47252
|
+
}
|
|
47253
|
+
if (isNodeOfType(parent, "ArrayPattern")) didCrossDestructuringBoundary = true;
|
|
47254
|
+
current = parent;
|
|
47255
|
+
}
|
|
47256
|
+
return didCrossDestructuringBoundary;
|
|
47257
|
+
};
|
|
47258
|
+
const getVariablesDefinedByDeclarator = (analysis, declarator) => analysis.scopeManager.scopes.flatMap((scope) => scope.variables).filter((variable) => variable.defs.some((definition) => definition.node === declarator));
|
|
47259
|
+
const hasUnsafeExternalSubscriptionBindingUse = (analysis, variable, visitedVariables = /* @__PURE__ */ new Set()) => {
|
|
47260
|
+
if (visitedVariables.has(variable)) return false;
|
|
47261
|
+
visitedVariables.add(variable);
|
|
47262
|
+
return variable.references.some((candidateReference) => {
|
|
47263
|
+
if (candidateReference.init) return false;
|
|
47264
|
+
if (candidateReference.isWrite()) return true;
|
|
47265
|
+
let usageRoot = findTransparentExpressionRoot(candidateReference.identifier);
|
|
47266
|
+
while (isNodeOfType(usageRoot.parent, "MemberExpression") && usageRoot.parent.object === usageRoot) usageRoot = findTransparentExpressionRoot(usageRoot.parent);
|
|
47267
|
+
const usageParent = usageRoot.parent;
|
|
47268
|
+
if (isNodeOfType(usageParent, "AssignmentExpression") && usageParent.left === usageRoot || isNodeOfType(usageParent, "UpdateExpression") && usageParent.argument === usageRoot || isNodeOfType(usageParent, "UnaryExpression") && usageParent.operator === "delete" && usageParent.argument === usageRoot) return true;
|
|
47269
|
+
let usageAncestor = usageRoot.parent;
|
|
47270
|
+
while (usageAncestor && !isNodeOfType(usageAncestor, "VariableDeclarator") && !isFunctionLike$1(usageAncestor) && !isNodeOfType(usageAncestor, "Program")) usageAncestor = usageAncestor.parent;
|
|
47271
|
+
if (isNodeOfType(usageAncestor, "VariableDeclarator") && usageAncestor.init) {
|
|
47272
|
+
const aliasVariables = getVariablesDefinedByDeclarator(analysis, usageAncestor);
|
|
47273
|
+
return aliasVariables.length === 0 || aliasVariables.some((aliasVariable) => hasUnsafeExternalSubscriptionBindingUse(analysis, aliasVariable, visitedVariables));
|
|
47274
|
+
}
|
|
47275
|
+
return false;
|
|
47276
|
+
});
|
|
47277
|
+
};
|
|
47278
|
+
const hasOnlySafeExternalSubscriptionResultBindings = (analysis, declarator, variables = getVariablesDefinedByDeclarator(analysis, declarator)) => {
|
|
47279
|
+
return variables.length > 0 && variables.every((variable) => {
|
|
47280
|
+
const bindingIdentifier = variable.defs.find((candidateDefinition) => candidateDefinition.node === declarator)?.name;
|
|
47281
|
+
return Boolean(bindingIdentifier && isNodeOfType(bindingIdentifier, "Identifier") && isSafeExternalSubscriptionResultBinding(bindingIdentifier, declarator.id) && !hasUnsafeExternalSubscriptionBindingUse(analysis, variable));
|
|
47282
|
+
});
|
|
47283
|
+
};
|
|
47284
|
+
const hasImmutableExternalSubscriptionCallResult = (analysis, rawCallee, relatedRefs, allowWholeResult) => {
|
|
47285
|
+
const callee = stripParenExpression(rawCallee);
|
|
47286
|
+
const callExpression = callee.parent;
|
|
47287
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression") || callExpression.callee !== callee) return false;
|
|
47288
|
+
const initializer = findTransparentExpressionRoot(callExpression);
|
|
47289
|
+
const declarator = initializer.parent;
|
|
47290
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== initializer || !isNodeOfType(declarator.parent, "VariableDeclaration") || declarator.parent.kind !== "const") return false;
|
|
47291
|
+
const relatedVariables = getVariablesDefinedByDeclarator(analysis, declarator).filter((variable) => relatedRefs.some((relatedRef) => relatedRef.resolved === variable));
|
|
47292
|
+
if (relatedVariables.length === 0) return false;
|
|
47293
|
+
return isNodeOfType(declarator.id, "Identifier") ? allowWholeResult && relatedVariables.every((variable) => !hasUnsafeExternalSubscriptionBindingUse(analysis, variable)) : hasOnlySafeExternalSubscriptionResultBindings(analysis, declarator, relatedVariables);
|
|
47294
|
+
};
|
|
47295
|
+
const isImmutableImportedExternalSubscriptionHookCallee = (analysis, rawCallee, relatedRefs) => {
|
|
47296
|
+
const hookName = getImportedExternalSubscriptionHookName(analysis, rawCallee);
|
|
47297
|
+
return Boolean(hookName && hasImmutableExternalSubscriptionCallResult(analysis, rawCallee, relatedRefs, EXTERNAL_SUBSCRIPTION_PRIMITIVE_RESULT_HOOK_NAMES.has(hookName)));
|
|
47298
|
+
};
|
|
47299
|
+
const isExternalSubscriptionHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
47300
|
+
const declarator = def.node;
|
|
47301
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !declarator.init || !isNodeOfType(declarator.parent, "VariableDeclaration") || declarator.parent.kind !== "const") return false;
|
|
47302
|
+
const initializer = stripParenExpression(declarator.init);
|
|
47303
|
+
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
47304
|
+
const hookName = getImportedExternalSubscriptionHookName(analysis, initializer.callee);
|
|
47305
|
+
if (!hookName) return false;
|
|
47306
|
+
if (isNodeOfType(declarator.id, "Identifier")) return EXTERNAL_SUBSCRIPTION_PRIMITIVE_RESULT_HOOK_NAMES.has(hookName) && ref.resolved && !hasUnsafeExternalSubscriptionBindingUse(analysis, ref.resolved);
|
|
47307
|
+
const bindingIdentifier = def.name;
|
|
47308
|
+
return isNodeOfType(bindingIdentifier, "Identifier") && isSafeExternalSubscriptionResultBinding(bindingIdentifier, declarator.id) && ref.resolved && !hasUnsafeExternalSubscriptionBindingUse(analysis, ref.resolved);
|
|
47309
|
+
}));
|
|
47310
|
+
const isExternalSubscriptionHookResultArgument = (analysis, argument) => {
|
|
47311
|
+
const unwrappedArgument = stripParenExpression(argument);
|
|
47312
|
+
if (!isNodeOfType(unwrappedArgument, "Identifier")) return false;
|
|
47313
|
+
const argumentRef = getRef(analysis, unwrappedArgument);
|
|
47314
|
+
return Boolean(argumentRef && isExternalSubscriptionHookResultRef(analysis, argumentRef));
|
|
47315
|
+
};
|
|
46287
47316
|
const isCallbackPropReference = (analysis, ref) => {
|
|
46288
47317
|
if (!isProp(analysis, ref)) return false;
|
|
46289
47318
|
const identifier = ref.identifier;
|
|
@@ -46316,8 +47345,9 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
|
|
|
46316
47345
|
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
|
|
46317
47346
|
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
46318
47347
|
};
|
|
46319
|
-
const getLocalHookExternalStateProof = (analysis, ref) => {
|
|
47348
|
+
const getLocalHookExternalStateProof = (analysis, ref, scopes) => {
|
|
46320
47349
|
let hookFunction = resolveToFunction(ref);
|
|
47350
|
+
let didResolveThroughResultBinding = false;
|
|
46321
47351
|
if (!hookFunction) for (const definition of ref.resolved?.defs ?? []) {
|
|
46322
47352
|
const definitionNode = definition.node;
|
|
46323
47353
|
if (!isNodeOfType(definitionNode, "VariableDeclarator") || !definitionNode.init) continue;
|
|
@@ -46328,29 +47358,27 @@ const getLocalHookExternalStateProof = (analysis, ref) => {
|
|
|
46328
47358
|
const calleeReference = getRef(analysis, callee);
|
|
46329
47359
|
if (!calleeReference) continue;
|
|
46330
47360
|
hookFunction = resolveToFunction(calleeReference);
|
|
46331
|
-
if (hookFunction)
|
|
47361
|
+
if (hookFunction) {
|
|
47362
|
+
didResolveThroughResultBinding = true;
|
|
47363
|
+
break;
|
|
47364
|
+
}
|
|
46332
47365
|
}
|
|
46333
47366
|
if (!hookFunction) return null;
|
|
46334
|
-
|
|
47367
|
+
if (didResolveThroughResultBinding && (!ref.resolved || hasUnsafeExternalSubscriptionBindingUse(analysis, ref.resolved))) return false;
|
|
47368
|
+
if (isNodeOfType(hookFunction, "ArrowFunctionExpression") && !isNodeOfType(hookFunction.body, "BlockStatement") && isReactHookCall(stripParenExpression(hookFunction.body), "useSyncExternalStore", scopes)) return true;
|
|
47369
|
+
const returnStatements = collectFunctionReturnStatements(hookFunction);
|
|
47370
|
+
if (returnStatements.length > 0 && returnStatements.every((returnStatement) => returnStatement.argument && isReactHookCall(stripParenExpression(returnStatement.argument), "useSyncExternalStore", scopes))) return true;
|
|
47371
|
+
const returnedReferences = returnStatements.flatMap((returnStatement) => returnStatement.argument ? getDownstreamRefs(analysis, returnStatement.argument) : []);
|
|
46335
47372
|
if (returnedReferences.length === 0) return null;
|
|
46336
47373
|
return returnedReferences.every((returnedReference) => isState(analysis, returnedReference) && isExternallyDrivenState(analysis, returnedReference));
|
|
46337
47374
|
};
|
|
46338
|
-
const
|
|
46339
|
-
const
|
|
46340
|
-
if (!isNodeOfType(
|
|
46341
|
-
const
|
|
46342
|
-
if (
|
|
46343
|
-
|
|
46344
|
-
return Boolean(ref.resolved?.defs.some((def) => {
|
|
46345
|
-
const node = def.node;
|
|
46346
|
-
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
46347
|
-
const initializer = stripParenExpression(node.init);
|
|
46348
|
-
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
46349
|
-
const callee = stripParenExpression(initializer.callee);
|
|
46350
|
-
return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(callee.name);
|
|
46351
|
-
}));
|
|
47375
|
+
const isImmutableLocalExternalStoreHookCallee = (analysis, rawCallee, relatedRefs, scopes) => {
|
|
47376
|
+
const callee = stripParenExpression(rawCallee);
|
|
47377
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
47378
|
+
const calleeReference = getRef(analysis, callee);
|
|
47379
|
+
if (!calleeReference || getLocalHookExternalStateProof(analysis, calleeReference, scopes) !== true) return false;
|
|
47380
|
+
return hasImmutableExternalSubscriptionCallResult(analysis, callee, relatedRefs, true);
|
|
46352
47381
|
};
|
|
46353
|
-
const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
|
|
46354
47382
|
const isCalleePosition = (identifier) => {
|
|
46355
47383
|
const parent = identifier.parent;
|
|
46356
47384
|
return Boolean(parent && (isNodeOfType(parent, "CallExpression") || isNodeOfType(parent, "NewExpression")) && parent.callee === identifier);
|
|
@@ -46411,25 +47439,35 @@ const noPassDataToParent = defineRule({
|
|
|
46411
47439
|
return getFunctionalUpdaterDataRefs(analysis, argument);
|
|
46412
47440
|
}
|
|
46413
47441
|
if (isHandlerBagArgument(analysis, argument)) return [];
|
|
47442
|
+
if (isExternalSubscriptionHookResultArgument(analysis, argument)) return [];
|
|
46414
47443
|
if (isParentWiredHookResultArgument(analysis, argument)) return [];
|
|
46415
47444
|
if (isNodeOfType(argument, "Identifier")) {
|
|
46416
47445
|
const argumentRef = getRef(analysis, argument);
|
|
46417
47446
|
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
46418
47447
|
}
|
|
46419
47448
|
return getDownstreamRefs(analysis, argument);
|
|
46420
|
-
}).flatMap((argumentRef) =>
|
|
46421
|
-
|
|
47449
|
+
}).flatMap((argumentRef) => {
|
|
47450
|
+
if (isExternallyDrivenState(analysis, argumentRef) || getLocalHookExternalStateProof(analysis, argumentRef, context.scopes) === true) return [];
|
|
47451
|
+
const upstreamRefs = getUpstreamRefs(analysis, argumentRef);
|
|
47452
|
+
return upstreamRefs.filter((upstreamRef) => {
|
|
47453
|
+
if (!isLeafRef(upstreamRef)) return false;
|
|
47454
|
+
return !isImmutableImportedExternalSubscriptionHookCallee(analysis, upstreamRef.identifier, upstreamRefs) && !isImmutableLocalExternalStoreHookCallee(analysis, upstreamRef.identifier, upstreamRefs, context.scopes);
|
|
47455
|
+
});
|
|
47456
|
+
});
|
|
47457
|
+
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) {
|
|
47458
|
+
const wrapperUpstreamRefs = getArgsUpstreamRefs(analysis, ref);
|
|
47459
|
+
argsUpstreamRefs.push(...wrapperUpstreamRefs.filter((upstreamRef) => isLeafRef(upstreamRef) && !isImmutableImportedExternalSubscriptionHookCallee(analysis, upstreamRef.identifier, wrapperUpstreamRefs) && !isImmutableLocalExternalStoreHookCallee(analysis, upstreamRef.identifier, wrapperUpstreamRefs, context.scopes)));
|
|
47460
|
+
}
|
|
46422
47461
|
if (!argsUpstreamRefs.some((argRef) => {
|
|
46423
|
-
|
|
46424
|
-
if (
|
|
47462
|
+
const argIdentifier = argRef.identifier;
|
|
47463
|
+
if (isUseStateIdentifier(argIdentifier)) return false;
|
|
46425
47464
|
if (isProp(analysis, argRef)) return false;
|
|
46426
|
-
if (isUseRefIdentifier(
|
|
47465
|
+
if (isUseRefIdentifier(argIdentifier)) return false;
|
|
46427
47466
|
if (isRefCurrent(argRef)) return false;
|
|
46428
47467
|
if (isConstant(argRef)) return false;
|
|
46429
47468
|
if (isParentWiredHookResultRef(analysis, argRef)) return false;
|
|
46430
47469
|
if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
|
|
46431
47470
|
if (resolveToFunction(argRef)) return false;
|
|
46432
|
-
const argIdentifier = argRef.identifier;
|
|
46433
47471
|
if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
|
|
46434
47472
|
if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
|
|
46435
47473
|
return true;
|
|
@@ -55380,6 +56418,7 @@ const preferModuleScopePureFunction = defineRule({
|
|
|
55380
56418
|
tags: ["test-noise"],
|
|
55381
56419
|
severity: "warn",
|
|
55382
56420
|
category: "Architecture",
|
|
56421
|
+
disabledWhen: ["react-compiler"],
|
|
55383
56422
|
recommendation: "Move the function above the component, at the top of the file. It doesn't use local state, so rebuilding it each update is wasted work.",
|
|
55384
56423
|
create: (context) => {
|
|
55385
56424
|
const report = (functionNode, name, componentName) => {
|
|
@@ -55419,17 +56458,6 @@ const preferModuleScopePureFunction = defineRule({
|
|
|
55419
56458
|
}
|
|
55420
56459
|
});
|
|
55421
56460
|
//#endregion
|
|
55422
|
-
//#region src/plugin/utils/get-require-call-source.ts
|
|
55423
|
-
const getRequireCallSource = (expression) => {
|
|
55424
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
55425
|
-
if (isNodeOfType(unwrappedExpression, "MemberExpression")) return getRequireCallSource(unwrappedExpression.object);
|
|
55426
|
-
if (!isNodeOfType(unwrappedExpression, "CallExpression")) return null;
|
|
55427
|
-
if (!isNodeOfType(unwrappedExpression.callee, "Identifier") || unwrappedExpression.callee.name !== "require") return null;
|
|
55428
|
-
const [firstArgument] = unwrappedExpression.arguments ?? [];
|
|
55429
|
-
if (!firstArgument || !isNodeOfType(firstArgument, "Literal")) return null;
|
|
55430
|
-
return typeof firstArgument.value === "string" ? firstArgument.value : null;
|
|
55431
|
-
};
|
|
55432
|
-
//#endregion
|
|
55433
56461
|
//#region src/plugin/utils/is-proven-node-crypto-namespace-reference.ts
|
|
55434
56462
|
const NODE_CRYPTO_MODULE_SOURCES = new Set(["crypto", "node:crypto"]);
|
|
55435
56463
|
const isProvenNodeCryptoNamespaceReference = (expression, scopes) => {
|
|
@@ -56021,10 +57049,10 @@ const classifyCallableReadsInsideEffect = (callableIdentifier, effectCallback, c
|
|
|
56021
57049
|
const preferUseEffectEvent = defineRule({
|
|
56022
57050
|
id: "prefer-use-effect-event",
|
|
56023
57051
|
title: "Effect re-subscribes on a changing callback",
|
|
56024
|
-
requires: ["react:19"],
|
|
57052
|
+
requires: ["react:19.2"],
|
|
56025
57053
|
tags: ["test-noise"],
|
|
56026
57054
|
severity: "warn",
|
|
56027
|
-
recommendation: "Wrap the callback with `useEffectEvent(callback)` (React 19+) and call it inside the sub-handler. An Effect Event always sees the latest props and state but isn't a dependency, so the effect won't re-subscribe every time the parent redraws. See https://react.dev/reference/react/useEffectEvent",
|
|
57055
|
+
recommendation: "Wrap the callback with `useEffectEvent(callback)` (React 19.2+) and call it inside the sub-handler. An Effect Event always sees the latest props and state but isn't a dependency, so the effect won't re-subscribe every time the parent redraws. See https://react.dev/reference/react/useEffectEvent",
|
|
56028
57056
|
create: (context) => {
|
|
56029
57057
|
const checkComponent = (componentBody) => {
|
|
56030
57058
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
@@ -57629,7 +58657,6 @@ const reduxUseselectorInlineDerivation = defineRule({
|
|
|
57629
58657
|
title: "useSelector derives data inline",
|
|
57630
58658
|
severity: "warn",
|
|
57631
58659
|
category: "Performance",
|
|
57632
|
-
disabledWhen: ["react-compiler"],
|
|
57633
58660
|
recommendation: "Select the raw slice and memoize derivation so Redux actions do not rebuild a collection and redraw this component.",
|
|
57634
58661
|
create: (context) => {
|
|
57635
58662
|
let aliases = /* @__PURE__ */ new Set();
|
|
@@ -57689,7 +58716,6 @@ const reduxUseselectorReturnsNewCollection = defineRule({
|
|
|
57689
58716
|
title: "useSelector returns a new collection",
|
|
57690
58717
|
severity: "warn",
|
|
57691
58718
|
category: "Performance",
|
|
57692
|
-
disabledWhen: ["react-compiler"],
|
|
57693
58719
|
recommendation: "Return a stable selected value, split selectors, or pass `shallowEqual` so every Redux action does not redraw this component.",
|
|
57694
58720
|
create: (context) => {
|
|
57695
58721
|
let aliases = /* @__PURE__ */ new Set();
|
|
@@ -57815,6 +58841,7 @@ const renderingHoistJsx = defineRule({
|
|
|
57815
58841
|
title: "Constant JSX rebuilt each render",
|
|
57816
58842
|
tags: ["test-noise"],
|
|
57817
58843
|
severity: "warn",
|
|
58844
|
+
disabledWhen: ["react-compiler"],
|
|
57818
58845
|
recommendation: "Move the static JSX out to the top of the file: `const ICON = <svg>...</svg>`, so it isn't rebuilt on every render",
|
|
57819
58846
|
create: (context) => {
|
|
57820
58847
|
let componentDepth = 0;
|
|
@@ -58261,8 +59288,9 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
58261
59288
|
id: "rendering-hydration-no-flicker",
|
|
58262
59289
|
title: "useEffect setState flashes on mount",
|
|
58263
59290
|
tags: ["test-noise"],
|
|
59291
|
+
requires: ["ssr"],
|
|
58264
59292
|
severity: "warn",
|
|
58265
|
-
recommendation: "
|
|
59293
|
+
recommendation: "Read the value with `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)` inside a reusable hook, or add `suppressHydrationWarning` to the element",
|
|
58266
59294
|
create: (context) => ({ CallExpression(node) {
|
|
58267
59295
|
if (!isHookCall$2(node, USE_EFFECT_ONLY) || (node.arguments?.length ?? 0) < 2) return;
|
|
58268
59296
|
const depsNode = node.arguments[1];
|
|
@@ -58715,7 +59743,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
58715
59743
|
title: "Loading useState forces extra render",
|
|
58716
59744
|
tags: ["test-noise"],
|
|
58717
59745
|
severity: "warn",
|
|
58718
|
-
recommendation: "Replace with `const [isPending, startTransition] = useTransition()`, which
|
|
59746
|
+
recommendation: "Replace with `const [isPending, startTransition] = useTransition()`, which marks the update as non-urgent and interruptible so the input stays responsive",
|
|
58719
59747
|
create: (context) => ({ VariableDeclarator(node) {
|
|
58720
59748
|
if (!isNodeOfType(node.id, "ArrayPattern") || !node.id.elements?.length) return;
|
|
58721
59749
|
if (!node.init || !isHookCall$2(node.init, "useState")) return;
|
|
@@ -58747,7 +59775,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
58747
59775
|
}
|
|
58748
59776
|
context.report({
|
|
58749
59777
|
node: node.init,
|
|
58750
|
-
message: `This
|
|
59778
|
+
message: `This makes the "${stateVariableName}" update urgent and blocking because it's a plain useState flag, so if it's a state change & not a data fetch, use useTransition to keep the UI responsive while it runs`
|
|
58751
59779
|
});
|
|
58752
59780
|
} })
|
|
58753
59781
|
});
|
|
@@ -58756,7 +59784,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
58756
59784
|
const isRepositorySecretFilePath = (relativePath) => DOTENV_FILE_PATTERN.test(relativePath) || /(?:^|\/)\.npmrc$/.test(relativePath) || /(?:^|\/)[^/]*(?:credential|credentials|service-account|serviceAccount|firebase-admin|google-service-account|gcp-service-account)[^/]*\.(?:json|env|pem|key)$/i.test(relativePath);
|
|
58757
59785
|
//#endregion
|
|
58758
59786
|
//#region src/plugin/rules/security-scan/repository-secret-file.ts
|
|
58759
|
-
const isRepositorySecretExamplePath = (relativePath) => /(?:^|\/)\.env
|
|
59787
|
+
const isRepositorySecretExamplePath = (relativePath) => /(?:^|\/)\.env(?:\.[^./]+)*\.(?:example|sample|template|dist|defaults?)$|(?:^|\/)[^/]*(?:example|sample|template)[^/]*\.(?:env|json|pem|key)$/i.test(relativePath);
|
|
58760
59788
|
const repositorySecretFile = defineRule({
|
|
58761
59789
|
id: "repository-secret-file",
|
|
58762
59790
|
title: "Secret file checked into repository",
|
|
@@ -59062,6 +60090,7 @@ const rerenderDependencies = defineRule({
|
|
|
59062
60090
|
title: "Unstable value recreated every render",
|
|
59063
60091
|
tags: ["test-noise"],
|
|
59064
60092
|
severity: "error",
|
|
60093
|
+
disabledWhen: ["react-compiler"],
|
|
59065
60094
|
recommendation: "Move it into a useMemo, useRef, or a constant outside the component so it stays the same between renders.",
|
|
59066
60095
|
create: (context) => ({ CallExpression(node) {
|
|
59067
60096
|
if (!isReactHookCall(node, HOOKS_WITH_DEPS, context.scopes) || node.arguments.length < 2) return;
|
|
@@ -59474,7 +60503,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
59474
60503
|
title: "useMemo before an early return",
|
|
59475
60504
|
tags: ["test-noise"],
|
|
59476
60505
|
severity: "warn",
|
|
59477
|
-
recommendation: "Move the JSX into a child component
|
|
60506
|
+
recommendation: "Move the JSX into a child component rendered after the early return, so renders that take the early return never build it",
|
|
59478
60507
|
create: (context) => {
|
|
59479
60508
|
const inspectFunctionBody = (statements) => {
|
|
59480
60509
|
let memoNode = null;
|
|
@@ -59500,7 +60529,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
59500
60529
|
if (callbackGuardTests.some((guardTest) => areConditionsStructurallyEqual(stmt.test, guardTest))) continue;
|
|
59501
60530
|
context.report({
|
|
59502
60531
|
node: memoNode,
|
|
59503
|
-
message: "This
|
|
60532
|
+
message: "This rebuilds the JSX whenever its dependencies change even on renders that take the early return, so move the JSX into a child component rendered after the early return to skip it"
|
|
59504
60533
|
});
|
|
59505
60534
|
return;
|
|
59506
60535
|
}
|
|
@@ -60003,6 +61032,7 @@ const rerenderMemoWithDefaultValue = defineRule({
|
|
|
60003
61032
|
title: "Empty default prop breaks memo",
|
|
60004
61033
|
tags: ["test-noise"],
|
|
60005
61034
|
severity: "warn",
|
|
61035
|
+
disabledWhen: ["react-compiler"],
|
|
60006
61036
|
recommendation: "Move it to the top of the file: `const EMPTY_ITEMS: Item[] = []`, then use that as the default value",
|
|
60007
61037
|
create: (context) => {
|
|
60008
61038
|
let memoRegistry = /* @__PURE__ */ new Map();
|
|
@@ -60135,6 +61165,717 @@ const isSetterCalledDuringRender = (root, setterName) => {
|
|
|
60135
61165
|
return found;
|
|
60136
61166
|
};
|
|
60137
61167
|
//#endregion
|
|
61168
|
+
//#region src/plugin/rules/state-and-effects/utils/create-external-location-invalidation-checker.ts
|
|
61169
|
+
const HISTORY_LOCATION_MUTATION_METHOD_NAMES = new Set(["pushState", "replaceState"]);
|
|
61170
|
+
const AGGREGATE_MUTATION_METHOD_NAMES = new Set([...MUTATING_ARRAY_METHODS, ...MUTATING_COLLECTION_METHODS]);
|
|
61171
|
+
const OBJECT_AGGREGATE_MUTATION_METHOD_NAMES = new Set([
|
|
61172
|
+
"assign",
|
|
61173
|
+
"defineProperties",
|
|
61174
|
+
"defineProperty",
|
|
61175
|
+
"setPrototypeOf"
|
|
61176
|
+
]);
|
|
61177
|
+
const REFLECT_AGGREGATE_MUTATION_METHOD_NAMES = new Set([
|
|
61178
|
+
"defineProperty",
|
|
61179
|
+
"deleteProperty",
|
|
61180
|
+
"set",
|
|
61181
|
+
"setPrototypeOf"
|
|
61182
|
+
]);
|
|
61183
|
+
const LOCATION_CHANGE_EVENT_NAMES = new Set(["hashchange", "popstate"]);
|
|
61184
|
+
const containsGlobalLocationSnapshotRead = (node, scopes) => {
|
|
61185
|
+
let didFindLocationSnapshotRead = false;
|
|
61186
|
+
walkAst(node, (child) => {
|
|
61187
|
+
if (didFindLocationSnapshotRead) return false;
|
|
61188
|
+
if (child !== node && isFunctionLike$1(child) && !executesDuringRender(findTransparentExpressionRoot(child), scopes)) return false;
|
|
61189
|
+
if (!isProvenGlobalNamespaceReference(child, "location", scopes)) return;
|
|
61190
|
+
didFindLocationSnapshotRead = true;
|
|
61191
|
+
return false;
|
|
61192
|
+
});
|
|
61193
|
+
return didFindLocationSnapshotRead;
|
|
61194
|
+
};
|
|
61195
|
+
const resolveExactLocalOrReactCallbackFunction = (expression, scopes) => {
|
|
61196
|
+
const localFunction = resolveExactLocalFunction(expression, scopes);
|
|
61197
|
+
if (isFunctionLike$1(localFunction)) return localFunction;
|
|
61198
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
61199
|
+
let callbackSource = unwrappedExpression;
|
|
61200
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
61201
|
+
const callbackSymbol = resolveConstIdentifierAlias(unwrappedExpression, scopes);
|
|
61202
|
+
if (callbackSymbol?.kind !== "const" || !callbackSymbol.initializer) return null;
|
|
61203
|
+
callbackSource = callbackSymbol.initializer;
|
|
61204
|
+
}
|
|
61205
|
+
const unwrappedCallbackSource = stripParenExpression(callbackSource);
|
|
61206
|
+
if (!isNodeOfType(unwrappedCallbackSource, "CallExpression") || !isReactApiCall(unwrappedCallbackSource, "useCallback", scopes, { resolveNamedAliases: true })) return null;
|
|
61207
|
+
const callback = unwrappedCallbackSource.arguments?.[0];
|
|
61208
|
+
if (!callback || isNodeOfType(callback, "SpreadElement")) return null;
|
|
61209
|
+
return resolveExactLocalFunction(stripParenExpression(callback), scopes);
|
|
61210
|
+
};
|
|
61211
|
+
const bindingReadsExactGlobalLocationSnapshot = (bindingIdentifier, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
61212
|
+
const symbol = scopes.symbolFor(bindingIdentifier);
|
|
61213
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
61214
|
+
if ((symbol.kind === "function" ? scopes.scopeFor(symbol.declarationNode) : symbol.scope).symbols.some((candidateSymbol) => candidateSymbol.name === symbol.name && candidateSymbol.references.some((reference) => reference.flag !== "read"))) return false;
|
|
61215
|
+
const resolvedFunction = resolveExactLocalOrReactCallbackFunction(bindingIdentifier, scopes);
|
|
61216
|
+
const initializer = isFunctionLike$1(resolvedFunction) ? resolvedFunction.body : getDirectUnreassignedInitializer(symbol);
|
|
61217
|
+
if (!initializer) return false;
|
|
61218
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
61219
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
61220
|
+
if (containsGlobalLocationSnapshotRead(initializer, scopes)) return true;
|
|
61221
|
+
let didFindAliasedLocationSnapshotRead = false;
|
|
61222
|
+
walkAst(initializer, (child) => {
|
|
61223
|
+
if (didFindAliasedLocationSnapshotRead) return false;
|
|
61224
|
+
if (child !== initializer && isFunctionLike$1(child) && !executesDuringRender(findTransparentExpressionRoot(child), scopes)) return false;
|
|
61225
|
+
if (!isNodeOfType(child, "Identifier")) return;
|
|
61226
|
+
if (bindingReadsExactGlobalLocationSnapshot(child, scopes, nextVisitedSymbolIds)) {
|
|
61227
|
+
didFindAliasedLocationSnapshotRead = true;
|
|
61228
|
+
return false;
|
|
61229
|
+
}
|
|
61230
|
+
});
|
|
61231
|
+
return didFindAliasedLocationSnapshotRead;
|
|
61232
|
+
};
|
|
61233
|
+
const hasRenderReachableLocationSnapshotRead = (componentBody, renderReachableExpressions, directRenderNames, scopes) => {
|
|
61234
|
+
if (renderReachableExpressions.some((expression) => containsGlobalLocationSnapshotRead(expression, scopes))) return true;
|
|
61235
|
+
for (const statement of componentBody.body ?? []) {
|
|
61236
|
+
if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) {
|
|
61237
|
+
if (directRenderNames.has(statement.id.name) && bindingReadsExactGlobalLocationSnapshot(statement.id, scopes)) return true;
|
|
61238
|
+
continue;
|
|
61239
|
+
}
|
|
61240
|
+
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
61241
|
+
for (const declarator of statement.declarations ?? []) {
|
|
61242
|
+
if (!isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
|
|
61243
|
+
if (!directRenderNames.has(declarator.id.name)) continue;
|
|
61244
|
+
if (bindingReadsExactGlobalLocationSnapshot(declarator.id, scopes)) return true;
|
|
61245
|
+
}
|
|
61246
|
+
}
|
|
61247
|
+
return false;
|
|
61248
|
+
};
|
|
61249
|
+
const isGlobalHistoryLocationMutation = (node, scopes) => {
|
|
61250
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
61251
|
+
const callee = stripParenExpression(node.callee);
|
|
61252
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
61253
|
+
const methodName = getStaticPropertyName(callee);
|
|
61254
|
+
return Boolean(methodName && HISTORY_LOCATION_MUTATION_METHOD_NAMES.has(methodName) && isProvenGlobalNamespaceReference(callee.object, "history", scopes));
|
|
61255
|
+
};
|
|
61256
|
+
const getStaticLocationChangeEvent = (node) => {
|
|
61257
|
+
if (!node) return null;
|
|
61258
|
+
const eventNameNode = stripParenExpression(node);
|
|
61259
|
+
if (!isNodeOfType(eventNameNode, "Literal") || typeof eventNameNode.value !== "string") return null;
|
|
61260
|
+
return LOCATION_CHANGE_EVENT_NAMES.has(eventNameNode.value) ? eventNameNode.value : null;
|
|
61261
|
+
};
|
|
61262
|
+
const addToSetIndex = (index, key, value) => {
|
|
61263
|
+
const indexedValues = index.get(key) ?? /* @__PURE__ */ new Set();
|
|
61264
|
+
indexedValues.add(value);
|
|
61265
|
+
index.set(key, indexedValues);
|
|
61266
|
+
};
|
|
61267
|
+
const getSynchronousInvocationExpression = (functionNode, scopes) => {
|
|
61268
|
+
const functionExpressionRoot = findTransparentExpressionRoot(functionNode);
|
|
61269
|
+
if (!executesDuringRender(functionExpressionRoot, scopes)) return null;
|
|
61270
|
+
const parent = functionExpressionRoot.parent;
|
|
61271
|
+
return isNodeOfType(parent, "CallExpression") || isNodeOfType(parent, "NewExpression") ? parent : null;
|
|
61272
|
+
};
|
|
61273
|
+
const getLocationListenerOperation = (callExpression, scopes) => {
|
|
61274
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
61275
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
61276
|
+
const methodName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : isNodeOfType(callee, "Identifier") ? callee.name : null;
|
|
61277
|
+
if (methodName !== "addEventListener" && methodName !== "removeEventListener") return null;
|
|
61278
|
+
if (!(isNodeOfType(callee, "MemberExpression") ? isProvenGlobalObjectReference(callee.object, scopes) : isNodeOfType(callee, "Identifier") && scopes.isGlobalReference(callee))) return null;
|
|
61279
|
+
const eventName = getStaticLocationChangeEvent(callExpression.arguments?.[0]);
|
|
61280
|
+
if (!eventName) return null;
|
|
61281
|
+
const listenerExpression = callExpression.arguments?.[1];
|
|
61282
|
+
if (!listenerExpression || isNodeOfType(listenerExpression, "SpreadElement")) return null;
|
|
61283
|
+
const listenerFunction = resolveExactLocalOrReactCallbackFunction(listenerExpression, scopes);
|
|
61284
|
+
if (!isFunctionLike$1(listenerFunction)) return null;
|
|
61285
|
+
const captureArgument = callExpression.arguments?.[2];
|
|
61286
|
+
const capture = isNodeOfType(captureArgument, "SpreadElement") ? null : resolveEventListenerCapture(captureArgument, {
|
|
61287
|
+
allowComputedString: true,
|
|
61288
|
+
allowIndeterminateEntries: true
|
|
61289
|
+
});
|
|
61290
|
+
return {
|
|
61291
|
+
operation: methodName === "addEventListener" ? "add" : "remove",
|
|
61292
|
+
registration: {
|
|
61293
|
+
callExpression,
|
|
61294
|
+
listenerFunction,
|
|
61295
|
+
capture,
|
|
61296
|
+
eventName
|
|
61297
|
+
}
|
|
61298
|
+
};
|
|
61299
|
+
};
|
|
61300
|
+
const buildLocationInvalidationIndex = (componentBody, componentFunction, context) => {
|
|
61301
|
+
const index = {
|
|
61302
|
+
componentFunction,
|
|
61303
|
+
context,
|
|
61304
|
+
effectCallbacks: /* @__PURE__ */ new Set(),
|
|
61305
|
+
expressionsByOwner: /* @__PURE__ */ new Map(),
|
|
61306
|
+
historyMutationsByOwner: /* @__PURE__ */ new Map(),
|
|
61307
|
+
awaitExpressionsByOwner: /* @__PURE__ */ new Map(),
|
|
61308
|
+
callSitesByFunction: /* @__PURE__ */ new Map(),
|
|
61309
|
+
calledFunctionByExpression: /* @__PURE__ */ new Map(),
|
|
61310
|
+
synchronousInvocationsByFunction: /* @__PURE__ */ new Map(),
|
|
61311
|
+
synchronousCallbacksByExpression: /* @__PURE__ */ new Map(),
|
|
61312
|
+
callsByCalleeSymbolId: /* @__PURE__ */ new Map(),
|
|
61313
|
+
identifierCalls: /* @__PURE__ */ new Set(),
|
|
61314
|
+
listenerRegistrations: [],
|
|
61315
|
+
listenerRemovals: [],
|
|
61316
|
+
mountedListenerFunctions: /* @__PURE__ */ new Set(),
|
|
61317
|
+
mutationExecutionsByOwner: /* @__PURE__ */ new Map(),
|
|
61318
|
+
synchronousMutationResultByFunction: /* @__PURE__ */ new Map()
|
|
61319
|
+
};
|
|
61320
|
+
walkAst(componentBody, (child) => {
|
|
61321
|
+
if (isFunctionLike$1(child)) {
|
|
61322
|
+
const invocationExpression = getSynchronousInvocationExpression(child, context.scopes);
|
|
61323
|
+
if (invocationExpression) {
|
|
61324
|
+
addToSetIndex(index.synchronousInvocationsByFunction, child, invocationExpression);
|
|
61325
|
+
addToSetIndex(index.synchronousCallbacksByExpression, invocationExpression, child);
|
|
61326
|
+
}
|
|
61327
|
+
return;
|
|
61328
|
+
}
|
|
61329
|
+
const owner = context.cfg.enclosingFunction(child);
|
|
61330
|
+
if (!owner) return;
|
|
61331
|
+
if (isNodeOfType(child, "AwaitExpression")) {
|
|
61332
|
+
addToSetIndex(index.awaitExpressionsByOwner, owner, child);
|
|
61333
|
+
return;
|
|
61334
|
+
}
|
|
61335
|
+
if (!isNodeOfType(child, "CallExpression") && !isNodeOfType(child, "NewExpression")) return;
|
|
61336
|
+
addToSetIndex(index.expressionsByOwner, owner, child);
|
|
61337
|
+
if (isNodeOfType(child, "CallExpression")) {
|
|
61338
|
+
if (isGlobalHistoryLocationMutation(child, context.scopes)) addToSetIndex(index.historyMutationsByOwner, owner, child);
|
|
61339
|
+
if (isNodeOfType(stripParenExpression(child.callee), "Identifier")) index.identifierCalls.add(child);
|
|
61340
|
+
const calledFunction = resolveExactLocalOrReactCallbackFunction(child.callee, context.scopes);
|
|
61341
|
+
if (isFunctionLike$1(calledFunction)) {
|
|
61342
|
+
addToSetIndex(index.callSitesByFunction, calledFunction, child);
|
|
61343
|
+
index.calledFunctionByExpression.set(child, calledFunction);
|
|
61344
|
+
}
|
|
61345
|
+
if (context.cfg.enclosingFunction(child) === componentFunction && isReactApiCall(child, EFFECT_HOOK_NAMES$1, context.scopes, {
|
|
61346
|
+
allowGlobalReactNamespace: true,
|
|
61347
|
+
allowUnboundBareCalls: true,
|
|
61348
|
+
resolveNamedAliases: true
|
|
61349
|
+
})) {
|
|
61350
|
+
const effectCallback = getEffectCallback(child, context.scopes);
|
|
61351
|
+
if (isFunctionLike$1(effectCallback)) index.effectCallbacks.add(effectCallback);
|
|
61352
|
+
}
|
|
61353
|
+
const listenerOperation = getLocationListenerOperation(child, context.scopes);
|
|
61354
|
+
if (listenerOperation?.operation === "add") index.listenerRegistrations.push(listenerOperation.registration);
|
|
61355
|
+
else if (listenerOperation) index.listenerRemovals.push(listenerOperation.registration);
|
|
61356
|
+
}
|
|
61357
|
+
});
|
|
61358
|
+
return index;
|
|
61359
|
+
};
|
|
61360
|
+
const isDescendantWithoutFunctionBoundary = (descendant, ancestor) => {
|
|
61361
|
+
let current = descendant;
|
|
61362
|
+
while (current && current !== ancestor) {
|
|
61363
|
+
if (current !== descendant && isFunctionLike$1(current)) return false;
|
|
61364
|
+
current = current.parent;
|
|
61365
|
+
}
|
|
61366
|
+
return current === ancestor;
|
|
61367
|
+
};
|
|
61368
|
+
const areInMutuallyExclusiveConditionalBranches = (firstNode, secondNode) => {
|
|
61369
|
+
const firstBranches = /* @__PURE__ */ new Map();
|
|
61370
|
+
let current = firstNode;
|
|
61371
|
+
while (current?.parent) {
|
|
61372
|
+
const parent = current.parent;
|
|
61373
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === current || parent.alternate === current)) firstBranches.set(parent, current);
|
|
61374
|
+
if (current !== firstNode && isFunctionLike$1(current)) break;
|
|
61375
|
+
current = parent;
|
|
61376
|
+
}
|
|
61377
|
+
current = secondNode;
|
|
61378
|
+
while (current?.parent) {
|
|
61379
|
+
const parent = current.parent;
|
|
61380
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === current || parent.alternate === current)) {
|
|
61381
|
+
const firstBranch = firstBranches.get(parent);
|
|
61382
|
+
if (firstBranch && firstBranch !== current) return true;
|
|
61383
|
+
}
|
|
61384
|
+
if (current !== secondNode && isFunctionLike$1(current)) break;
|
|
61385
|
+
current = parent;
|
|
61386
|
+
}
|
|
61387
|
+
return false;
|
|
61388
|
+
};
|
|
61389
|
+
const collectStableBooleanGuardConstraints = (node, functionBoundary, scopes) => {
|
|
61390
|
+
const constraintsBySymbolId = /* @__PURE__ */ new Map();
|
|
61391
|
+
const recordConstraint = (test, requiredTestValue) => {
|
|
61392
|
+
let expression = stripParenExpression(test);
|
|
61393
|
+
let requiredValue = requiredTestValue;
|
|
61394
|
+
while (isNodeOfType(expression, "UnaryExpression") && expression.operator === "!") {
|
|
61395
|
+
requiredValue = !requiredValue;
|
|
61396
|
+
expression = stripParenExpression(expression.argument);
|
|
61397
|
+
}
|
|
61398
|
+
if (!isNodeOfType(expression, "Identifier")) return;
|
|
61399
|
+
const symbol = scopes.symbolFor(expression);
|
|
61400
|
+
if (!symbol || symbol.references.some((reference) => reference.flag !== "read")) return;
|
|
61401
|
+
const existingConstraint = constraintsBySymbolId.get(symbol.id);
|
|
61402
|
+
constraintsBySymbolId.set(symbol.id, existingConstraint === void 0 || existingConstraint === requiredValue ? requiredValue : null);
|
|
61403
|
+
};
|
|
61404
|
+
let current = node;
|
|
61405
|
+
while (current?.parent && current !== functionBoundary) {
|
|
61406
|
+
const parent = current.parent;
|
|
61407
|
+
if (isNodeOfType(parent, "IfStatement")) {
|
|
61408
|
+
if (parent.consequent === current) recordConstraint(parent.test, true);
|
|
61409
|
+
if (parent.alternate === current) recordConstraint(parent.test, false);
|
|
61410
|
+
} else if (isNodeOfType(parent, "ConditionalExpression")) {
|
|
61411
|
+
if (parent.consequent === current) recordConstraint(parent.test, true);
|
|
61412
|
+
if (parent.alternate === current) recordConstraint(parent.test, false);
|
|
61413
|
+
} else if (isNodeOfType(parent, "LogicalExpression") && parent.right === current) {
|
|
61414
|
+
if (parent.operator === "&&") recordConstraint(parent.left, true);
|
|
61415
|
+
if (parent.operator === "||") recordConstraint(parent.left, false);
|
|
61416
|
+
}
|
|
61417
|
+
if (current !== node && isFunctionLike$1(current)) break;
|
|
61418
|
+
current = parent;
|
|
61419
|
+
}
|
|
61420
|
+
return constraintsBySymbolId;
|
|
61421
|
+
};
|
|
61422
|
+
const haveContradictoryStableBooleanGuards = (firstNode, secondNode, functionBoundary, scopes) => {
|
|
61423
|
+
const firstConstraints = collectStableBooleanGuardConstraints(firstNode, functionBoundary, scopes);
|
|
61424
|
+
const secondConstraints = collectStableBooleanGuardConstraints(secondNode, functionBoundary, scopes);
|
|
61425
|
+
for (const requiredValue of firstConstraints.values()) if (requiredValue === null) return true;
|
|
61426
|
+
for (const requiredValue of secondConstraints.values()) if (requiredValue === null) return true;
|
|
61427
|
+
for (const [symbolId, firstRequiredValue] of firstConstraints) {
|
|
61428
|
+
const secondRequiredValue = secondConstraints.get(symbolId);
|
|
61429
|
+
if (firstRequiredValue !== null && secondRequiredValue !== void 0 && secondRequiredValue !== null && firstRequiredValue !== secondRequiredValue) return true;
|
|
61430
|
+
}
|
|
61431
|
+
return false;
|
|
61432
|
+
};
|
|
61433
|
+
const isInsideIntrinsicReactEventHandlerAttribute = (node, functionBoundary) => {
|
|
61434
|
+
let current = node;
|
|
61435
|
+
while (current && current !== functionBoundary) {
|
|
61436
|
+
if (isEventHandlerAttribute(current) && isJsxAttributeOnIntrinsicHtmlElement(current)) return true;
|
|
61437
|
+
current = current.parent;
|
|
61438
|
+
}
|
|
61439
|
+
return false;
|
|
61440
|
+
};
|
|
61441
|
+
const getTransparentExpressionBindingIdentifier = (expression) => {
|
|
61442
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
61443
|
+
const parent = expressionRoot.parent;
|
|
61444
|
+
if (isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) return parent.id;
|
|
61445
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === expressionRoot && isNodeOfType(parent.left, "Identifier")) return parent.left;
|
|
61446
|
+
return null;
|
|
61447
|
+
};
|
|
61448
|
+
const getIntrinsicReactEventHandlerBindingIdentifier = (functionNode, index) => {
|
|
61449
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
61450
|
+
const functionExpressionRoot = findTransparentExpressionRoot(functionNode);
|
|
61451
|
+
const directTransparentBindingIdentifier = getTransparentExpressionBindingIdentifier(functionExpressionRoot);
|
|
61452
|
+
if (directTransparentBindingIdentifier) return directTransparentBindingIdentifier;
|
|
61453
|
+
const callbackCall = functionExpressionRoot.parent;
|
|
61454
|
+
if (!isNodeOfType(callbackCall, "CallExpression") || callbackCall.arguments?.[0] !== functionExpressionRoot || !isReactApiCall(callbackCall, "useCallback", index.context.scopes, { resolveNamedAliases: true })) return null;
|
|
61455
|
+
return getTransparentExpressionBindingIdentifier(callbackCall);
|
|
61456
|
+
};
|
|
61457
|
+
const isExclusiveIntrinsicReactEventHandler = (functionNode, index) => {
|
|
61458
|
+
const bindingIdentifier = getIntrinsicReactEventHandlerBindingIdentifier(functionNode, index);
|
|
61459
|
+
if (!bindingIdentifier) return isInsideIntrinsicReactEventHandlerAttribute(functionNode, index.componentFunction);
|
|
61460
|
+
const bindingSymbol = index.context.scopes.symbolFor(bindingIdentifier);
|
|
61461
|
+
return Boolean(bindingSymbol && bindingSymbol.references.length > 0 && bindingSymbol.references.every((reference) => isInsideIntrinsicReactEventHandlerAttribute(reference.identifier, index.componentFunction)));
|
|
61462
|
+
};
|
|
61463
|
+
const canNodeReachNode = (sourceNode, targetNode, index) => {
|
|
61464
|
+
const { context } = index;
|
|
61465
|
+
if (!isNodeReachableWithinFunction(sourceNode, context)) return false;
|
|
61466
|
+
if (!isNodeReachableWithinFunction(targetNode, context)) return false;
|
|
61467
|
+
const sourceOwner = context.cfg.enclosingFunction(sourceNode);
|
|
61468
|
+
const targetOwner = context.cfg.enclosingFunction(targetNode);
|
|
61469
|
+
if (!sourceOwner || sourceOwner !== targetOwner) return false;
|
|
61470
|
+
if (areInMutuallyExclusiveConditionalBranches(sourceNode, targetNode)) return false;
|
|
61471
|
+
if (haveContradictoryStableBooleanGuards(sourceNode, targetNode, sourceOwner, context.scopes)) return false;
|
|
61472
|
+
if (isDescendantWithoutFunctionBoundary(targetNode, sourceNode)) return false;
|
|
61473
|
+
if (isDescendantWithoutFunctionBoundary(sourceNode, targetNode)) return true;
|
|
61474
|
+
const functionCfg = context.cfg.cfgFor(sourceOwner);
|
|
61475
|
+
const sourceBlock = functionCfg?.blockOf(sourceNode);
|
|
61476
|
+
const targetBlock = functionCfg?.blockOf(targetNode);
|
|
61477
|
+
if (!functionCfg || !sourceBlock || !targetBlock) return false;
|
|
61478
|
+
if (sourceBlock === targetBlock) {
|
|
61479
|
+
const sourceStart = getRangeStart(sourceNode);
|
|
61480
|
+
const targetStart = getRangeStart(targetNode);
|
|
61481
|
+
return sourceStart !== null && targetStart !== null && sourceStart < targetStart;
|
|
61482
|
+
}
|
|
61483
|
+
const visitedBlocks = new Set([sourceBlock]);
|
|
61484
|
+
const pendingBlocks = sourceBlock.successors.filter((edge) => edge.kind !== "throw").map((edge) => edge.to);
|
|
61485
|
+
while (pendingBlocks.length > 0) {
|
|
61486
|
+
const block = pendingBlocks.pop();
|
|
61487
|
+
if (!block || visitedBlocks.has(block)) continue;
|
|
61488
|
+
if (block === targetBlock) return true;
|
|
61489
|
+
visitedBlocks.add(block);
|
|
61490
|
+
for (const edge of block.successors) if (edge.kind !== "throw") pendingBlocks.push(edge.to);
|
|
61491
|
+
}
|
|
61492
|
+
return false;
|
|
61493
|
+
};
|
|
61494
|
+
const getInlineIntrinsicHandlerJsxElement = (functionNode) => {
|
|
61495
|
+
let currentNode = functionNode;
|
|
61496
|
+
while (currentNode) {
|
|
61497
|
+
if (isNodeOfType(currentNode, "JSXAttribute") && isEventHandlerAttribute(currentNode) && isJsxAttributeOnIntrinsicHtmlElement(currentNode)) {
|
|
61498
|
+
const openingElement = currentNode.parent;
|
|
61499
|
+
return isNodeOfType(openingElement?.parent, "JSXElement") ? openingElement.parent : null;
|
|
61500
|
+
}
|
|
61501
|
+
if (currentNode !== functionNode && isFunctionLike$1(currentNode)) return null;
|
|
61502
|
+
currentNode = currentNode.parent;
|
|
61503
|
+
}
|
|
61504
|
+
return null;
|
|
61505
|
+
};
|
|
61506
|
+
const isProvenNonEscapingVoidRead = (identifier) => {
|
|
61507
|
+
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
61508
|
+
return isNodeOfType(expressionRoot.parent, "UnaryExpression") && expressionRoot.parent.operator === "void" && expressionRoot.parent.argument === expressionRoot;
|
|
61509
|
+
};
|
|
61510
|
+
const getDirectReadonlyValueAliasSymbol = (referenceIdentifier, sourceSymbol, index) => {
|
|
61511
|
+
const referenceRoot = findTransparentExpressionRoot(referenceIdentifier);
|
|
61512
|
+
const parent = referenceRoot.parent;
|
|
61513
|
+
if (isNodeOfType(parent, "VariableDeclarator") && parent.init === referenceRoot && isNodeOfType(parent.id, "Identifier")) {
|
|
61514
|
+
const aliasSymbol = index.context.scopes.symbolFor(parent.id);
|
|
61515
|
+
const aliasInitializer = aliasSymbol ? getDirectConstInitializer(aliasSymbol) : null;
|
|
61516
|
+
const unwrappedInitializer = aliasInitializer ? stripParenExpression(aliasInitializer) : null;
|
|
61517
|
+
return aliasSymbol && isNodeOfType(unwrappedInitializer, "Identifier") && index.context.scopes.symbolFor(unwrappedInitializer) === sourceSymbol ? aliasSymbol : null;
|
|
61518
|
+
}
|
|
61519
|
+
if (!isNodeOfType(parent, "CallExpression") || parent.arguments?.[0] !== referenceRoot || !isReactApiCall(parent, "useCallback", index.context.scopes, { resolveNamedAliases: true })) return null;
|
|
61520
|
+
const callbackResultRoot = findTransparentExpressionRoot(parent);
|
|
61521
|
+
const resultParent = callbackResultRoot.parent;
|
|
61522
|
+
if (!isNodeOfType(resultParent, "VariableDeclarator") || resultParent.init !== callbackResultRoot || !isNodeOfType(resultParent.id, "Identifier")) return null;
|
|
61523
|
+
const callbackResultSymbol = index.context.scopes.symbolFor(resultParent.id);
|
|
61524
|
+
return callbackResultSymbol && getDirectConstInitializer(callbackResultSymbol) === callbackResultRoot ? callbackResultSymbol : null;
|
|
61525
|
+
};
|
|
61526
|
+
const isKnownAggregateMutationReference = (referenceIdentifier, index) => {
|
|
61527
|
+
const referenceRoot = findTransparentExpressionRoot(referenceIdentifier);
|
|
61528
|
+
const parent = referenceRoot.parent;
|
|
61529
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === referenceRoot) {
|
|
61530
|
+
const memberRoot = findTransparentExpressionRoot(parent);
|
|
61531
|
+
const callExpression = memberRoot.parent;
|
|
61532
|
+
const methodName = getStaticPropertyName(parent);
|
|
61533
|
+
if (isNodeOfType(callExpression, "CallExpression") && callExpression.callee === memberRoot && methodName !== null && AGGREGATE_MUTATION_METHOD_NAMES.has(methodName)) return true;
|
|
61534
|
+
}
|
|
61535
|
+
if (!isNodeOfType(parent, "CallExpression") || parent.arguments?.[0] !== referenceRoot) return false;
|
|
61536
|
+
const callee = stripParenExpression(parent.callee);
|
|
61537
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
61538
|
+
const methodName = getStaticPropertyName(callee);
|
|
61539
|
+
if (methodName === null) return false;
|
|
61540
|
+
return OBJECT_AGGREGATE_MUTATION_METHOD_NAMES.has(methodName) && isProvenGlobalNamespaceReference(callee.object, "Object", index.context.scopes) || REFLECT_AGGREGATE_MUTATION_METHOD_NAMES.has(methodName) && isProvenGlobalNamespaceReference(callee.object, "Reflect", index.context.scopes);
|
|
61541
|
+
};
|
|
61542
|
+
const collectExactReadonlyValueEscapeAnchors = (symbol, index, valueFunctionNode, options = {}, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
61543
|
+
if (visitedSymbolIds.has(symbol.id)) return null;
|
|
61544
|
+
if (symbol.references.some((reference) => reference.flag !== "read" || isWithinAssignmentTarget(reference.identifier) || options.rejectKnownMutations && isKnownAggregateMutationReference(reference.identifier, index))) return null;
|
|
61545
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
61546
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
61547
|
+
const escapeAnchors = [];
|
|
61548
|
+
for (const reference of symbol.references) {
|
|
61549
|
+
if (valueFunctionNode && index.context.cfg.enclosingFunction(reference.identifier) === valueFunctionNode || isProvenNonEscapingVoidRead(reference.identifier)) continue;
|
|
61550
|
+
const aliasSymbol = getDirectReadonlyValueAliasSymbol(reference.identifier, symbol, index);
|
|
61551
|
+
if (!aliasSymbol) {
|
|
61552
|
+
escapeAnchors.push(reference.identifier);
|
|
61553
|
+
continue;
|
|
61554
|
+
}
|
|
61555
|
+
const aliasEscapeAnchors = collectExactReadonlyValueEscapeAnchors(aliasSymbol, index, valueFunctionNode, options, nextVisitedSymbolIds);
|
|
61556
|
+
if (!aliasEscapeAnchors) return null;
|
|
61557
|
+
escapeAnchors.push(...aliasEscapeAnchors);
|
|
61558
|
+
}
|
|
61559
|
+
return escapeAnchors.length > 0 ? escapeAnchors : null;
|
|
61560
|
+
};
|
|
61561
|
+
const findReadonlyJsxAggregateRoot = (jsxElement) => {
|
|
61562
|
+
let aggregateRoot = findTransparentExpressionRoot(jsxElement);
|
|
61563
|
+
while (aggregateRoot.parent) {
|
|
61564
|
+
const parent = aggregateRoot.parent;
|
|
61565
|
+
if ((isNodeOfType(parent, "JSXElement") || isNodeOfType(parent, "JSXFragment")) && parent.children?.some((child) => child === aggregateRoot)) {
|
|
61566
|
+
aggregateRoot = parent;
|
|
61567
|
+
continue;
|
|
61568
|
+
}
|
|
61569
|
+
if (isNodeOfType(parent, "ArrayExpression") && parent.elements?.some((element) => element === aggregateRoot)) {
|
|
61570
|
+
aggregateRoot = findTransparentExpressionRoot(parent);
|
|
61571
|
+
continue;
|
|
61572
|
+
}
|
|
61573
|
+
if (isNodeOfType(parent, "Property") && parent.value === aggregateRoot && parent.kind === "init" && !parent.computed && isNodeOfType(parent.parent, "ObjectExpression")) {
|
|
61574
|
+
aggregateRoot = findTransparentExpressionRoot(parent.parent);
|
|
61575
|
+
continue;
|
|
61576
|
+
}
|
|
61577
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === aggregateRoot || parent.alternate === aggregateRoot)) {
|
|
61578
|
+
aggregateRoot = findTransparentExpressionRoot(parent);
|
|
61579
|
+
continue;
|
|
61580
|
+
}
|
|
61581
|
+
if (isNodeOfType(parent, "LogicalExpression") && (parent.right === aggregateRoot || parent.left === aggregateRoot && parent.operator !== "&&")) {
|
|
61582
|
+
aggregateRoot = findTransparentExpressionRoot(parent);
|
|
61583
|
+
continue;
|
|
61584
|
+
}
|
|
61585
|
+
if (isNodeOfType(parent, "SequenceExpression") && getFinalSequenceExpressionValue(parent) === getFinalSequenceExpressionValue(aggregateRoot)) {
|
|
61586
|
+
aggregateRoot = findTransparentExpressionRoot(parent);
|
|
61587
|
+
continue;
|
|
61588
|
+
}
|
|
61589
|
+
break;
|
|
61590
|
+
}
|
|
61591
|
+
return aggregateRoot;
|
|
61592
|
+
};
|
|
61593
|
+
const getReadonlyJsxValueEscapeAnchors = (jsxElement, index) => {
|
|
61594
|
+
const jsxValueRoot = findReadonlyJsxAggregateRoot(jsxElement);
|
|
61595
|
+
const parent = jsxValueRoot.parent;
|
|
61596
|
+
if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== jsxValueRoot) return [jsxValueRoot];
|
|
61597
|
+
if (!isNodeOfType(parent.id, "Identifier")) return null;
|
|
61598
|
+
const bindingSymbol = index.context.scopes.symbolFor(parent.id);
|
|
61599
|
+
if (!bindingSymbol || getDirectConstInitializer(bindingSymbol) !== jsxValueRoot) return null;
|
|
61600
|
+
return collectExactReadonlyValueEscapeAnchors(bindingSymbol, index, null, { rejectKnownMutations: true });
|
|
61601
|
+
};
|
|
61602
|
+
const getExclusiveIntrinsicReactEventHandlerAnchors = (functionNode, index) => {
|
|
61603
|
+
if (!isExclusiveIntrinsicReactEventHandler(functionNode, index)) return null;
|
|
61604
|
+
const bindingIdentifier = getIntrinsicReactEventHandlerBindingIdentifier(functionNode, index);
|
|
61605
|
+
if (!bindingIdentifier) {
|
|
61606
|
+
const jsxElement = getInlineIntrinsicHandlerJsxElement(functionNode);
|
|
61607
|
+
return jsxElement ? getReadonlyJsxValueEscapeAnchors(jsxElement, index) : null;
|
|
61608
|
+
}
|
|
61609
|
+
return index.context.scopes.symbolFor(bindingIdentifier)?.references.map((reference) => reference.identifier) ?? null;
|
|
61610
|
+
};
|
|
61611
|
+
const getReadonlyFunctionEscapeAnchors = (functionNode, index) => {
|
|
61612
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
61613
|
+
if (!bindingIdentifier) return null;
|
|
61614
|
+
const bindingSymbol = index.context.scopes.symbolFor(bindingIdentifier);
|
|
61615
|
+
if (!bindingSymbol || resolveExactLocalFunction(bindingIdentifier, index.context.scopes) !== functionNode) return null;
|
|
61616
|
+
return collectExactReadonlyValueEscapeAnchors(bindingSymbol, index, functionNode);
|
|
61617
|
+
};
|
|
61618
|
+
const isAliasInitializedBeforeExecution = (aliasDeclaration, executionNode, index, visitedFunctionNodes = /* @__PURE__ */ new Set()) => {
|
|
61619
|
+
if (!isNodeReachableWithinFunction(aliasDeclaration, index.context)) return false;
|
|
61620
|
+
const aliasOwner = index.context.cfg.enclosingFunction(aliasDeclaration);
|
|
61621
|
+
const executionOwner = index.context.cfg.enclosingFunction(executionNode);
|
|
61622
|
+
if (!aliasOwner || !executionOwner) return false;
|
|
61623
|
+
if (aliasOwner === executionOwner) return canNodeReachNode(aliasDeclaration, executionNode, index);
|
|
61624
|
+
if (visitedFunctionNodes.has(executionOwner)) return false;
|
|
61625
|
+
const nextVisitedFunctionNodes = new Set(visitedFunctionNodes);
|
|
61626
|
+
nextVisitedFunctionNodes.add(executionOwner);
|
|
61627
|
+
if (aliasOwner === index.componentFunction && (index.effectCallbacks.has(executionOwner) || index.mountedListenerFunctions.has(executionOwner))) return doNodesCoverEveryPathFromFunctionEntry(index.componentFunction, [aliasDeclaration], index.context, { ignoreThrowEdges: true });
|
|
61628
|
+
const invocations = new Set([...index.callSitesByFunction.get(executionOwner) ?? [], ...index.synchronousInvocationsByFunction.get(executionOwner) ?? []]);
|
|
61629
|
+
if (invocations.size > 0) return [...invocations].every((invocation) => isAliasInitializedBeforeExecution(aliasDeclaration, invocation, index, nextVisitedFunctionNodes));
|
|
61630
|
+
const deferredExecutionAnchors = getExclusiveIntrinsicReactEventHandlerAnchors(executionOwner, index);
|
|
61631
|
+
if (deferredExecutionAnchors) return deferredExecutionAnchors.every((anchor) => isAliasInitializedBeforeExecution(aliasDeclaration, anchor, index, nextVisitedFunctionNodes));
|
|
61632
|
+
const functionEscapeAnchors = getReadonlyFunctionEscapeAnchors(executionOwner, index);
|
|
61633
|
+
return Boolean(functionEscapeAnchors?.every((anchor) => isAliasInitializedBeforeExecution(aliasDeclaration, anchor, index, nextVisitedFunctionNodes)));
|
|
61634
|
+
};
|
|
61635
|
+
const resolveExactReadonlyCalleeSymbolId = (callExpression, index) => {
|
|
61636
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
61637
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
61638
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
61639
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
61640
|
+
let symbol = index.context.scopes.symbolFor(callee);
|
|
61641
|
+
while (symbol) {
|
|
61642
|
+
if (visitedSymbolIds.has(symbol.id)) return null;
|
|
61643
|
+
visitedSymbolIds.add(symbol.id);
|
|
61644
|
+
const initializer = getDirectConstInitializer(symbol);
|
|
61645
|
+
if (!initializer) return symbol.id;
|
|
61646
|
+
if (symbol.references.some((reference) => reference.flag !== "read" || isWithinAssignmentTarget(reference.identifier)) || !isAliasInitializedBeforeExecution(symbol.declarationNode, callExpression, index)) return null;
|
|
61647
|
+
const unwrappedInitializer = stripParenExpression(initializer);
|
|
61648
|
+
if (!isNodeOfType(unwrappedInitializer, "Identifier")) return symbol.id;
|
|
61649
|
+
const initializerSymbol = index.context.scopes.symbolFor(unwrappedInitializer);
|
|
61650
|
+
if (!initializerSymbol || !isAliasInitializedBeforeExecution(initializerSymbol.declarationNode, symbol.declarationNode, index)) return null;
|
|
61651
|
+
symbol = initializerSymbol;
|
|
61652
|
+
}
|
|
61653
|
+
return null;
|
|
61654
|
+
};
|
|
61655
|
+
const collectExactReadonlyCalleeCalls = (index) => {
|
|
61656
|
+
for (const callExpression of index.identifierCalls) {
|
|
61657
|
+
const calleeSymbolId = resolveExactReadonlyCalleeSymbolId(callExpression, index);
|
|
61658
|
+
if (calleeSymbolId !== null) addToSetIndex(index.callsByCalleeSymbolId, calleeSymbolId, callExpression);
|
|
61659
|
+
}
|
|
61660
|
+
};
|
|
61661
|
+
const canNodeReachNormalFunctionExit = (node, functionNode, index) => {
|
|
61662
|
+
const functionCfg = index.context.cfg.cfgFor(functionNode);
|
|
61663
|
+
const sourceBlock = functionCfg?.blockOf(node);
|
|
61664
|
+
if (!functionCfg || !sourceBlock) return false;
|
|
61665
|
+
const visitedBlocks = /* @__PURE__ */ new Set();
|
|
61666
|
+
const pendingBlocks = [sourceBlock];
|
|
61667
|
+
while (pendingBlocks.length > 0) {
|
|
61668
|
+
const block = pendingBlocks.pop();
|
|
61669
|
+
if (!block || visitedBlocks.has(block)) continue;
|
|
61670
|
+
visitedBlocks.add(block);
|
|
61671
|
+
for (const edge of block.successors) {
|
|
61672
|
+
if (edge.kind === "throw") continue;
|
|
61673
|
+
if (edge.to === functionCfg.exit) return true;
|
|
61674
|
+
pendingBlocks.push(edge.to);
|
|
61675
|
+
}
|
|
61676
|
+
}
|
|
61677
|
+
return false;
|
|
61678
|
+
};
|
|
61679
|
+
const canExecuteBeforeAsyncSuspension = (node, functionNode, index) => {
|
|
61680
|
+
if (!isFunctionLike$1(functionNode) || !functionNode.async) return isNodeReachableWithinFunction(node, index.context);
|
|
61681
|
+
const functionCfg = index.context.cfg.cfgFor(functionNode);
|
|
61682
|
+
const targetBlock = functionCfg?.blockOf(node);
|
|
61683
|
+
if (!functionCfg || !targetBlock) return false;
|
|
61684
|
+
const awaitsByBlock = /* @__PURE__ */ new Map();
|
|
61685
|
+
for (const awaitExpression of index.awaitExpressionsByOwner.get(functionNode) ?? []) {
|
|
61686
|
+
const awaitBlock = functionCfg.blockOf(awaitExpression);
|
|
61687
|
+
if (!awaitBlock) continue;
|
|
61688
|
+
const blockAwaits = awaitsByBlock.get(awaitBlock) ?? [];
|
|
61689
|
+
blockAwaits.push(awaitExpression);
|
|
61690
|
+
awaitsByBlock.set(awaitBlock, blockAwaits);
|
|
61691
|
+
}
|
|
61692
|
+
const visitedBlocks = /* @__PURE__ */ new Set();
|
|
61693
|
+
const pendingBlocks = [functionCfg.entry];
|
|
61694
|
+
const targetStart = getRangeStart(node);
|
|
61695
|
+
while (pendingBlocks.length > 0) {
|
|
61696
|
+
const block = pendingBlocks.pop();
|
|
61697
|
+
if (!block || visitedBlocks.has(block)) continue;
|
|
61698
|
+
visitedBlocks.add(block);
|
|
61699
|
+
const blockAwaits = awaitsByBlock.get(block) ?? [];
|
|
61700
|
+
if (block === targetBlock) return !blockAwaits.some((awaitExpression) => {
|
|
61701
|
+
if (isNodeOfType(awaitExpression, "AwaitExpression") && isDescendantWithoutFunctionBoundary(node, awaitExpression.argument)) return false;
|
|
61702
|
+
if (isDescendantWithoutFunctionBoundary(awaitExpression, node)) return true;
|
|
61703
|
+
const awaitStart = getRangeStart(awaitExpression);
|
|
61704
|
+
return awaitStart !== null && targetStart !== null && awaitStart < targetStart;
|
|
61705
|
+
});
|
|
61706
|
+
if (blockAwaits.length > 0) continue;
|
|
61707
|
+
for (const edge of block.successors) if (edge.kind !== "throw") pendingBlocks.push(edge.to);
|
|
61708
|
+
}
|
|
61709
|
+
return false;
|
|
61710
|
+
};
|
|
61711
|
+
const canReactBatchMutationAfterExecution = (executionNode, mutationNode, owner, index) => (isExclusiveIntrinsicReactEventHandler(owner, index) || index.effectCallbacks.has(owner)) && canNodeReachNode(executionNode, mutationNode, index) && canExecuteBeforeAsyncSuspension(mutationNode, owner, index) && canNodeReachNormalFunctionExit(mutationNode, owner, index);
|
|
61712
|
+
const functionMaySynchronouslyMutateLocation = (functionNode, index, visitingFunctions, cycleAffectedFunctions) => {
|
|
61713
|
+
const cachedResult = index.synchronousMutationResultByFunction.get(functionNode);
|
|
61714
|
+
if (cachedResult !== void 0) return cachedResult;
|
|
61715
|
+
if (!isFunctionLike$1(functionNode) || functionNode.generator) return false;
|
|
61716
|
+
if (visitingFunctions.has(functionNode)) {
|
|
61717
|
+
let didReachCycleEntry = false;
|
|
61718
|
+
for (const visitingFunction of visitingFunctions) {
|
|
61719
|
+
if (visitingFunction === functionNode) didReachCycleEntry = true;
|
|
61720
|
+
if (didReachCycleEntry) cycleAffectedFunctions.add(visitingFunction);
|
|
61721
|
+
}
|
|
61722
|
+
return false;
|
|
61723
|
+
}
|
|
61724
|
+
visitingFunctions.add(functionNode);
|
|
61725
|
+
const doesMutateSynchronously = [...collectLocationMutationExecutions(functionNode, index, visitingFunctions, cycleAffectedFunctions)].some((mutationExecution) => canExecuteBeforeAsyncSuspension(mutationExecution, functionNode, index) && canNodeReachNormalFunctionExit(mutationExecution, functionNode, index));
|
|
61726
|
+
visitingFunctions.delete(functionNode);
|
|
61727
|
+
if (doesMutateSynchronously || !cycleAffectedFunctions.has(functionNode)) index.synchronousMutationResultByFunction.set(functionNode, doesMutateSynchronously);
|
|
61728
|
+
return doesMutateSynchronously;
|
|
61729
|
+
};
|
|
61730
|
+
const collectLocationMutationExecutions = (functionNode, index, visitingFunctions = /* @__PURE__ */ new Set(), cycleAffectedFunctions = /* @__PURE__ */ new Set()) => {
|
|
61731
|
+
const cachedExecutions = index.mutationExecutionsByOwner.get(functionNode);
|
|
61732
|
+
if (cachedExecutions) return cachedExecutions;
|
|
61733
|
+
const mutationExecutions = new Set(index.historyMutationsByOwner.get(functionNode) ?? []);
|
|
61734
|
+
for (const expression of index.expressionsByOwner.get(functionNode) ?? []) {
|
|
61735
|
+
const calledFunction = index.calledFunctionByExpression.get(expression);
|
|
61736
|
+
if (calledFunction && functionMaySynchronouslyMutateLocation(calledFunction, index, visitingFunctions, cycleAffectedFunctions)) mutationExecutions.add(expression);
|
|
61737
|
+
for (const callbackFunction of index.synchronousCallbacksByExpression.get(expression) ?? []) if (functionMaySynchronouslyMutateLocation(callbackFunction, index, visitingFunctions, cycleAffectedFunctions)) mutationExecutions.add(expression);
|
|
61738
|
+
}
|
|
61739
|
+
if (!cycleAffectedFunctions.has(functionNode)) index.mutationExecutionsByOwner.set(functionNode, mutationExecutions);
|
|
61740
|
+
return mutationExecutions;
|
|
61741
|
+
};
|
|
61742
|
+
const isDefinitelyMatchingLocationListenerRemoval = (registration, removal) => registration.eventName === removal.eventName && registration.listenerFunction === removal.listenerFunction && registration.capture !== null && removal.capture !== null && registration.capture === removal.capture;
|
|
61743
|
+
const functionMustSynchronouslyRemoveLocationListener = (functionNode, registration, index, visitingFunctions) => {
|
|
61744
|
+
if (!isFunctionLike$1(functionNode) || functionNode.generator) return false;
|
|
61745
|
+
if (visitingFunctions.has(functionNode)) return false;
|
|
61746
|
+
const nextVisitingFunctions = new Set(visitingFunctions);
|
|
61747
|
+
nextVisitingFunctions.add(functionNode);
|
|
61748
|
+
const removalExecutions = [];
|
|
61749
|
+
for (const removal of index.listenerRemovals) {
|
|
61750
|
+
if (!isDefinitelyMatchingLocationListenerRemoval(registration, removal)) continue;
|
|
61751
|
+
if (index.context.cfg.enclosingFunction(removal.callExpression) !== functionNode) continue;
|
|
61752
|
+
if (canExecuteBeforeAsyncSuspension(removal.callExpression, functionNode, index)) removalExecutions.push(removal.callExpression);
|
|
61753
|
+
}
|
|
61754
|
+
for (const expression of index.expressionsByOwner.get(functionNode) ?? []) {
|
|
61755
|
+
const calledFunction = index.calledFunctionByExpression.get(expression);
|
|
61756
|
+
if (calledFunction && canExecuteBeforeAsyncSuspension(expression, functionNode, index) && functionMustSynchronouslyRemoveLocationListener(calledFunction, registration, index, nextVisitingFunctions)) removalExecutions.push(expression);
|
|
61757
|
+
}
|
|
61758
|
+
return doNodesCoverEveryPathFromFunctionEntry(functionNode, removalExecutions, index.context);
|
|
61759
|
+
};
|
|
61760
|
+
const collectSynchronousLocationListenerRemovalExecutions = (functionNode, registration, index) => {
|
|
61761
|
+
const removalExecutions = /* @__PURE__ */ new Set();
|
|
61762
|
+
for (const removal of index.listenerRemovals) if (isDefinitelyMatchingLocationListenerRemoval(registration, removal) && index.context.cfg.enclosingFunction(removal.callExpression) === functionNode && canExecuteBeforeAsyncSuspension(removal.callExpression, functionNode, index)) removalExecutions.add(removal.callExpression);
|
|
61763
|
+
for (const expression of index.expressionsByOwner.get(functionNode) ?? []) {
|
|
61764
|
+
const calledFunction = index.calledFunctionByExpression.get(expression);
|
|
61765
|
+
if (calledFunction && canExecuteBeforeAsyncSuspension(expression, functionNode, index) && functionMustSynchronouslyRemoveLocationListener(calledFunction, registration, index, /* @__PURE__ */ new Set())) removalExecutions.add(expression);
|
|
61766
|
+
}
|
|
61767
|
+
return removalExecutions;
|
|
61768
|
+
};
|
|
61769
|
+
const collectImpliedExpressionExecutionBoundaries = (node, owner) => {
|
|
61770
|
+
const expressionBoundaries = [];
|
|
61771
|
+
let currentChild = node;
|
|
61772
|
+
let currentParent = currentChild.parent ?? null;
|
|
61773
|
+
while (currentParent && currentParent !== owner) {
|
|
61774
|
+
if (isNodeOfType(currentParent, "ConditionalExpression") && currentParent.test === currentChild) {
|
|
61775
|
+
const staticTestValue = readStaticBoolean(getFinalSequenceExpressionValue(currentParent.test));
|
|
61776
|
+
if (staticTestValue !== null) expressionBoundaries.push(staticTestValue ? currentParent.consequent : currentParent.alternate);
|
|
61777
|
+
}
|
|
61778
|
+
if (isNodeOfType(currentParent, "LogicalExpression") && currentParent.left === currentChild) {
|
|
61779
|
+
const staticLeftValue = readStaticBoolean(getFinalSequenceExpressionValue(currentParent.left));
|
|
61780
|
+
if (currentParent.operator === "&&" && staticLeftValue === true || currentParent.operator === "||" && staticLeftValue === false) expressionBoundaries.push(currentParent.right);
|
|
61781
|
+
}
|
|
61782
|
+
if (isNodeOfType(currentParent, "ConditionalExpression") && (currentParent.consequent === currentChild || currentParent.alternate === currentChild) || isNodeOfType(currentParent, "LogicalExpression") && currentParent.right === currentChild || isNodeOfType(currentParent, "AssignmentPattern") && currentParent.right === currentChild) expressionBoundaries.push(currentChild);
|
|
61783
|
+
currentChild = currentParent;
|
|
61784
|
+
currentParent = currentChild.parent ?? null;
|
|
61785
|
+
}
|
|
61786
|
+
return expressionBoundaries;
|
|
61787
|
+
};
|
|
61788
|
+
const canExecutionReachFunctionExitWithoutListenerRemoval = (executionNode, registration, index) => {
|
|
61789
|
+
if (!isNodeReachableWithinFunction(executionNode, index.context)) return false;
|
|
61790
|
+
const owner = index.context.cfg.enclosingFunction(executionNode);
|
|
61791
|
+
if (!owner) return false;
|
|
61792
|
+
const functionCfg = index.context.cfg.cfgFor(owner);
|
|
61793
|
+
const sourceBlock = functionCfg?.blockOf(executionNode);
|
|
61794
|
+
if (!functionCfg || !sourceBlock) return false;
|
|
61795
|
+
const expressionBoundaries = collectImpliedExpressionExecutionBoundaries(executionNode, owner);
|
|
61796
|
+
const matchingRemovalsByBlock = /* @__PURE__ */ new Map();
|
|
61797
|
+
for (const removalExecution of collectSynchronousLocationListenerRemovalExecutions(owner, registration, index)) {
|
|
61798
|
+
const removalBlock = functionCfg.blockOf(removalExecution);
|
|
61799
|
+
if (!removalBlock) continue;
|
|
61800
|
+
const blockRemovals = matchingRemovalsByBlock.get(removalBlock) ?? [];
|
|
61801
|
+
blockRemovals.push(removalExecution);
|
|
61802
|
+
matchingRemovalsByBlock.set(removalBlock, blockRemovals);
|
|
61803
|
+
}
|
|
61804
|
+
const sourceStart = getRangeStart(executionNode);
|
|
61805
|
+
const hasRemovalAfterExecution = (block) => {
|
|
61806
|
+
const removals = (matchingRemovalsByBlock.get(block) ?? []).filter((removal) => {
|
|
61807
|
+
if (block !== sourceBlock) return true;
|
|
61808
|
+
const removalStart = getRangeStart(removal);
|
|
61809
|
+
return sourceStart !== null && removalStart !== null && sourceStart < removalStart;
|
|
61810
|
+
});
|
|
61811
|
+
if (collectExpressionPathCoverageNodes(owner, removals, index.context).size > 0) return true;
|
|
61812
|
+
if (block !== sourceBlock) return false;
|
|
61813
|
+
return expressionBoundaries.some((expressionBoundary) => collectExpressionPathCoverageNodes(owner, removals, index.context, expressionBoundary).size > 0);
|
|
61814
|
+
};
|
|
61815
|
+
const visitedBlocks = /* @__PURE__ */ new Set();
|
|
61816
|
+
const pendingBlocks = [sourceBlock];
|
|
61817
|
+
while (pendingBlocks.length > 0) {
|
|
61818
|
+
const block = pendingBlocks.pop();
|
|
61819
|
+
if (!block || visitedBlocks.has(block)) continue;
|
|
61820
|
+
visitedBlocks.add(block);
|
|
61821
|
+
if (hasRemovalAfterExecution(block)) continue;
|
|
61822
|
+
for (const edge of block.successors) {
|
|
61823
|
+
if (edge.kind === "throw") continue;
|
|
61824
|
+
if (edge.to === functionCfg.exit) return true;
|
|
61825
|
+
pendingBlocks.push(edge.to);
|
|
61826
|
+
}
|
|
61827
|
+
}
|
|
61828
|
+
return false;
|
|
61829
|
+
};
|
|
61830
|
+
const isListenerActiveAtMountedExit = (executionNode, registration, index, visitedExecutions = /* @__PURE__ */ new Set()) => {
|
|
61831
|
+
if (visitedExecutions.has(executionNode)) return false;
|
|
61832
|
+
if (!canExecutionReachFunctionExitWithoutListenerRemoval(executionNode, registration, index)) return false;
|
|
61833
|
+
const owner = index.context.cfg.enclosingFunction(executionNode);
|
|
61834
|
+
if (!owner) return false;
|
|
61835
|
+
if (owner === index.componentFunction || index.effectCallbacks.has(owner)) return true;
|
|
61836
|
+
const nextVisitedExecutions = new Set(visitedExecutions);
|
|
61837
|
+
nextVisitedExecutions.add(executionNode);
|
|
61838
|
+
for (const callSite of index.callSitesByFunction.get(owner) ?? []) if (isListenerActiveAtMountedExit(callSite, registration, index, nextVisitedExecutions)) return true;
|
|
61839
|
+
for (const invocation of index.synchronousInvocationsByFunction.get(owner) ?? []) if (isListenerActiveAtMountedExit(invocation, registration, index, nextVisitedExecutions)) return true;
|
|
61840
|
+
return false;
|
|
61841
|
+
};
|
|
61842
|
+
const collectMountedListenerFunctions = (index) => {
|
|
61843
|
+
for (const registration of index.listenerRegistrations) if (isListenerActiveAtMountedExit(registration.callExpression, registration, index)) index.mountedListenerFunctions.add(registration.listenerFunction);
|
|
61844
|
+
};
|
|
61845
|
+
const setterArgumentMutatesLocation = (setterCall, index) => {
|
|
61846
|
+
if (!isNodeOfType(setterCall, "CallExpression")) return false;
|
|
61847
|
+
return (setterCall.arguments ?? []).some((argument) => {
|
|
61848
|
+
if (isNodeOfType(argument, "SpreadElement")) return false;
|
|
61849
|
+
const updaterFunction = resolveExactLocalFunction(argument, index.context.scopes);
|
|
61850
|
+
return Boolean(isFunctionLike$1(updaterFunction) && functionMaySynchronouslyMutateLocation(updaterFunction, index, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()));
|
|
61851
|
+
});
|
|
61852
|
+
};
|
|
61853
|
+
const executionAnchorInvalidatesLocationSnapshot = (executionAnchor, index, visitedExecutions = /* @__PURE__ */ new Set()) => {
|
|
61854
|
+
if (visitedExecutions.has(executionAnchor)) return false;
|
|
61855
|
+
if (!isNodeReachableWithinFunction(executionAnchor, index.context)) return false;
|
|
61856
|
+
const owner = index.context.cfg.enclosingFunction(executionAnchor);
|
|
61857
|
+
if (!owner) return false;
|
|
61858
|
+
if (index.mountedListenerFunctions.has(owner)) return true;
|
|
61859
|
+
if (setterArgumentMutatesLocation(executionAnchor, index) || [...collectLocationMutationExecutions(owner, index)].some((mutationExecution) => canNodeReachNode(mutationExecution, executionAnchor, index) || canReactBatchMutationAfterExecution(executionAnchor, mutationExecution, owner, index))) return true;
|
|
61860
|
+
const nextVisitedExecutions = new Set(visitedExecutions);
|
|
61861
|
+
nextVisitedExecutions.add(executionAnchor);
|
|
61862
|
+
for (const callSite of index.callSitesByFunction.get(owner) ?? []) if (executionAnchorInvalidatesLocationSnapshot(callSite, index, nextVisitedExecutions)) return true;
|
|
61863
|
+
for (const invocation of index.synchronousInvocationsByFunction.get(owner) ?? []) if (executionAnchorInvalidatesLocationSnapshot(invocation, index, nextVisitedExecutions)) return true;
|
|
61864
|
+
return false;
|
|
61865
|
+
};
|
|
61866
|
+
const setterInvalidatesGlobalLocationSnapshot = (setterBindingIdentifier, index) => {
|
|
61867
|
+
const setterSymbol = index.context.scopes.symbolFor(setterBindingIdentifier);
|
|
61868
|
+
if (!setterSymbol) return false;
|
|
61869
|
+
return [...index.callsByCalleeSymbolId.get(setterSymbol.id) ?? []].some((setterCall) => executionAnchorInvalidatesLocationSnapshot(setterCall, index));
|
|
61870
|
+
};
|
|
61871
|
+
const createExternalLocationInvalidationChecker = ({ componentBody, componentFunction, context, directRenderNames, renderReachableExpressions }) => {
|
|
61872
|
+
if (!hasRenderReachableLocationSnapshotRead(componentBody, renderReachableExpressions, directRenderNames, context.scopes)) return () => false;
|
|
61873
|
+
const locationInvalidationIndex = buildLocationInvalidationIndex(componentBody, componentFunction, context);
|
|
61874
|
+
collectMountedListenerFunctions(locationInvalidationIndex);
|
|
61875
|
+
collectExactReadonlyCalleeCalls(locationInvalidationIndex);
|
|
61876
|
+
return (setterBindingIdentifier) => setterInvalidatesGlobalLocationSnapshot(setterBindingIdentifier, locationInvalidationIndex);
|
|
61877
|
+
};
|
|
61878
|
+
//#endregion
|
|
60138
61879
|
//#region src/plugin/rules/state-and-effects/rerender-state-only-in-handlers.ts
|
|
60139
61880
|
const isInsideConditionTest = (identifier, stopAt) => {
|
|
60140
61881
|
let current = identifier;
|
|
@@ -60256,7 +61997,8 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
60256
61997
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
60257
61998
|
const bindings = collectUseStateBindings(componentBody, context.scopes);
|
|
60258
61999
|
if (bindings.length === 0) return;
|
|
60259
|
-
|
|
62000
|
+
const renderReachableExpressions = collectRenderReachableExpressions(componentBody);
|
|
62001
|
+
if (renderReachableExpressions.length === 0) return;
|
|
60260
62002
|
const eventHandlerReferenceNames = collectFunctionLikeLocalNames(componentBody, context.scopes);
|
|
60261
62003
|
const dependencyGraph = buildLocalDependencyGraph(componentBody, eventHandlerReferenceNames);
|
|
60262
62004
|
const directRenderNames = collectRenderReachableNames(componentBody, context.scopes, eventHandlerReferenceNames);
|
|
@@ -60270,12 +62012,22 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
60270
62012
|
for (const effectInfo of effectInfos) for (const dependencyName of effectInfo.dependencyNames) if (!selfEchoValueNames.has(dependencyName)) effectConsumedNames.add(dependencyName);
|
|
60271
62013
|
for (const hookArgumentName of collectCustomHookArgumentNames(componentBody, context.scopes)) effectConsumedNames.add(hookArgumentName);
|
|
60272
62014
|
for (const reachableName of expandTransitiveDependencies(effectConsumedNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
62015
|
+
const componentFunction = context.cfg.enclosingFunction(componentBody);
|
|
62016
|
+
const doesSetterInvalidateExternalLocation = isFunctionLike$1(componentFunction) ? createExternalLocationInvalidationChecker({
|
|
62017
|
+
componentBody,
|
|
62018
|
+
componentFunction,
|
|
62019
|
+
context,
|
|
62020
|
+
directRenderNames,
|
|
62021
|
+
renderReachableExpressions
|
|
62022
|
+
}) : () => false;
|
|
60273
62023
|
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
60274
62024
|
walkAst(componentBody, (child) => {
|
|
60275
62025
|
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
|
|
60276
62026
|
});
|
|
60277
62027
|
for (const binding of bindings) {
|
|
60278
62028
|
if (renderReachableNames.has(binding.valueName)) continue;
|
|
62029
|
+
const setterBindingIdentifier = isNodeOfType(binding.declarator.id, "ArrayPattern") ? binding.declarator.id.elements?.[1] : null;
|
|
62030
|
+
if (setterBindingIdentifier && doesSetterInvalidateExternalLocation(setterBindingIdentifier)) continue;
|
|
60279
62031
|
if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
|
|
60280
62032
|
const setterSuffix = binding.setterName.slice(3);
|
|
60281
62033
|
if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
|
|
@@ -60828,6 +62580,7 @@ const rnListDataMapped = defineRule({
|
|
|
60828
62580
|
title: "List data rebuilt every render",
|
|
60829
62581
|
tags: ["test-noise"],
|
|
60830
62582
|
requires: ["react-native"],
|
|
62583
|
+
disabledWhen: ["react-compiler"],
|
|
60831
62584
|
severity: "warn",
|
|
60832
62585
|
recommendation: "This builds a new array each time the parent redraws, so every row redraws too. Wrap it in `useMemo(() => items.map(...), [items])` to keep the same array.",
|
|
60833
62586
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
@@ -63181,6 +64934,66 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
63181
64934
|
} })
|
|
63182
64935
|
});
|
|
63183
64936
|
//#endregion
|
|
64937
|
+
//#region src/plugin/constants/global-aria-properties.ts
|
|
64938
|
+
const GLOBAL_ARIA_PROPERTIES = new Set([
|
|
64939
|
+
"aria-atomic",
|
|
64940
|
+
"aria-braillelabel",
|
|
64941
|
+
"aria-brailleroledescription",
|
|
64942
|
+
"aria-busy",
|
|
64943
|
+
"aria-controls",
|
|
64944
|
+
"aria-current",
|
|
64945
|
+
"aria-describedby",
|
|
64946
|
+
"aria-description",
|
|
64947
|
+
"aria-details",
|
|
64948
|
+
"aria-disabled",
|
|
64949
|
+
"aria-dropeffect",
|
|
64950
|
+
"aria-errormessage",
|
|
64951
|
+
"aria-flowto",
|
|
64952
|
+
"aria-grabbed",
|
|
64953
|
+
"aria-haspopup",
|
|
64954
|
+
"aria-hidden",
|
|
64955
|
+
"aria-invalid",
|
|
64956
|
+
"aria-keyshortcuts",
|
|
64957
|
+
"aria-label",
|
|
64958
|
+
"aria-labelledby",
|
|
64959
|
+
"aria-live",
|
|
64960
|
+
"aria-owns",
|
|
64961
|
+
"aria-relevant",
|
|
64962
|
+
"aria-roledescription"
|
|
64963
|
+
]);
|
|
64964
|
+
//#endregion
|
|
64965
|
+
//#region src/plugin/constants/prohibited-aria-properties-by-role.ts
|
|
64966
|
+
const ACCESSIBLE_NAME_PROPERTIES = new Set([
|
|
64967
|
+
"aria-braillelabel",
|
|
64968
|
+
"aria-label",
|
|
64969
|
+
"aria-labelledby"
|
|
64970
|
+
]);
|
|
64971
|
+
const PROHIBITED_ARIA_PROPERTIES_BY_ROLE = {
|
|
64972
|
+
caption: ACCESSIBLE_NAME_PROPERTIES,
|
|
64973
|
+
code: ACCESSIBLE_NAME_PROPERTIES,
|
|
64974
|
+
definition: ACCESSIBLE_NAME_PROPERTIES,
|
|
64975
|
+
deletion: ACCESSIBLE_NAME_PROPERTIES,
|
|
64976
|
+
emphasis: ACCESSIBLE_NAME_PROPERTIES,
|
|
64977
|
+
generic: new Set([
|
|
64978
|
+
"aria-braillelabel",
|
|
64979
|
+
"aria-brailleroledescription",
|
|
64980
|
+
"aria-label",
|
|
64981
|
+
"aria-labelledby",
|
|
64982
|
+
"aria-roledescription"
|
|
64983
|
+
]),
|
|
64984
|
+
insertion: ACCESSIBLE_NAME_PROPERTIES,
|
|
64985
|
+
mark: ACCESSIBLE_NAME_PROPERTIES,
|
|
64986
|
+
none: ACCESSIBLE_NAME_PROPERTIES,
|
|
64987
|
+
paragraph: ACCESSIBLE_NAME_PROPERTIES,
|
|
64988
|
+
presentation: ACCESSIBLE_NAME_PROPERTIES,
|
|
64989
|
+
strong: ACCESSIBLE_NAME_PROPERTIES,
|
|
64990
|
+
subscript: ACCESSIBLE_NAME_PROPERTIES,
|
|
64991
|
+
superscript: ACCESSIBLE_NAME_PROPERTIES,
|
|
64992
|
+
term: ACCESSIBLE_NAME_PROPERTIES,
|
|
64993
|
+
time: ACCESSIBLE_NAME_PROPERTIES,
|
|
64994
|
+
tooltip: ACCESSIBLE_NAME_PROPERTIES
|
|
64995
|
+
};
|
|
64996
|
+
//#endregion
|
|
63184
64997
|
//#region src/plugin/constants/role-supports-aria-props.ts
|
|
63185
64998
|
const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
63186
64999
|
alert: new Set([
|
|
@@ -66281,6 +68094,12 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
66281
68094
|
])
|
|
66282
68095
|
};
|
|
66283
68096
|
//#endregion
|
|
68097
|
+
//#region src/plugin/utils/is-aria-property-supported-by-role.ts
|
|
68098
|
+
const isAriaPropertySupportedByRole = (role, property) => {
|
|
68099
|
+
if (PROHIBITED_ARIA_PROPERTIES_BY_ROLE[role]?.has(property)) return false;
|
|
68100
|
+
return ROLE_SUPPORTS_ARIA_PROPS[role]?.has(property) === true || GLOBAL_ARIA_PROPERTIES.has(property);
|
|
68101
|
+
};
|
|
68102
|
+
//#endregion
|
|
66284
68103
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
66285
68104
|
const buildMessageDefault = (roles, propName) => {
|
|
66286
68105
|
return `Screen reader users get no help from \`${propName}\` because role ${roles.map((role) => `\`${role}\``).join(" / ")} ignores it, so remove it or change the role.`;
|
|
@@ -66317,16 +68136,10 @@ const roleSupportsAriaProps = defineRule({
|
|
|
66317
68136
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
66318
68137
|
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType, context.scopes)].filter((role) => role !== null);
|
|
66319
68138
|
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
66320
|
-
const
|
|
66321
|
-
for (const role of roleCandidates) {
|
|
66322
|
-
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
66323
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
66324
|
-
if (!supported) return;
|
|
66325
|
-
supportedSets.push(supported);
|
|
66326
|
-
}
|
|
68139
|
+
for (const role of roleCandidates) if (!VALID_ARIA_ROLES.has(role)) return;
|
|
66327
68140
|
const isImplicit = !roleAttribute;
|
|
66328
68141
|
for (const { attribute, propName } of ariaAttributes) {
|
|
66329
|
-
if (
|
|
68142
|
+
if (roleCandidates.some((role) => isAriaPropertySupportedByRole(role, propName))) continue;
|
|
66330
68143
|
context.report({
|
|
66331
68144
|
node: attribute,
|
|
66332
68145
|
message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
|
|
@@ -71390,18 +73203,6 @@ const reactDoctorRules = [
|
|
|
71390
73203
|
requires: [...new Set(["react", ...jsxNoScriptUrl.requires ?? []])]
|
|
71391
73204
|
}
|
|
71392
73205
|
},
|
|
71393
|
-
{
|
|
71394
|
-
key: "react-doctor/jsx-no-target-blank",
|
|
71395
|
-
id: "jsx-no-target-blank",
|
|
71396
|
-
source: "react-doctor",
|
|
71397
|
-
originallyExternal: true,
|
|
71398
|
-
rule: {
|
|
71399
|
-
...jsxNoTargetBlank,
|
|
71400
|
-
framework: "global",
|
|
71401
|
-
category: "Security",
|
|
71402
|
-
requires: [...new Set(["react", ...jsxNoTargetBlank.requires ?? []])]
|
|
71403
|
-
}
|
|
71404
|
-
},
|
|
71405
73206
|
{
|
|
71406
73207
|
key: "react-doctor/jsx-no-undef",
|
|
71407
73208
|
id: "jsx-no-undef",
|
|
@@ -75108,7 +76909,7 @@ const EXTERNAL_RULES = [
|
|
|
75108
76909
|
{
|
|
75109
76910
|
key: "react-hooks-js/set-state-in-effect",
|
|
75110
76911
|
source: "react-compiler",
|
|
75111
|
-
severity: "
|
|
76912
|
+
severity: "warn"
|
|
75112
76913
|
},
|
|
75113
76914
|
{
|
|
75114
76915
|
key: "react-hooks-js/globals",
|