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.
- package/dist/index.js +513 -63
- 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
|
|
1184
|
-
const
|
|
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
|
-
|
|
1196
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) &&
|
|
32017
|
-
if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
|
|
32018
|
-
|
|
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
|
-
|
|
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) && !
|
|
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
|
|
32071
|
-
if (!
|
|
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:
|
|
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
|
|
32941
|
-
|
|
32942
|
-
|
|
32943
|
-
if (!isNodeOfType(
|
|
32944
|
-
|
|
32945
|
-
|
|
32946
|
-
|
|
32947
|
-
|
|
32948
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
36924
|
-
|
|
36925
|
-
|
|
36926
|
-
|
|
36927
|
-
|
|
36928
|
-
|
|
36929
|
-
|
|
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") ||
|
|
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
|
|
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") &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|