oxlint-plugin-react-doctor 0.7.6-dev.0150fc9 → 0.7.6-dev.0a8d30b

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.
Files changed (2) hide show
  1. package/dist/index.js +513 -63
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1180,8 +1180,8 @@ const stripParenExpression = (node) => {
1180
1180
  return current;
1181
1181
  };
1182
1182
  //#endregion
1183
- //#region src/plugin/utils/get-root-identifier-name.ts
1184
- const getRootIdentifierName = (node, options) => {
1183
+ //#region src/plugin/utils/get-root-identifier.ts
1184
+ const getRootIdentifier$1 = (node, options) => {
1185
1185
  if (!node) return null;
1186
1186
  const followCallChains = options?.followCallChains === true;
1187
1187
  let cursor = node;
@@ -1192,16 +1192,18 @@ const getRootIdentifierName = (node, options) => {
1192
1192
  continue;
1193
1193
  }
1194
1194
  if (followCallChains && isNodeOfType(cursor, "CallExpression")) {
1195
- const callee = cursor.callee;
1196
- if (!isNodeOfType(callee, "MemberExpression")) return null;
1197
- cursor = callee.object;
1195
+ if (!isNodeOfType(cursor.callee, "MemberExpression")) return null;
1196
+ cursor = cursor.callee.object;
1198
1197
  continue;
1199
1198
  }
1200
1199
  break;
1201
1200
  }
1202
- return isNodeOfType(cursor, "Identifier") ? cursor.name : null;
1201
+ return isNodeOfType(cursor, "Identifier") ? cursor : null;
1203
1202
  };
1204
1203
  //#endregion
1204
+ //#region src/plugin/utils/get-root-identifier-name.ts
1205
+ const getRootIdentifierName = (node, options) => getRootIdentifier$1(node, options)?.name ?? null;
1206
+ //#endregion
1205
1207
  //#region src/plugin/rules/state-and-effects/advanced-event-handler-refs.ts
1206
1208
  const STABLE_HANDLER_HOOK_NAMES = new Set([
1207
1209
  "useCallback",
@@ -5962,7 +5964,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
5962
5964
  //#endregion
5963
5965
  //#region src/plugin/utils/get-static-property-key-name.ts
5964
5966
  const getStaticPropertyKeyName = (node, options = {}) => {
5965
- if (!isNodeOfType(node, "Property")) return null;
5967
+ if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition")) return null;
5966
5968
  if (node.computed) {
5967
5969
  if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
5968
5970
  return null;
@@ -31959,6 +31961,26 @@ const noImgLazyWithHighFetchpriority = defineRule({
31959
31961
  } })
31960
31962
  });
31961
31963
  //#endregion
31964
+ //#region src/plugin/utils/react-ref-origin.ts
31965
+ const resolveReactRefSymbol = (memberExpression, scopes) => {
31966
+ const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
31967
+ if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticPropertyName(memberExpression) !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
31968
+ const symbol = resolveConstIdentifierAlias(receiver, scopes);
31969
+ if (!symbol?.initializer) return null;
31970
+ const initializer = stripParenExpression(symbol.initializer);
31971
+ if (!isNodeOfType(initializer, "CallExpression")) return null;
31972
+ return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
31973
+ };
31974
+ const hasReactRefCurrentOrigin = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
31975
+ const expression = stripParenExpression(node);
31976
+ if (resolveReactRefSymbol(expression, scopes)) return true;
31977
+ if (!isNodeOfType(expression, "Identifier")) return false;
31978
+ const symbol = resolveConstIdentifierAlias(expression, scopes);
31979
+ if (!symbol?.initializer || visitedSymbolIds.has(symbol.id)) return false;
31980
+ visitedSymbolIds.add(symbol.id);
31981
+ return hasReactRefCurrentOrigin(symbol.initializer, scopes, visitedSymbolIds);
31982
+ };
31983
+ //#endregion
31962
31984
  //#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
31963
31985
  const TIMER_FUNCTION_NAMES = new Set([
31964
31986
  "cancelAnimationFrame",
@@ -31990,6 +32012,33 @@ const NOTIFICATION_METHOD_NAMES = new Set([
31990
32012
  "success",
31991
32013
  "warning"
31992
32014
  ]);
32015
+ const NOTIFICATION_MODULE_SOURCES = new Set([
32016
+ "@chakra-ui/react",
32017
+ "@heroui/react",
32018
+ "@mantine/notifications",
32019
+ "antd",
32020
+ "react-hot-toast",
32021
+ "react-toastify",
32022
+ "sonner"
32023
+ ]);
32024
+ const SYNCHRONOUS_ARRAY_METHOD_NAMES = new Set([
32025
+ "every",
32026
+ "filter",
32027
+ "find",
32028
+ "findIndex",
32029
+ "flatMap",
32030
+ "forEach",
32031
+ "map",
32032
+ "reduce",
32033
+ "reduceRight",
32034
+ "some",
32035
+ "sort"
32036
+ ]);
32037
+ const isNotificationModuleSource = (source) => {
32038
+ if (!source) return false;
32039
+ for (const moduleSource of NOTIFICATION_MODULE_SOURCES) if (source === moduleSource || source.startsWith(`${moduleSource}/`)) return true;
32040
+ return /(?:^|[/_.-])(?:notification|toast)s?(?:$|[/_.-])/i.test(source);
32041
+ };
31993
32042
  const getMemberCall = (node) => {
31994
32043
  if (!isNodeOfType(node, "CallExpression")) return null;
31995
32044
  if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
@@ -32001,27 +32050,37 @@ const getMemberCall = (node) => {
32001
32050
  };
32002
32051
  const isNotificationReceiver = (receiver, scopes) => {
32003
32052
  if (!isNodeOfType(receiver, "Identifier")) return false;
32004
- if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
32053
+ const importBinding = getImportBindingForName(receiver, receiver.name);
32054
+ if (importBinding) return isNotificationModuleSource(importBinding.source) && (importBinding.exportedName === "default" || importBinding.exportedName !== null && NOTIFICATION_RECEIVER_NAMES.has(importBinding.exportedName));
32005
32055
  const symbol = scopes.symbolFor(receiver);
32006
- if (symbol?.kind === "import") return true;
32007
32056
  if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
32008
32057
  const callee = symbol.initializer.callee;
32009
- return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
32058
+ if (!isNodeOfType(callee, "Identifier")) return false;
32059
+ const hookImport = getImportBindingForName(callee, callee.name);
32060
+ return Boolean(hookImport && isNotificationModuleSource(hookImport.source) && hookImport.exportedName !== null && /^use(?:Message|Notification|Toast)$/.test(hookImport.exportedName));
32061
+ };
32062
+ const isKnownGlobalObject = (node, expectedName, scopes) => isNodeOfType(node, "Identifier") && node.name === expectedName && scopes.isGlobalReference(node);
32063
+ const isGlobalMember = (node, expectedPropertyName, scopes) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.property, "Identifier") && node.property.name === expectedPropertyName && (isKnownGlobalObject(node.object, "window", scopes) || isKnownGlobalObject(node.object, "globalThis", scopes));
32064
+ const isStorageReceiver = (node, scopes) => {
32065
+ if (isNodeOfType(node, "Identifier") && STORAGE_RECEIVER_NAMES.has(node.name) && scopes.isGlobalReference(node)) return true;
32066
+ for (const receiverName of STORAGE_RECEIVER_NAMES) if (isGlobalMember(node, receiverName, scopes)) return true;
32067
+ return false;
32010
32068
  };
32011
32069
  const getKnownImpureCall = (callExpression, scopes) => {
32012
32070
  if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
32071
+ if (isNodeOfType(callExpression.callee, "MemberExpression") && !callExpression.callee.computed && isNodeOfType(callExpression.callee.property, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.property.name) && (isKnownGlobalObject(callExpression.callee.object, "window", scopes) || isKnownGlobalObject(callExpression.callee.object, "globalThis", scopes))) return `${callExpression.callee.property.name}()`;
32013
32072
  const memberCall = getMemberCall(callExpression);
32014
32073
  if (!memberCall) return null;
32015
32074
  const { methodName, receiver } = memberCall;
32016
- if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
32017
- if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
32018
- if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
32075
+ if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isStorageReceiver(receiver, scopes)) return `${methodName}()`;
32076
+ if (EXTERNAL_READ_METHOD_NAMES.has(methodName) && hasReactRefCurrentOrigin(receiver, scopes)) return `.${methodName}()`;
32077
+ const receiverRoot = getRootIdentifier$1(receiver);
32078
+ if (EXTERNAL_READ_METHOD_NAMES.has(methodName) && receiverRoot !== null && isKnownGlobalObject(receiverRoot, "document", scopes)) return `.${methodName}()`;
32079
+ if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNotificationReceiver(receiver, scopes)) return `${methodName}()`;
32019
32080
  return null;
32020
32081
  };
32021
32082
  const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
32022
- let rootIdentifier = null;
32023
- if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
32024
- else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
32083
+ const rootIdentifier = getRootIdentifier$1(assignmentTarget);
32025
32084
  if (!rootIdentifier) return null;
32026
32085
  const updaterScope = scopes.ownScopeFor(updater);
32027
32086
  if (!updaterScope) return null;
@@ -32030,12 +32089,26 @@ const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) =>
32030
32089
  if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
32031
32090
  return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
32032
32091
  };
32092
+ const isArrayValue = (node, scopes) => {
32093
+ const expression = stripParenExpression(node);
32094
+ if (isNodeOfType(expression, "ArrayExpression")) return true;
32095
+ if (!isNodeOfType(expression, "Identifier")) return false;
32096
+ return isNodeOfType(resolveConstIdentifierAlias(expression, scopes)?.initializer, "ArrayExpression");
32097
+ };
32098
+ const isDefinitelySynchronousCallback = (callback, scopes) => {
32099
+ const parent = callback.parent;
32100
+ if (!parent) return false;
32101
+ if (isNodeOfType(parent, "CallExpression") && parent.callee === callback) return true;
32102
+ if (!isNodeOfType(parent, "CallExpression") || !parent.arguments?.some((argument) => argument === callback)) return false;
32103
+ if (isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ARRAY_METHOD_NAMES.has(parent.callee.property.name) && isArrayValue(parent.callee.object, scopes)) return true;
32104
+ return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.object, "Identifier") && parent.callee.object.name === "Array" && scopes.isGlobalReference(parent.callee.object) && isNodeOfType(parent.callee.property, "Identifier") && parent.callee.property.name === "from" && parent.arguments[1] === callback;
32105
+ };
32033
32106
  const findImpureUpdaterOperation = (updater, scopes) => {
32034
32107
  const analysis = getProgramAnalysis(updater);
32035
32108
  let operation = null;
32036
32109
  walkAst(updater, (child) => {
32037
32110
  if (operation) return false;
32038
- if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
32111
+ if (child !== updater && isFunctionLike$1(child) && !isDefinitelySynchronousCallback(child, scopes)) return false;
32039
32112
  if (isNodeOfType(child, "CallExpression")) {
32040
32113
  if (isNodeOfType(child.callee, "Identifier") && analysis) {
32041
32114
  const calleeReference = getRef(analysis, child.callee);
@@ -32067,18 +32140,25 @@ const noImpureStateUpdater = defineRule({
32067
32140
  severity: "error",
32068
32141
  recommendation: "Keep state updater callbacks pure and return only the next state. Move notifications, storage, timers, ref writes, and other external work into the event or effect that queues the update.",
32069
32142
  create: (context) => ({ CallExpression(node) {
32070
- const updater = node.arguments?.[0];
32071
- if (!updater || !isFunctionLike$1(updater)) return;
32143
+ const updaterArgument = node.arguments?.[0];
32144
+ if (!updaterArgument) return;
32072
32145
  const analysis = getProgramAnalysis(node);
32073
32146
  if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
32074
32147
  const calleeReference = getRef(analysis, node.callee);
32075
32148
  if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
32076
32149
  const stateDeclarator = getUseStateDecl(analysis, calleeReference);
32077
32150
  if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
32151
+ let updater = null;
32152
+ if (isFunctionLike$1(updaterArgument)) updater = updaterArgument;
32153
+ else if (isNodeOfType(updaterArgument, "Identifier")) {
32154
+ const updaterReference = getRef(analysis, updaterArgument);
32155
+ if (updaterReference) updater = resolveToFunction(updaterReference);
32156
+ }
32157
+ if (!updater) return;
32078
32158
  const operation = findImpureUpdaterOperation(updater, context.scopes);
32079
32159
  if (!operation) return;
32080
32160
  context.report({
32081
- node: updater,
32161
+ node: updaterArgument,
32082
32162
  message: `This state updater performs ${operation}. React may run updater functions more than once, so side effects here can repeat or observe inconsistent external state.`
32083
32163
  });
32084
32164
  } })
@@ -32923,6 +33003,18 @@ const INTL_FORMAT_METHOD_NAMES = new Set([
32923
33003
  "formatToParts",
32924
33004
  "formatRange"
32925
33005
  ]);
33006
+ const ABSENT_PROPERTY_PROOF = { status: "absent" };
33007
+ const PRESENT_PROPERTY_PROOF = { status: "present" };
33008
+ const UNDEFINED_PROPERTY_PROOF = { status: "undefined" };
33009
+ const UNKNOWN_PROPERTY_PROOF = { status: "unknown" };
33010
+ const READ_ONLY_OBJECT_METHOD_NAMES = new Set([
33011
+ "hasOwnProperty",
33012
+ "isPrototypeOf",
33013
+ "propertyIsEnumerable",
33014
+ "toLocaleString",
33015
+ "toString",
33016
+ "valueOf"
33017
+ ]);
32926
33018
  const isProvableDateExpression = (expression) => {
32927
33019
  if (!expression) return false;
32928
33020
  const unwrapped = stripParenExpression(expression);
@@ -32937,32 +33029,290 @@ const receiverNameLooksDateFlavored = (expression) => {
32937
33029
  if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
32938
33030
  return false;
32939
33031
  };
32940
- const objectLiteralHasProperty = (objectExpression, propertyName) => {
32941
- if (!objectExpression) return false;
32942
- const unwrapped = stripParenExpression(objectExpression);
32943
- if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
32944
- for (const property of unwrapped.properties ?? []) {
32945
- if (!isNodeOfType(property, "Property")) continue;
32946
- if (property.computed) continue;
32947
- if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
32948
- if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
33032
+ const isStaticUndefined = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
33033
+ const expression = stripParenExpression(node);
33034
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return true;
33035
+ if (!isNodeOfType(expression, "Identifier")) return false;
33036
+ if (expression.name === "undefined" && scopes.isGlobalReference(expression)) return true;
33037
+ const symbol = scopes.symbolFor(expression);
33038
+ if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || visitedSymbolIds.has(symbol.id)) return false;
33039
+ visitedSymbolIds.add(symbol.id);
33040
+ return isStaticUndefined(symbol.initializer, scopes, visitedSymbolIds);
33041
+ };
33042
+ const isNestedAssignmentTarget = (expression) => {
33043
+ let target = expression;
33044
+ let parent = target.parent;
33045
+ while (parent) {
33046
+ if (isNodeOfType(parent, "AssignmentExpression")) return parent.left === target;
33047
+ if (isNodeOfType(parent, "ForInStatement") || isNodeOfType(parent, "ForOfStatement")) return parent.left === target;
33048
+ if (isNodeOfType(parent, "AssignmentPattern")) {
33049
+ if (parent.left !== target) return false;
33050
+ } else if (isNodeOfType(parent, "RestElement")) {
33051
+ if (parent.argument !== target) return false;
33052
+ } else if (isNodeOfType(parent, "ArrayPattern")) {
33053
+ if (!parent.elements?.some((element) => element === target)) return false;
33054
+ } else if (isNodeOfType(parent, "Property")) {
33055
+ if (parent.value !== target || !isNodeOfType(parent.parent, "ObjectPattern")) return false;
33056
+ } else if (isNodeOfType(parent, "ObjectPattern")) {
33057
+ if (!parent.properties?.some((property) => property === target)) return false;
33058
+ } else return false;
33059
+ target = parent;
33060
+ parent = target.parent;
33061
+ }
33062
+ return false;
33063
+ };
33064
+ const isPotentialMutationReference = (identifier, readNode) => {
33065
+ let expression = identifier;
33066
+ let parent = expression.parent;
33067
+ while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === expression) {
33068
+ expression = parent;
33069
+ parent = expression.parent;
33070
+ }
33071
+ const rootExpression = expression;
33072
+ let memberDepth = 0;
33073
+ while (parent && isNodeOfType(parent, "MemberExpression") && parent.object === expression) {
33074
+ memberDepth += 1;
33075
+ expression = parent;
33076
+ parent = expression.parent;
33077
+ while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === expression) {
33078
+ expression = parent;
33079
+ parent = expression.parent;
33080
+ }
33081
+ }
33082
+ if (!parent || isAstDescendant(identifier, readNode)) return false;
33083
+ if (isNestedAssignmentTarget(expression)) return true;
33084
+ if (isNodeOfType(parent, "AssignmentExpression") && parent.left === expression) return true;
33085
+ if (isNodeOfType(parent, "UpdateExpression") && parent.argument === expression) return true;
33086
+ if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === expression) return true;
33087
+ if (isNodeOfType(parent, "CallExpression")) {
33088
+ if (parent.callee === expression) {
33089
+ if (memberDepth === 0) return true;
33090
+ const memberExpression = stripParenExpression(expression);
33091
+ return memberDepth === 1 && isNodeOfType(memberExpression, "MemberExpression") && !READ_ONLY_OBJECT_METHOD_NAMES.has(getStaticPropertyName(memberExpression) ?? "");
33092
+ }
33093
+ return memberDepth === 0 && parent.arguments?.some((argument) => argument === rootExpression);
32949
33094
  }
33095
+ if (isNodeOfType(parent, "NewExpression")) return memberDepth === 0 && parent.arguments?.some((argument) => argument === rootExpression);
33096
+ if (isNodeOfType(parent, "AssignmentExpression") && parent.right === rootExpression) return true;
32950
33097
  return false;
32951
33098
  };
32952
- const hasExplicitLocaleArgument = (argument) => {
33099
+ const getSimpleAlias = (identifier, scopes) => {
33100
+ let expression = identifier;
33101
+ let parent = expression.parent;
33102
+ while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === expression) {
33103
+ expression = parent;
33104
+ parent = expression.parent;
33105
+ }
33106
+ if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== expression || !isNodeOfType(parent.id, "Identifier")) return null;
33107
+ const symbol = scopes.symbolFor(parent.id);
33108
+ return symbol ? {
33109
+ symbol,
33110
+ readNode: parent.id
33111
+ } : null;
33112
+ };
33113
+ const getDirectCallForExpression = (expression) => {
33114
+ let callee = expression;
33115
+ let parent = callee.parent;
33116
+ while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === callee) {
33117
+ callee = parent;
33118
+ parent = callee.parent;
33119
+ }
33120
+ return isNodeOfType(parent, "CallExpression") && parent.callee === callee ? parent : null;
33121
+ };
33122
+ const getMethodOwner = (functionNode, scopes) => {
33123
+ const methodNode = functionNode.parent;
33124
+ if (isNodeOfType(methodNode, "Property") && methodNode.value === functionNode && isNodeOfType(methodNode.parent, "ObjectExpression")) {
33125
+ const objectParent = methodNode.parent.parent;
33126
+ if (!isNodeOfType(objectParent, "VariableDeclarator") || objectParent.init !== methodNode.parent || !isNodeOfType(objectParent.id, "Identifier")) return null;
33127
+ const ownerSymbol = scopes.symbolFor(objectParent.id);
33128
+ const methodName = getStaticPropertyKeyName(methodNode, { allowComputedString: true });
33129
+ return ownerSymbol && methodName ? {
33130
+ methodName,
33131
+ ownerKind: "object",
33132
+ ownerSymbol,
33133
+ isStatic: false
33134
+ } : null;
33135
+ }
33136
+ if (!isNodeOfType(methodNode, "MethodDefinition") || methodNode.value !== functionNode || !isNodeOfType(methodNode.parent, "ClassBody")) return null;
33137
+ const classNode = methodNode.parent.parent;
33138
+ if (!isNodeOfType(classNode, "ClassDeclaration") && !isNodeOfType(classNode, "ClassExpression")) return null;
33139
+ let bindingIdentifier = isNodeOfType(classNode.id, "Identifier") ? classNode.id : null;
33140
+ if (!bindingIdentifier && isNodeOfType(classNode.parent, "VariableDeclarator")) bindingIdentifier = isNodeOfType(classNode.parent.id, "Identifier") ? classNode.parent.id : null;
33141
+ if (!bindingIdentifier) return null;
33142
+ const ownerSymbol = scopes.symbolFor(bindingIdentifier);
33143
+ const methodName = getStaticPropertyKeyName(methodNode, { allowComputedString: true });
33144
+ return ownerSymbol && methodName ? {
33145
+ methodName,
33146
+ ownerKind: "class",
33147
+ ownerSymbol,
33148
+ isStatic: Boolean(methodNode.static)
33149
+ } : null;
33150
+ };
33151
+ const doesSymbolResolveToOwner = (symbol, ownerSymbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
33152
+ if (!symbol) return false;
33153
+ if (symbol.declarationNode === ownerSymbol.declarationNode) return true;
33154
+ if (symbol.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return false;
33155
+ visitedSymbolIds.add(symbol.id);
33156
+ const initializer = stripParenExpression(symbol.initializer);
33157
+ return isNodeOfType(initializer, "Identifier") && doesSymbolResolveToOwner(scopes.symbolFor(initializer), ownerSymbol, scopes, visitedSymbolIds);
33158
+ };
33159
+ const isClassInstanceExpression = (expression, ownerSymbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
33160
+ const candidate = stripParenExpression(expression);
33161
+ if (isNodeOfType(candidate, "NewExpression") && isNodeOfType(candidate.callee, "Identifier")) return doesSymbolResolveToOwner(scopes.symbolFor(candidate.callee), ownerSymbol, scopes);
33162
+ if (!isNodeOfType(candidate, "Identifier")) return false;
33163
+ const symbol = scopes.symbolFor(candidate);
33164
+ if (symbol?.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return false;
33165
+ visitedSymbolIds.add(symbol.id);
33166
+ return isClassInstanceExpression(symbol.initializer, ownerSymbol, scopes, visitedSymbolIds);
33167
+ };
33168
+ const getMethodCalls = (functionNode, scopes) => {
33169
+ const owner = getMethodOwner(functionNode, scopes);
33170
+ if (!owner) return [];
33171
+ const calls = [];
33172
+ walkAst(scopes.rootScope.node, (child) => {
33173
+ if (!isNodeOfType(child, "MemberExpression")) return;
33174
+ if (getStaticPropertyName(child) !== owner.methodName) return;
33175
+ const receiver = stripParenExpression(child.object);
33176
+ let doesReceiverMatch = isNodeOfType(receiver, "Identifier") && doesSymbolResolveToOwner(scopes.symbolFor(receiver), owner.ownerSymbol, scopes);
33177
+ if (owner.ownerKind === "class" && !owner.isStatic) doesReceiverMatch = isClassInstanceExpression(receiver, owner.ownerSymbol, scopes);
33178
+ if (!doesReceiverMatch) return;
33179
+ const call = getDirectCallForExpression(child);
33180
+ if (call) calls.push(call);
33181
+ });
33182
+ return calls;
33183
+ };
33184
+ const isFunctionInvokedBeforeUsage = (functionNode, usageNode, usageBoundary, scopes, visitedSymbolIds, visitedFunctionNodes = /* @__PURE__ */ new Set()) => {
33185
+ if (visitedFunctionNodes.has(functionNode)) return false;
33186
+ visitedFunctionNodes.add(functionNode);
33187
+ const usageFunction = findEnclosingFunction(usageNode);
33188
+ const immediateCall = getDirectCallForExpression(functionNode);
33189
+ if (immediateCall) {
33190
+ const immediateCallFunction = findEnclosingFunction(immediateCall);
33191
+ if (immediateCallFunction === usageFunction) {
33192
+ const immediateCallStart = getRangeStart(immediateCall);
33193
+ return immediateCallStart === null || immediateCallStart < usageBoundary;
33194
+ }
33195
+ if (!immediateCallFunction) return usageFunction !== null;
33196
+ return isFunctionInvokedBeforeUsage(immediateCallFunction, usageNode, usageBoundary, scopes, visitedSymbolIds, new Set(visitedFunctionNodes));
33197
+ }
33198
+ for (const methodCall of getMethodCalls(functionNode, scopes)) {
33199
+ const methodCallFunction = findEnclosingFunction(methodCall);
33200
+ if (methodCallFunction === usageFunction) {
33201
+ const methodCallStart = getRangeStart(methodCall);
33202
+ if (methodCallStart === null || methodCallStart < usageBoundary) return true;
33203
+ continue;
33204
+ }
33205
+ if (!methodCallFunction) {
33206
+ if (usageFunction) return true;
33207
+ continue;
33208
+ }
33209
+ if (isFunctionInvokedBeforeUsage(methodCallFunction, usageNode, usageBoundary, scopes, new Set(visitedSymbolIds), new Set(visitedFunctionNodes))) return true;
33210
+ }
33211
+ const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
33212
+ if (!bindingIdentifier) return false;
33213
+ const symbol = scopes.symbolFor(bindingIdentifier);
33214
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
33215
+ visitedSymbolIds.add(symbol.id);
33216
+ let wasInvokedBeforeUsage = false;
33217
+ walkAst(scopes.rootScope.node, (child) => {
33218
+ if (wasInvokedBeforeUsage || !isNodeOfType(child, "Identifier")) return;
33219
+ if (scopes.symbolFor(child)?.declarationNode !== symbol.declarationNode) return;
33220
+ const call = getDirectCallForExpression(child);
33221
+ if (!call) return false;
33222
+ const callFunction = findEnclosingFunction(call);
33223
+ if (callFunction === usageFunction) {
33224
+ const callStart = getRangeStart(call);
33225
+ wasInvokedBeforeUsage = callStart === null || callStart < usageBoundary;
33226
+ return;
33227
+ }
33228
+ if (!callFunction) {
33229
+ wasInvokedBeforeUsage = usageFunction !== null;
33230
+ return;
33231
+ }
33232
+ wasInvokedBeforeUsage = isFunctionInvokedBeforeUsage(callFunction, usageNode, usageBoundary, scopes, new Set(visitedSymbolIds), new Set(visitedFunctionNodes));
33233
+ });
33234
+ return wasInvokedBeforeUsage;
33235
+ };
33236
+ const getMutationUsageBoundary = (usageNode, readNode) => {
33237
+ const readStart = getRangeStart(readNode);
33238
+ let readExpression = readNode;
33239
+ let readParent = readExpression.parent;
33240
+ while (readParent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(readParent.type) && "expression" in readParent && readParent.expression === readExpression) {
33241
+ readExpression = readParent;
33242
+ readParent = readExpression.parent;
33243
+ }
33244
+ if ((isNodeOfType(usageNode, "CallExpression") || isNodeOfType(usageNode, "NewExpression")) && usageNode.arguments?.some((argument) => argument === readExpression)) return usageNode.range?.[1] ?? null;
33245
+ if (isAstDescendant(readNode, usageNode)) return readStart;
33246
+ return getRangeStart(usageNode);
33247
+ };
33248
+ const wasMutatedBeforeUsage = (symbol, usageNode, readNode, scopes, visitedMutationSymbolIds = /* @__PURE__ */ new Set(), inheritedUsageBoundary, readNodesBySymbolId = /* @__PURE__ */ new Map()) => {
33249
+ if (visitedMutationSymbolIds.has(symbol.id)) return false;
33250
+ visitedMutationSymbolIds.add(symbol.id);
33251
+ const usageBoundary = inheritedUsageBoundary === void 0 ? getMutationUsageBoundary(usageNode, readNode) : inheritedUsageBoundary;
33252
+ if (typeof usageBoundary !== "number") return true;
33253
+ const usageFunction = findEnclosingFunction(usageNode);
33254
+ return symbol.references.some((reference) => {
33255
+ const referenceStart = getRangeStart(reference.identifier);
33256
+ const simpleAlias = getSimpleAlias(reference.identifier, scopes);
33257
+ if (simpleAlias) return wasMutatedBeforeUsage(simpleAlias.symbol, usageNode, readNodesBySymbolId.get(simpleAlias.symbol.id) ?? simpleAlias.readNode, scopes, new Set(visitedMutationSymbolIds), usageBoundary, readNodesBySymbolId);
33258
+ if (!isPotentialMutationReference(reference.identifier, readNode)) return false;
33259
+ if (referenceStart === null) return true;
33260
+ const mutationFunction = findEnclosingFunction(reference.identifier);
33261
+ if (mutationFunction === usageFunction) return referenceStart < usageBoundary;
33262
+ if (!mutationFunction) return usageFunction !== null;
33263
+ if (usageFunction && isAstDescendant(usageFunction, mutationFunction)) return true;
33264
+ return isFunctionInvokedBeforeUsage(mutationFunction, usageNode, usageBoundary, scopes, /* @__PURE__ */ new Set());
33265
+ });
33266
+ };
33267
+ const getObjectPropertyProof = (objectExpression, propertyName, scopes, usageNode, visitedSymbolIds = /* @__PURE__ */ new Set(), inheritedUsageBoundary, readNodesBySymbolId = /* @__PURE__ */ new Map()) => {
33268
+ if (!objectExpression) return ABSENT_PROPERTY_PROOF;
33269
+ const unwrapped = stripParenExpression(objectExpression);
33270
+ if (isNodeOfType(unwrapped, "Literal") || isStaticUndefined(unwrapped, scopes)) return ABSENT_PROPERTY_PROOF;
33271
+ if (isNodeOfType(unwrapped, "Identifier")) {
33272
+ const symbol = scopes.symbolFor(unwrapped);
33273
+ const nextReadNodesBySymbolId = new Map(readNodesBySymbolId);
33274
+ if (symbol) nextReadNodesBySymbolId.set(symbol.id, unwrapped);
33275
+ const usageBoundary = inheritedUsageBoundary === void 0 ? getMutationUsageBoundary(usageNode, unwrapped) : inheritedUsageBoundary;
33276
+ if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || visitedSymbolIds.has(symbol.id) || wasMutatedBeforeUsage(symbol, usageNode, unwrapped, scopes, /* @__PURE__ */ new Set(), usageBoundary, nextReadNodesBySymbolId)) return UNKNOWN_PROPERTY_PROOF;
33277
+ visitedSymbolIds.add(symbol.id);
33278
+ return getObjectPropertyProof(symbol.initializer, propertyName, scopes, usageNode, visitedSymbolIds, usageBoundary, nextReadNodesBySymbolId);
33279
+ }
33280
+ if (isNodeOfType(unwrapped, "ConditionalExpression")) {
33281
+ const consequent = getObjectPropertyProof(unwrapped.consequent, propertyName, scopes, usageNode, new Set(visitedSymbolIds), inheritedUsageBoundary, new Map(readNodesBySymbolId));
33282
+ const alternate = getObjectPropertyProof(unwrapped.alternate, propertyName, scopes, usageNode, new Set(visitedSymbolIds), inheritedUsageBoundary, new Map(readNodesBySymbolId));
33283
+ return consequent.status === alternate.status ? consequent : UNKNOWN_PROPERTY_PROOF;
33284
+ }
33285
+ if (isNodeOfType(unwrapped, "CallExpression") && isNodeOfType(unwrapped.callee, "MemberExpression") && !unwrapped.callee.computed && isNodeOfType(unwrapped.callee.object, "Identifier") && unwrapped.callee.object.name === "Object" && scopes.isGlobalReference(unwrapped.callee.object) && isNodeOfType(unwrapped.callee.property, "Identifier") && unwrapped.callee.property.name === "freeze") return getObjectPropertyProof(unwrapped.arguments?.[0], propertyName, scopes, usageNode, visitedSymbolIds, inheritedUsageBoundary, readNodesBySymbolId);
33286
+ if (!isNodeOfType(unwrapped, "ObjectExpression")) return UNKNOWN_PROPERTY_PROOF;
33287
+ const properties = unwrapped.properties ?? [];
33288
+ for (let propertyIndex = properties.length - 1; propertyIndex >= 0; propertyIndex -= 1) {
33289
+ const property = properties[propertyIndex];
33290
+ if (!property) continue;
33291
+ if (isNodeOfType(property, "SpreadElement")) {
33292
+ const spreadProof = getObjectPropertyProof(property.argument, propertyName, scopes, unwrapped, new Set(visitedSymbolIds), void 0);
33293
+ if (spreadProof.status !== "absent") return spreadProof;
33294
+ continue;
33295
+ }
33296
+ if (!isNodeOfType(property, "Property") || getStaticPropertyKeyName(property) !== propertyName) continue;
33297
+ return isStaticUndefined(property.value, scopes) ? UNDEFINED_PROPERTY_PROOF : PRESENT_PROPERTY_PROOF;
33298
+ }
33299
+ return ABSENT_PROPERTY_PROOF;
33300
+ };
33301
+ const objectHasExplicitProperty = (objectExpression, propertyName, scopes, usageNode) => getObjectPropertyProof(objectExpression, propertyName, scopes, usageNode).status === "present";
33302
+ const hasExplicitLocaleArgument = (argument, scopes) => {
32953
33303
  if (!argument) return false;
32954
33304
  const unwrapped = stripParenExpression(argument);
32955
- if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
33305
+ if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined" && scopes.isGlobalReference(unwrapped) || isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "void") return false;
32956
33306
  return true;
32957
33307
  };
32958
- const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
33308
+ const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate, scopes) => {
32959
33309
  const localeArgument = call.arguments?.[0];
32960
- if (!hasExplicitLocaleArgument(localeArgument)) return false;
33310
+ if (!hasExplicitLocaleArgument(localeArgument, scopes)) return false;
32961
33311
  const optionsArgument = call.arguments?.[1];
32962
- if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
33312
+ if (objectHasExplicitProperty(optionsArgument, "timeZone", scopes, call)) return true;
32963
33313
  return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
32964
33314
  };
32965
- const matchLocaleMethodCall = (call) => {
33315
+ const matchLocaleMethodCall = (call, scopes) => {
32966
33316
  const callee = call.callee;
32967
33317
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
32968
33318
  if (!isNodeOfType(callee.property, "Identifier")) return null;
@@ -32970,7 +33320,7 @@ const matchLocaleMethodCall = (call) => {
32970
33320
  if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
32971
33321
  const receiverIsProvablyDate = isProvableDateExpression(callee.object);
32972
33322
  if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
32973
- if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
33323
+ if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate, scopes)) return null;
32974
33324
  return {
32975
33325
  node: call,
32976
33326
  display: `${methodName}()`
@@ -32986,13 +33336,13 @@ const getIntlFormatterName = (expression) => {
32986
33336
  if (!isNodeOfType(callee.property, "Identifier")) return null;
32987
33337
  return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
32988
33338
  };
32989
- const isDeterministicIntlConstruction = (construction, formatterName) => {
33339
+ const isDeterministicIntlConstruction = (construction, formatterName, scopes) => {
32990
33340
  if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
32991
- if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
33341
+ if (!hasExplicitLocaleArgument(construction.arguments?.[0], scopes)) return false;
32992
33342
  if (formatterName !== "DateTimeFormat") return true;
32993
- return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
33343
+ return objectHasExplicitProperty(construction.arguments?.[1], "timeZone", scopes, construction);
32994
33344
  };
32995
- const matchIntlFormatCall = (call) => {
33345
+ const matchIntlFormatCall = (call, scopes) => {
32996
33346
  const callee = call.callee;
32997
33347
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
32998
33348
  if (!isNodeOfType(callee.property, "Identifier")) return null;
@@ -33005,7 +33355,7 @@ const matchIntlFormatCall = (call) => {
33005
33355
  if (!construction) return null;
33006
33356
  const formatterName = getIntlFormatterName(construction);
33007
33357
  if (!formatterName) return null;
33008
- if (isDeterministicIntlConstruction(construction, formatterName)) return null;
33358
+ if (isDeterministicIntlConstruction(construction, formatterName, scopes)) return null;
33009
33359
  return {
33010
33360
  node: call,
33011
33361
  display: `Intl.${formatterName}().${callee.property.name}()`
@@ -33074,7 +33424,7 @@ const noLocaleFormatInRender = defineRule({
33074
33424
  fileIsEmailTemplate = hasEmailTemplateImport(node);
33075
33425
  },
33076
33426
  CallExpression(node) {
33077
- const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
33427
+ const match = matchLocaleMethodCall(node, context.scopes) ?? matchIntlFormatCall(node, context.scopes) ?? matchDateDefaultStringification(node);
33078
33428
  if (match) reportIfRenderPhase(match);
33079
33429
  },
33080
33430
  TemplateLiteral(node) {
@@ -33094,7 +33444,7 @@ const noLocaleFormatInRender = defineRule({
33094
33444
  walkAst(helperNode.body ?? helperNode, (child) => {
33095
33445
  if (isFunctionLike$1(child)) return false;
33096
33446
  if (!isNodeOfType(child, "CallExpression")) return;
33097
- const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
33447
+ const match = matchLocaleMethodCall(child, context.scopes) ?? matchIntlFormatCall(child, context.scopes);
33098
33448
  if (!match || reportedNodes.has(match.node)) return;
33099
33449
  if (fileIsEmailTemplate) return;
33100
33450
  if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
@@ -36920,28 +37270,85 @@ const noRedundantShouldComponentUpdate = defineRule({
36920
37270
  });
36921
37271
  //#endregion
36922
37272
  //#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
36923
- const resolveReactRefSymbol = (memberExpression, scopes) => {
36924
- const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
36925
- if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
36926
- const symbol = resolveConstIdentifierAlias(receiver, scopes);
36927
- if (!symbol?.initializer) return null;
36928
- const initializer = stripParenExpression(symbol.initializer);
36929
- if (!isNodeOfType(initializer, "CallExpression")) return null;
36930
- return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
36931
- };
37273
+ const REPEATED_ANCESTOR_TYPES = new Set([
37274
+ "DoWhileStatement",
37275
+ "ForInStatement",
37276
+ "ForOfStatement",
37277
+ "ForStatement",
37278
+ "WhileStatement"
37279
+ ]);
36932
37280
  const isSameRefCurrentMember = (node, refSymbol, scopes) => {
36933
- if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
37281
+ if (!isNodeOfType(node, "MemberExpression") || getStaticPropertyName(node) !== "current") return false;
36934
37282
  const receiver = stripParenExpression(node.object);
36935
37283
  return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
36936
37284
  };
37285
+ const isSameRefCurrentAlias = (node, refSymbol, scopes) => {
37286
+ if (isSameRefCurrentMember(node, refSymbol, scopes)) return true;
37287
+ if (!isNodeOfType(node, "Identifier")) return false;
37288
+ const aliasSymbol = scopes.symbolFor(node);
37289
+ return aliasSymbol?.kind === "const" && aliasSymbol.initializer !== null && isSameRefCurrentMember(stripParenExpression(aliasSymbol.initializer), refSymbol, scopes);
37290
+ };
37291
+ const isEmptySentinel = (node, scopes) => isNodeOfType(node, "Literal") && node.value === null || isNodeOfType(node, "Identifier") && node.name === "undefined" && scopes.isGlobalReference(node);
37292
+ const hasRepeatedExecutionAncestor = (node, stop) => {
37293
+ let ancestor = node.parent;
37294
+ while (ancestor && ancestor !== stop) {
37295
+ if (isFunctionLike$1(ancestor) || REPEATED_ANCESTOR_TYPES.has(ancestor.type)) return true;
37296
+ ancestor = ancestor.parent;
37297
+ }
37298
+ return ancestor !== stop;
37299
+ };
37300
+ const getBranchConstraints = (node, branchRoot) => {
37301
+ const constraints = /* @__PURE__ */ new Map();
37302
+ let descendant = node;
37303
+ let ancestor = descendant.parent;
37304
+ while (ancestor && descendant !== branchRoot) {
37305
+ if (isNodeOfType(ancestor, "IfStatement")) {
37306
+ if (ancestor.consequent === descendant) constraints.set(ancestor, true);
37307
+ if (ancestor.alternate === descendant) constraints.set(ancestor, false);
37308
+ }
37309
+ descendant = ancestor;
37310
+ ancestor = ancestor.parent;
37311
+ }
37312
+ return constraints;
37313
+ };
37314
+ const canExecuteTogether = (firstConstraints, secondConstraints) => {
37315
+ for (const [statement, branch] of firstConstraints) {
37316
+ const otherBranch = secondConstraints.get(statement);
37317
+ if (otherBranch !== void 0 && otherBranch !== branch) return false;
37318
+ }
37319
+ return true;
37320
+ };
37321
+ const hasNoPriorCoExecutableWrite = (assignmentExpression, branchRoot, refSymbol, scopes) => {
37322
+ const assignmentConstraints = getBranchConstraints(assignmentExpression, branchRoot);
37323
+ const assignmentStart = getRangeStart(assignmentExpression);
37324
+ let hasCoExecutableWrite = false;
37325
+ walkAst(branchRoot, (child) => {
37326
+ if (hasCoExecutableWrite) return false;
37327
+ const childStart = getRangeStart(child);
37328
+ if (child === assignmentExpression || !isNodeOfType(child, "AssignmentExpression") || assignmentStart === null || childStart === null || childStart >= assignmentStart || resolveReactRefSymbol(child.left, scopes)?.id !== refSymbol.id || hasRepeatedExecutionAncestor(child, branchRoot)) return;
37329
+ if (canExecuteTogether(assignmentConstraints, getBranchConstraints(child, branchRoot))) {
37330
+ hasCoExecutableWrite = true;
37331
+ return false;
37332
+ }
37333
+ });
37334
+ return !hasCoExecutableWrite;
37335
+ };
36937
37336
  const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
36938
- if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
37337
+ if (assignmentExpression.operator === "??=" || assignmentExpression.operator === "||=") return true;
37338
+ if (assignmentExpression.operator !== "=") return false;
36939
37339
  let descendant = assignmentExpression;
36940
37340
  let ancestor = descendant.parent;
36941
37341
  while (ancestor) {
36942
- if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
37342
+ if (isNodeOfType(ancestor, "IfStatement") && isNodeOfType(ancestor.test, "BinaryExpression") && [
37343
+ "===",
37344
+ "==",
37345
+ "!==",
37346
+ "!="
37347
+ ].includes(ancestor.test.operator)) {
36943
37348
  const { left, right } = ancestor.test;
36944
- if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
37349
+ const comparesEmptySentinel = isSameRefCurrentAlias(left, refSymbol, scopes) && isEmptySentinel(right, scopes) || isSameRefCurrentAlias(right, refSymbol, scopes) && isEmptySentinel(left, scopes);
37350
+ const guardedBranch = ancestor.test.operator === "===" || ancestor.test.operator === "==" ? ancestor.consequent : ancestor.alternate;
37351
+ if (comparesEmptySentinel && guardedBranch === descendant && guardedBranch && !hasRepeatedExecutionAncestor(assignmentExpression, guardedBranch) && hasNoPriorCoExecutableWrite(assignmentExpression, guardedBranch, refSymbol, scopes)) return true;
36945
37352
  }
36946
37353
  descendant = ancestor;
36947
37354
  ancestor = descendant.parent;
@@ -45224,7 +45631,7 @@ const DEFERRABLE_HOOK_NAMES = new Set([
45224
45631
  "useParams",
45225
45632
  "usePathname"
45226
45633
  ]);
45227
- const findHookCallBindings = (componentBody) => {
45634
+ const findHookCallBindings = (componentBody, scopes) => {
45228
45635
  const bindings = [];
45229
45636
  if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
45230
45637
  for (const statement of componentBody.body ?? []) {
@@ -45235,8 +45642,10 @@ const findHookCallBindings = (componentBody) => {
45235
45642
  const callee = declarator.init.callee;
45236
45643
  if (!isNodeOfType(callee, "Identifier")) continue;
45237
45644
  if (!DEFERRABLE_HOOK_NAMES.has(callee.name)) continue;
45645
+ const valueSymbol = scopes.symbolFor(declarator.id);
45646
+ if (!valueSymbol) continue;
45238
45647
  bindings.push({
45239
- valueName: declarator.id.name,
45648
+ valueSymbol,
45240
45649
  hookName: callee.name,
45241
45650
  declarator
45242
45651
  });
@@ -45244,6 +45653,49 @@ const findHookCallBindings = (componentBody) => {
45244
45653
  }
45245
45654
  return bindings;
45246
45655
  };
45656
+ const collectExactAliasSymbols = (componentBody, sourceSymbol, scopes) => {
45657
+ const symbols = [sourceSymbol];
45658
+ const symbolIds = new Set([sourceSymbol.id]);
45659
+ const aliasSourceIdentifiers = /* @__PURE__ */ new Set();
45660
+ if (!isNodeOfType(componentBody, "BlockStatement")) return {
45661
+ symbols,
45662
+ aliasSourceIdentifiers
45663
+ };
45664
+ let didFindAlias = true;
45665
+ while (didFindAlias) {
45666
+ didFindAlias = false;
45667
+ for (const statement of componentBody.body ?? []) {
45668
+ if (!isNodeOfType(statement, "VariableDeclaration") || statement.kind !== "const") continue;
45669
+ for (const declarator of statement.declarations ?? []) {
45670
+ let aliasIdentifier = null;
45671
+ let sourceIdentifier = null;
45672
+ const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
45673
+ const arrayBinding = isNodeOfType(declarator.id, "ArrayPattern") ? declarator.id.elements[0] : null;
45674
+ const arrayValueNode = isNodeOfType(initializer, "ArrayExpression") ? initializer.elements[0] : null;
45675
+ const arrayValue = arrayValueNode && !isNodeOfType(arrayValueNode, "SpreadElement") ? stripParenExpression(arrayValueNode) : null;
45676
+ if (isNodeOfType(declarator.id, "Identifier") && isNodeOfType(initializer, "Identifier")) {
45677
+ aliasIdentifier = declarator.id;
45678
+ sourceIdentifier = initializer;
45679
+ } else if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements.length === 1 && isNodeOfType(arrayBinding, "Identifier") && isNodeOfType(initializer, "ArrayExpression") && initializer.elements.length === 1 && isNodeOfType(arrayValue, "Identifier")) {
45680
+ aliasIdentifier = arrayBinding;
45681
+ sourceIdentifier = arrayValue;
45682
+ }
45683
+ if (!aliasIdentifier || !sourceIdentifier) continue;
45684
+ const referencedSymbol = scopes.symbolFor(sourceIdentifier);
45685
+ const aliasSymbol = scopes.symbolFor(aliasIdentifier);
45686
+ if (!referencedSymbol || !symbolIds.has(referencedSymbol.id) || !aliasSymbol || aliasSymbol.kind !== "const" || symbolIds.has(aliasSymbol.id)) continue;
45687
+ symbolIds.add(aliasSymbol.id);
45688
+ symbols.push(aliasSymbol);
45689
+ aliasSourceIdentifiers.add(sourceIdentifier);
45690
+ didFindAlias = true;
45691
+ }
45692
+ }
45693
+ }
45694
+ return {
45695
+ symbols,
45696
+ aliasSourceIdentifiers
45697
+ };
45698
+ };
45247
45699
  const rerenderDeferReadsHook = defineRule({
45248
45700
  id: "rerender-defer-reads-hook",
45249
45701
  title: "URL hook value only read in handlers",
@@ -45254,15 +45706,13 @@ const rerenderDeferReadsHook = defineRule({
45254
45706
  create: (context) => {
45255
45707
  const checkComponent = (componentBody) => {
45256
45708
  if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
45257
- const bindings = findHookCallBindings(componentBody);
45709
+ const bindings = findHookCallBindings(componentBody, context.scopes);
45258
45710
  if (bindings.length === 0) return;
45259
45711
  const handlerBindingNames = collectHandlerBindingNames(componentBody);
45260
45712
  for (const binding of bindings) {
45713
+ const { symbols, aliasSourceIdentifiers } = collectExactAliasSymbols(componentBody, binding.valueSymbol, context.scopes);
45261
45714
  const referenceLocations = [];
45262
- walkAst(componentBody, (child) => {
45263
- if (isNodeOfType(binding.declarator, "VariableDeclarator") && child === binding.declarator.id) return;
45264
- if (isNodeOfType(child, "Identifier") && child.name === binding.valueName) referenceLocations.push(child);
45265
- });
45715
+ for (const symbol of symbols) for (const reference of symbol.references) if (!aliasSourceIdentifiers.has(reference.identifier)) referenceLocations.push(reference.identifier);
45266
45716
  if (referenceLocations.length === 0) continue;
45267
45717
  if (!referenceLocations.every((ref) => isInsideEventHandler(ref, handlerBindingNames))) continue;
45268
45718
  context.report({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.6-dev.0150fc9",
3
+ "version": "0.7.6-dev.0a8d30b",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",