oxlint-plugin-react-doctor 0.7.5-dev.22bb155 → 0.7.5-dev.3075e10

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 +141 -586
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -397,7 +397,6 @@ const isProductionFilePath = (relativePath, sourceFilePattern) => {
397
397
  //#endregion
398
398
  //#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
399
399
  const isProductionSourcePath = (relativePath) => {
400
- if (/\.d\.[cm]?[jt]s$/i.test(relativePath)) return false;
401
400
  return isProductionFilePath(relativePath, SOURCE_FILE_PATTERN);
402
401
  };
403
402
  //#endregion
@@ -5179,10 +5178,8 @@ const STORAGE_GLOBALS = new Set([
5179
5178
  const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
5180
5179
  const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
5181
5180
  const STRONG_AUTH_KEY_PATTERN = /jwt|secret|password|passwd|credential|private[-_]?key|api[-_]?key|bearer|access[-_]?token|refresh[-_]?token|auth[-_]?token|id[-_]?token|session/i;
5182
- const PRODUCT_API_KEY_RECORDS_PATTERN = /(?:^|[._:-])(?:created|saved|integration|mailing)[-_]?api[-_]?keys$/i;
5183
5181
  const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
5184
5182
  const isAuthCredentialKey = (key) => {
5185
- if (PRODUCT_API_KEY_RECORDS_PATTERN.test(key)) return false;
5186
5183
  if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
5187
5184
  if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
5188
5185
  if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
@@ -5934,21 +5931,6 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
5934
5931
  }
5935
5932
  });
5936
5933
  //#endregion
5937
- //#region src/plugin/utils/get-static-property-key-name.ts
5938
- const getStaticPropertyKeyName = (node, options = {}) => {
5939
- if (!isNodeOfType(node, "Property")) return null;
5940
- if (node.computed) {
5941
- if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
5942
- return null;
5943
- }
5944
- if (isNodeOfType(node.key, "Identifier")) return node.key.name;
5945
- if (isNodeOfType(node.key, "Literal")) {
5946
- if (typeof node.key.value === "string") return node.key.value;
5947
- if (options.stringifyNonStringLiterals) return String(node.key.value);
5948
- }
5949
- return null;
5950
- };
5951
- //#endregion
5952
5934
  //#region src/plugin/utils/is-presentation-role.ts
5953
5935
  const isPresentationRole = (openingElement) => {
5954
5936
  const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
@@ -5993,21 +5975,6 @@ const isPureEventBlockerHandler = (attribute) => {
5993
5975
  return false;
5994
5976
  };
5995
5977
  //#endregion
5996
- //#region src/plugin/utils/resolve-const-identifier-alias.ts
5997
- const resolveConstIdentifierAlias = (identifier, scopes) => {
5998
- if (!isNodeOfType(identifier, "Identifier")) return null;
5999
- const visitedSymbolIds = /* @__PURE__ */ new Set();
6000
- let symbol = scopes.symbolFor(identifier);
6001
- while (symbol?.kind === "const") {
6002
- if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
6003
- visitedSymbolIds.add(symbol.id);
6004
- const initializer = stripParenExpression(symbol.initializer);
6005
- if (!isNodeOfType(initializer, "Identifier")) return symbol;
6006
- symbol = scopes.symbolFor(initializer);
6007
- }
6008
- return symbol;
6009
- };
6010
- //#endregion
6011
5978
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
6012
5979
  const MESSAGE$57 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
6013
5980
  const KEY_HANDLERS = [
@@ -6018,63 +5985,6 @@ const KEY_HANDLERS = [
6018
5985
  "onKeyDownCapture",
6019
5986
  "onKeyPressCapture"
6020
5987
  ];
6021
- const CLICK_HANDLERS = ["onClick", "onClickCapture"];
6022
- const TRANSPARENT_SPREAD_EVENT_NAMES = new Set([...CLICK_HANDLERS, ...KEY_HANDLERS].map((eventName) => eventName.toLowerCase()));
6023
- const CONSERVATIVE_SPREAD_PROP_NAMES = new Set([
6024
- "aria-hidden",
6025
- "onmouseenter",
6026
- "onmouseover",
6027
- "role"
6028
- ]);
6029
- const resolveSpreadObjectExpression = (expression, scopes) => {
6030
- const innerExpression = stripParenExpression(expression);
6031
- if (isNodeOfType(innerExpression, "ObjectExpression")) return innerExpression;
6032
- if (!isNodeOfType(innerExpression, "Identifier")) return null;
6033
- const symbol = resolveConstIdentifierAlias(innerExpression, scopes);
6034
- if (symbol?.kind !== "const" || !symbol.initializer) return null;
6035
- const initializer = stripParenExpression(symbol.initializer);
6036
- return isNodeOfType(initializer, "ObjectExpression") ? initializer : null;
6037
- };
6038
- const collectTransparentSpreadEventNames = (expression, scopes, eventValues, visitedObjectExpressions) => {
6039
- const objectExpression = resolveSpreadObjectExpression(expression, scopes);
6040
- if (!objectExpression || visitedObjectExpressions.has(objectExpression)) return false;
6041
- visitedObjectExpressions.add(objectExpression);
6042
- let isTransparent = true;
6043
- for (const property of objectExpression.properties) {
6044
- if (isNodeOfType(property, "SpreadElement")) {
6045
- if (!collectTransparentSpreadEventNames(property.argument, scopes, eventValues, visitedObjectExpressions)) {
6046
- isTransparent = false;
6047
- break;
6048
- }
6049
- continue;
6050
- }
6051
- if (!isNodeOfType(property, "Property")) {
6052
- isTransparent = false;
6053
- break;
6054
- }
6055
- const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
6056
- if (!propertyName) {
6057
- isTransparent = false;
6058
- break;
6059
- }
6060
- const normalizedPropertyName = propertyName.toLowerCase();
6061
- if (CONSERVATIVE_SPREAD_PROP_NAMES.has(normalizedPropertyName)) {
6062
- isTransparent = false;
6063
- break;
6064
- }
6065
- if (TRANSPARENT_SPREAD_EVENT_NAMES.has(normalizedPropertyName)) eventValues.set(normalizedPropertyName, property.value);
6066
- }
6067
- visitedObjectExpressions.delete(objectExpression);
6068
- return isTransparent;
6069
- };
6070
- const getTransparentSpreadEventValues = (attributes, scopes) => {
6071
- const eventValues = /* @__PURE__ */ new Map();
6072
- for (const attribute of attributes) {
6073
- if (!isNodeOfType(attribute, "JSXSpreadAttribute")) continue;
6074
- if (!collectTransparentSpreadEventNames(attribute.argument, scopes, eventValues, /* @__PURE__ */ new Set())) return null;
6075
- }
6076
- return eventValues;
6077
- };
6078
5988
  const FOCUSLESS_CONTAINER_TAGS = new Set([
6079
5989
  "tr",
6080
5990
  "td",
@@ -6122,10 +6032,7 @@ const isFocusForwardingFunctionBody = (body) => {
6122
6032
  };
6123
6033
  const resolveHandlerFunction = (attribute) => {
6124
6034
  if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
6125
- return resolveHandlerFunctionExpression(attribute.value.expression);
6126
- };
6127
- const resolveHandlerFunctionExpression = (handlerExpression) => {
6128
- let expression = handlerExpression;
6035
+ let expression = attribute.value.expression;
6129
6036
  if (isNodeOfType(expression, "Identifier")) {
6130
6037
  const binding = findVariableInitializer(expression, expression.name);
6131
6038
  if (!binding?.initializer) return null;
@@ -6224,22 +6131,18 @@ const clickEventsHaveKeyEvents = defineRule({
6224
6131
  if (!HTML_TAGS.has(tag)) return;
6225
6132
  if (tag === "label") return;
6226
6133
  if (!FOCUSLESS_CONTAINER_TAGS.has(tag) && isInteractiveElement(tag, node)) return;
6227
- const spreadEventValues = getTransparentSpreadEventValues(node.attributes, context.scopes);
6228
- if (!spreadEventValues) return;
6229
6134
  const onClick = hasJsxPropIgnoreCase(node.attributes, "onClick") ?? hasJsxPropIgnoreCase(node.attributes, "onClickCapture");
6230
- const spreadOnClickExpression = CLICK_HANDLERS.map((name) => spreadEventValues.get(name.toLowerCase())).find((expression) => expression !== void 0);
6231
- if (!onClick && !spreadOnClickExpression) return;
6232
- if (onClick && isPureEventBlockerHandler(onClick)) return;
6233
- if (onClick && isFocusForwardingHandler(onClick)) return;
6234
- const spreadHandlerFunction = spreadOnClickExpression ? resolveHandlerFunctionExpression(spreadOnClickExpression) : null;
6235
- if (spreadHandlerFunction && (isFocusForwardingFunctionBody(spreadHandlerFunction.body ?? null) || containsBackdropDismissComparison(spreadHandlerFunction.body ?? null))) return;
6135
+ if (!onClick) return;
6136
+ if (isPureEventBlockerHandler(onClick)) return;
6137
+ if (isFocusForwardingHandler(onClick)) return;
6138
+ if (hasJsxSpreadAttribute$1(node.attributes)) return;
6236
6139
  if (hasCompositeItemRole(node)) return;
6237
6140
  if (isHoverSelectionListItem(tag, node)) return;
6238
- if (onClick && isBackdropDismissHandler(onClick)) return;
6141
+ if (isBackdropDismissHandler(onClick)) return;
6239
6142
  if (containsKeyboardActivatableDescendant(node.parent)) return;
6240
6143
  if (isHiddenFromScreenReader(node, context.settings)) return;
6241
6144
  if (isPresentationRole(node)) return;
6242
- if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler) || spreadEventValues.has(handler.toLowerCase()))) return;
6145
+ if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
6243
6146
  context.report({
6244
6147
  node: node.name,
6245
6148
  message: MESSAGE$57
@@ -6965,10 +6868,11 @@ const getTemplateInterpolations = (valueTail) => {
6965
6868
  const interpolations = valueTail.slice(1, closingBacktickIndex).match(/\$\{[^}]*\}/g);
6966
6869
  return interpolations === null ? "" : interpolations.join(" ");
6967
6870
  };
6968
- const getIdentifierDeclarationInitializer = (identifier, sinkIndex, fileContent) => {
6969
- const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
6970
- if (declaration === null) return null;
6971
- return fileContent.slice(declaration.initializerStartIndex, declaration.initializerStartIndex + STATIC_TEMPLATE_MAX_CHARS);
6871
+ const getIdentifierDeclarationInitializer = (identifier, fileContent) => {
6872
+ const declarationMatch = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]{0,120})?=\\s*`).exec(fileContent);
6873
+ if (declarationMatch === null) return null;
6874
+ const initializerStartIndex = declarationMatch.index + declarationMatch[0].length;
6875
+ return fileContent.slice(initializerStartIndex, initializerStartIndex + STATIC_TEMPLATE_MAX_CHARS);
6972
6876
  };
6973
6877
  const isPureStringLiteralConcat = (initializerText) => {
6974
6878
  if (!/^["']/.test(initializerText)) return false;
@@ -7038,153 +6942,6 @@ const isAllOperandsDomContentConcat = (valueExpression) => {
7038
6942
  return !HTML_TAINT_PATTERN.test(withoutCast.replace(DOM_CONTENT_SOURCE_VALUE_PATTERN, ""));
7039
6943
  });
7040
6944
  };
7041
- const findMatchingBraceIndex = (fileContent, openingBraceIndex) => {
7042
- let depth = 0;
7043
- let quote = null;
7044
- for (let index = openingBraceIndex; index < fileContent.length; index += 1) {
7045
- const character = fileContent[index];
7046
- if (quote !== null) {
7047
- if (character === quote && fileContent[index - 1] !== "\\") quote = null;
7048
- continue;
7049
- }
7050
- if (character === "\"" || character === "'" || character === "`") {
7051
- quote = character;
7052
- continue;
7053
- }
7054
- if (character === "{") depth += 1;
7055
- if (character === "}") {
7056
- depth -= 1;
7057
- if (depth === 0) return index;
7058
- }
7059
- }
7060
- return fileContent.length;
7061
- };
7062
- const findContainingBlockEndIndex = (fileContent, targetIndex) => {
7063
- const openingBraceIndexes = [];
7064
- let quote = null;
7065
- let isLineComment = false;
7066
- let isBlockComment = false;
7067
- for (let index = 0; index < targetIndex; index += 1) {
7068
- const character = fileContent[index];
7069
- const nextCharacter = fileContent[index + 1];
7070
- if (isLineComment) {
7071
- if (character === "\n") isLineComment = false;
7072
- continue;
7073
- }
7074
- if (isBlockComment) {
7075
- if (character === "*" && nextCharacter === "/") {
7076
- isBlockComment = false;
7077
- index += 1;
7078
- }
7079
- continue;
7080
- }
7081
- if (quote !== null) {
7082
- if (character === quote && fileContent[index - 1] !== "\\") quote = null;
7083
- continue;
7084
- }
7085
- if (character === "/" && nextCharacter === "/") {
7086
- isLineComment = true;
7087
- index += 1;
7088
- continue;
7089
- }
7090
- if (character === "/" && nextCharacter === "*") {
7091
- isBlockComment = true;
7092
- index += 1;
7093
- continue;
7094
- }
7095
- if (character === "\"" || character === "'" || character === "`") {
7096
- quote = character;
7097
- continue;
7098
- }
7099
- if (character === "{") openingBraceIndexes.push(index);
7100
- if (character === "}") openingBraceIndexes.pop();
7101
- }
7102
- const openingBraceIndex = openingBraceIndexes.at(-1);
7103
- return openingBraceIndex === void 0 ? fileContent.length : findMatchingBraceIndex(fileContent, openingBraceIndex);
7104
- };
7105
- const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) => {
7106
- const initializerPattern = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
7107
- let nearestDeclaration = null;
7108
- let nearestDeclarationIndex = -1;
7109
- for (const match of fileContent.matchAll(initializerPattern)) {
7110
- const declarationIndex = match.index;
7111
- if (declarationIndex === void 0 || declarationIndex >= sinkIndex || declarationIndex <= nearestDeclarationIndex || findContainingBlockEndIndex(fileContent, declarationIndex) < sinkIndex) continue;
7112
- nearestDeclarationIndex = declarationIndex;
7113
- const initializer = match[1];
7114
- if (initializer === void 0) continue;
7115
- nearestDeclaration = {
7116
- initializer,
7117
- initializerStartIndex: declarationIndex + match[0].length - initializer.length
7118
- };
7119
- }
7120
- return nearestDeclaration;
7121
- };
7122
- const findContainingFunctionParameterSource = (identifier, sinkIndex, fileContent) => {
7123
- const patterns = [/function\s+([\w$]+)\s*\(([^)]*)\)\s*\{/g, /(?:const|let|var)\s+([\w$]+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>\s*\{/g];
7124
- let closestSource = null;
7125
- let closestStartIndex = -1;
7126
- for (const pattern of patterns) for (const match of fileContent.matchAll(pattern)) {
7127
- const matchIndex = match.index;
7128
- if (matchIndex === void 0 || matchIndex >= sinkIndex || matchIndex < closestStartIndex) continue;
7129
- if (findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{")) < sinkIndex) continue;
7130
- const parameterIndex = (match[2] ?? "").split(",").findIndex((parameter) => parameter.trim().match(/^(?:\.\.\.)?([\w$]+)/)?.[1] === identifier);
7131
- if (parameterIndex < 0) continue;
7132
- closestStartIndex = matchIndex;
7133
- closestSource = {
7134
- functionName: match[1] ?? "",
7135
- parameterIndex
7136
- };
7137
- }
7138
- return closestSource;
7139
- };
7140
- const splitTopLevelArguments = (argumentText) => {
7141
- const argumentsList = [];
7142
- let startIndex = 0;
7143
- let depth = 0;
7144
- let quote = null;
7145
- for (let index = 0; index < argumentText.length; index += 1) {
7146
- const character = argumentText[index];
7147
- if (quote !== null) {
7148
- if (character === quote && argumentText[index - 1] !== "\\") quote = null;
7149
- continue;
7150
- }
7151
- if (character === "\"" || character === "'" || character === "`") {
7152
- quote = character;
7153
- continue;
7154
- }
7155
- if (character === "(" || character === "[" || character === "{") depth += 1;
7156
- if (character === ")" || character === "]" || character === "}") depth -= 1;
7157
- if (character === "," && depth === 0) {
7158
- argumentsList.push(argumentText.slice(startIndex, index).trim());
7159
- startIndex = index + 1;
7160
- }
7161
- }
7162
- argumentsList.push(argumentText.slice(startIndex).trim());
7163
- return argumentsList;
7164
- };
7165
- const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers) => {
7166
- const trimmedExpression = expression.trim();
7167
- if (STRING_LITERAL_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
7168
- if (MODULE_CONSTANT_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
7169
- if (SANITIZER_PATTERN.test(trimmedExpression)) return false;
7170
- if (ENV_CONFIG_VALUE_PATTERN.test(trimmedExpression)) return false;
7171
- if (I18N_VALUE_PATTERN.test(trimmedExpression)) return false;
7172
- if (ESCAPING_SERIALIZER_CALL_PATTERN.test(trimmedExpression)) return false;
7173
- if (HTML_TAINT_PATTERN.test(trimmedExpression)) return true;
7174
- const identifier = trimmedExpression.match(/^([\w$]+)\s*(?:[;,})\n]|$)/)?.[1];
7175
- if (identifier === void 0 || visitedIdentifiers.has(identifier)) return false;
7176
- visitedIdentifiers.add(identifier);
7177
- const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
7178
- if (declaration !== null && isHtmlTainted(declaration.initializer, fileContent, sinkIndex, visitedIdentifiers)) return true;
7179
- const parameterSource = findContainingFunctionParameterSource(identifier, sinkIndex, fileContent);
7180
- if (parameterSource === null || parameterSource.functionName.length === 0) return false;
7181
- const callPattern = new RegExp(`\\b${escapeRegExp(parameterSource.functionName)}\\s*\\(([^)]*)\\)`, "g");
7182
- for (const callMatch of fileContent.matchAll(callPattern)) {
7183
- const argument = splitTopLevelArguments(callMatch[1] ?? "")[parameterSource.parameterIndex];
7184
- if (argument !== void 0 && isHtmlTainted(argument, fileContent, sinkIndex, new Set(visitedIdentifiers))) return true;
7185
- }
7186
- return false;
7187
- };
7188
6945
  const dangerousHtmlSink = defineRule({
7189
6946
  id: "dangerous-html-sink",
7190
6947
  title: "HTML injection sink with dynamic content",
@@ -7211,7 +6968,6 @@ const dangerousHtmlSink = defineRule({
7211
6968
  const valueTail = fullValueTail.slice(0, VALUE_EXPRESSION_MAX_CHARS);
7212
6969
  const terminatorIndex = valueTail.search(/[;}]/);
7213
6970
  const valueExpression = terminatorIndex >= 0 ? valueTail.slice(0, terminatorIndex + 1) : valueTail;
7214
- const sinkIndex = lines.slice(0, lineIndex).join("\n").length + (lineIndex > 0 ? 1 : 0) + line.search(DANGEROUS_HTML_PATTERN);
7215
6971
  if (STRING_LITERAL_VALUE_PATTERN.test(valueExpression)) continue;
7216
6972
  if (MODULE_CONSTANT_VALUE_PATTERN.test(valueExpression)) continue;
7217
6973
  if (DOM_CONTENT_SOURCE_VALUE_PATTERN.test(valueExpression) && !valueExpression.includes("+")) {
@@ -7223,7 +6979,7 @@ const dangerousHtmlSink = defineRule({
7223
6979
  let templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
7224
6980
  const valueIdentifier = BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression) || MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN.test(valueExpression) || IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN.test(valueExpression) ? valueExpression.match(/^[\w$]+/)?.[0] : void 0;
7225
6981
  if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)) {
7226
- const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, sinkIndex, file.content);
6982
+ const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, file.content);
7227
6983
  if (declarationInitializer !== null) {
7228
6984
  if (isPureStringLiteralConcat(declarationInitializer)) continue;
7229
6985
  templateInterpolations = getTemplateInterpolations(declarationInitializer);
@@ -7234,7 +6990,7 @@ const dangerousHtmlSink = defineRule({
7234
6990
  if (SANITIZER_PATTERN.test(judgedExpression)) continue;
7235
6991
  if (ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
7236
6992
  if (I18N_VALUE_PATTERN.test(judgedExpression)) continue;
7237
- if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set())) continue;
6993
+ if (!HTML_TAINT_PATTERN.test(judgedExpression)) continue;
7238
6994
  if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
7239
6995
  if (/highlighted/i.test(valueExpression)) continue;
7240
6996
  if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
@@ -7712,7 +7468,7 @@ const isCreateClassLikeCall = (node) => {
7712
7468
  if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName$2(callee) === "createClass";
7713
7469
  return false;
7714
7470
  };
7715
- const isCreateContextCall$1 = (node) => {
7471
+ const isCreateContextCall = (node) => {
7716
7472
  if (!isNodeOfType(node, "CallExpression")) return false;
7717
7473
  const callee = node.callee;
7718
7474
  if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
@@ -7889,7 +7645,7 @@ const displayName = defineRule({
7889
7645
  }
7890
7646
  },
7891
7647
  CallExpression(node) {
7892
- if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall$1(node)) {
7648
+ if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall(node)) {
7893
7649
  const assignedName = getAssignedName(node);
7894
7650
  if (assignedName) {
7895
7651
  const programRoot = findProgramRoot(node);
@@ -7936,6 +7692,21 @@ const displayName = defineRule({
7936
7692
  }
7937
7693
  });
7938
7694
  //#endregion
7695
+ //#region src/plugin/utils/get-static-property-key-name.ts
7696
+ const getStaticPropertyKeyName = (node, options = {}) => {
7697
+ if (!isNodeOfType(node, "Property")) return null;
7698
+ if (node.computed) {
7699
+ if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
7700
+ return null;
7701
+ }
7702
+ if (isNodeOfType(node.key, "Identifier")) return node.key.name;
7703
+ if (isNodeOfType(node.key, "Literal")) {
7704
+ if (typeof node.key.value === "string") return node.key.value;
7705
+ if (options.stringifyNonStringLiterals) return String(node.key.value);
7706
+ }
7707
+ return null;
7708
+ };
7709
+ //#endregion
7939
7710
  //#region src/plugin/utils/read-static-boolean.ts
7940
7711
  const readStaticBoolean = (node) => {
7941
7712
  if (!node) return null;
@@ -7944,6 +7715,21 @@ const readStaticBoolean = (node) => {
7944
7715
  return unwrappedNode.value;
7945
7716
  };
7946
7717
  //#endregion
7718
+ //#region src/plugin/utils/resolve-const-identifier-alias.ts
7719
+ const resolveConstIdentifierAlias = (identifier, scopes) => {
7720
+ if (!isNodeOfType(identifier, "Identifier")) return null;
7721
+ const visitedSymbolIds = /* @__PURE__ */ new Set();
7722
+ let symbol = scopes.symbolFor(identifier);
7723
+ while (symbol?.kind === "const") {
7724
+ if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
7725
+ visitedSymbolIds.add(symbol.id);
7726
+ const initializer = stripParenExpression(symbol.initializer);
7727
+ if (!isNodeOfType(initializer, "Identifier")) return symbol;
7728
+ symbol = scopes.symbolFor(initializer);
7729
+ }
7730
+ return symbol;
7731
+ };
7732
+ //#endregion
7947
7733
  //#region src/plugin/utils/is-react-api-call.ts
7948
7734
  const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
7949
7735
  const isImportedFromReact = (symbol) => {
@@ -7951,9 +7737,9 @@ const isImportedFromReact = (symbol) => {
7951
7737
  const importDeclaration = symbol.declarationNode.parent;
7952
7738
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
7953
7739
  };
7954
- const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
7740
+ const isNamedReactApiImport = (identifier, apiNames, scopes) => {
7955
7741
  if (!isNodeOfType(identifier, "Identifier")) return false;
7956
- const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
7742
+ const symbol = scopes.symbolFor(identifier);
7957
7743
  if (!symbol || !isImportedFromReact(symbol)) return false;
7958
7744
  const importedName = getImportedName(symbol.declarationNode);
7959
7745
  return Boolean(importedName && includesApiName(apiNames, importedName));
@@ -7967,7 +7753,7 @@ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
7967
7753
  if (!isNodeOfType(node, "CallExpression")) return false;
7968
7754
  const callee = stripParenExpression(node.callee);
7969
7755
  if (isNodeOfType(callee, "Identifier")) {
7970
- if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
7756
+ if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
7971
7757
  return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
7972
7758
  }
7973
7759
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
@@ -9070,30 +8856,6 @@ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, conte
9070
8856
  }
9071
8857
  return matchingBlocks.size > 0;
9072
8858
  };
9073
- const doMatchingNodesCoverEveryPathFromFunctionEntry = (owner, matchingNodes, context) => {
9074
- const functionCfg = context.cfg.cfgFor(owner);
9075
- if (!functionCfg) return false;
9076
- const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
9077
- if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
9078
- const matchingBlock = functionCfg.blockOf(matchingNode);
9079
- return matchingBlock ? [matchingBlock] : [];
9080
- }));
9081
- if (matchingBlocks.size === 0) return false;
9082
- const visitedBlocks = new Set([functionCfg.entry]);
9083
- const pendingBlocks = [functionCfg.entry];
9084
- while (pendingBlocks.length > 0) {
9085
- const currentBlock = pendingBlocks.pop();
9086
- if (!currentBlock) break;
9087
- if (matchingBlocks.has(currentBlock)) continue;
9088
- for (const edge of currentBlock.successors) {
9089
- if (edge.to === functionCfg.exit) return false;
9090
- if (visitedBlocks.has(edge.to)) continue;
9091
- visitedBlocks.add(edge.to);
9092
- pendingBlocks.push(edge.to);
9093
- }
9094
- }
9095
- return true;
9096
- };
9097
8859
  const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
9098
8860
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
9099
8861
  if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
@@ -9294,83 +9056,6 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
9294
9056
  });
9295
9057
  return didCleanupFunctionMatch;
9296
9058
  };
9297
- const doesBoundCleanupReleaseUsage = (expression, usage, context) => {
9298
- const callExpression = stripParenExpression(expression);
9299
- if (!isNodeOfType(callExpression, "CallExpression")) return false;
9300
- const bindCallee = stripParenExpression(callExpression.callee);
9301
- if (!isNodeOfType(bindCallee, "MemberExpression") || bindCallee.computed || !isNodeOfType(bindCallee.property, "Identifier") || bindCallee.property.name !== "bind") return false;
9302
- const releaseMember = stripParenExpression(bindCallee.object);
9303
- if (!isNodeOfType(releaseMember, "MemberExpression") || releaseMember.computed || !isNodeOfType(releaseMember.property, "Identifier")) return false;
9304
- const releaseReceiverKey = resolveExpressionKey$1(releaseMember.object, context);
9305
- if (releaseReceiverKey === null || releaseReceiverKey !== resolveExpressionKey$1(callExpression.arguments?.[0], context)) return false;
9306
- const releaseVerbName = releaseMember.property.name;
9307
- if (usage.kind === "socket") return usage.handleKey === releaseReceiverKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
9308
- return usage.kind === "subscribe" && usage.handleKey === releaseReceiverKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName));
9309
- };
9310
- const callbackReturnsCleanupForUsage = (callback, usage, context) => {
9311
- if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
9312
- const doesReturnedValueReleaseUsage = (returnedValue) => {
9313
- if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) return true;
9314
- const cleanupFunction = resolveStableValue(returnedValue, context);
9315
- if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) return true;
9316
- return Boolean(cleanupFunction && isFunctionLike$1(cleanupFunction) && doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context));
9317
- };
9318
- if (!isNodeOfType(callback.body, "BlockStatement")) return doesReturnedValueReleaseUsage(stripParenExpression(callback.body));
9319
- const matchingCleanupReturns = [];
9320
- walkInsideStatementBlocks(callback.body, (child) => {
9321
- if (isNodeOfType(child, "ReturnStatement") && child.argument && doesReturnedValueReleaseUsage(stripParenExpression(child.argument))) matchingCleanupReturns.push(child);
9322
- });
9323
- return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
9324
- };
9325
- const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
9326
- if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
9327
- const functionCfg = context.cfg.cfgFor(callback);
9328
- const usageBlock = functionCfg?.blockOf(usage.node);
9329
- const usageStart = getRangeStart(usage.node);
9330
- if (!functionCfg || !usageBlock || usageStart === null) return false;
9331
- const findHandleGuard = (releaseCall) => {
9332
- if (usage.handleKey === null) return null;
9333
- let ancestor = releaseCall.parent;
9334
- while (ancestor && ancestor !== callback.body) {
9335
- if (isNodeOfType(ancestor, "IfStatement")) return ancestor.alternate === null && resolveExpressionKey$1(ancestor.test, context) === usage.handleKey && getRangeStart(ancestor) !== null && (getRangeStart(ancestor) ?? usageStart) < usageStart ? ancestor : null;
9336
- ancestor = ancestor.parent;
9337
- }
9338
- return null;
9339
- };
9340
- const matchingReleaseAnchors = [];
9341
- walkInsideStatementBlocks(callback.body, (child) => {
9342
- if (!isNodeOfType(child, "CallExpression")) return;
9343
- const releaseStart = getRangeStart(child);
9344
- const handleGuard = findHandleGuard(child);
9345
- if (releaseStart === null || releaseStart >= usageStart || functionCfg.blockOf(child) !== usageBlock && !handleGuard) return;
9346
- if (doesReleaseCallMatchUsage(child, usage, context)) {
9347
- matchingReleaseAnchors.push(handleGuard ?? child);
9348
- return;
9349
- }
9350
- const helperFunction = resolveStableValue(child.callee, context);
9351
- if (helperFunction && isFunctionLike$1(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context)) matchingReleaseAnchors.push(handleGuard ?? child);
9352
- });
9353
- return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingReleaseAnchors, context);
9354
- };
9355
- const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
9356
- const componentFunction = findEnclosingFunction(callback);
9357
- if (!componentFunction || !isNodeOfType(componentFunction, "ArrowFunctionExpression") && !isNodeOfType(componentFunction, "FunctionExpression") && !isNodeOfType(componentFunction, "FunctionDeclaration")) return false;
9358
- let didFindUnmountCleanup = false;
9359
- walkAst(componentFunction.body, (child) => {
9360
- if (didFindUnmountCleanup) return false;
9361
- if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction(child) !== componentFunction) return;
9362
- if (!isHookCall$2(child, CLEANUP_EFFECT_HOOK_NAMES)) return;
9363
- const dependencyList = child.arguments?.[1];
9364
- if (!isNodeOfType(dependencyList, "ArrayExpression") || dependencyList.elements.length > 0) return;
9365
- const cleanupCallback = getEffectCallback(child);
9366
- if (cleanupCallback && cleanupCallback !== callback && callbackReturnsCleanupForUsage(cleanupCallback, usage, context)) {
9367
- didFindUnmountCleanup = true;
9368
- return false;
9369
- }
9370
- });
9371
- return didFindUnmountCleanup;
9372
- };
9373
- const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
9374
9059
  const effectHasCleanupForUsage = (callback, usage, context) => {
9375
9060
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
9376
9061
  if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
@@ -9383,10 +9068,6 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
9383
9068
  if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
9384
9069
  const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
9385
9070
  if (!returnedValue) return;
9386
- if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) {
9387
- matchingCleanupReturns.push(child);
9388
- return;
9389
- }
9390
9071
  if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
9391
9072
  matchingCleanupReturns.push(child);
9392
9073
  return;
@@ -9402,17 +9083,13 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
9402
9083
  if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
9403
9084
  }
9404
9085
  const cleanupFunction = resolveStableValue(returnedValue, context);
9405
- if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) {
9406
- matchingCleanupReturns.push(child);
9407
- return;
9408
- }
9409
9086
  if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
9410
9087
  if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
9411
9088
  });
9412
9089
  return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
9413
9090
  };
9414
9091
  const findFirstUsageWithoutCleanup = (callback, usages, context) => {
9415
- for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context) && !hasSplitLifecycleCleanup(callback, usage, context)) return usage;
9092
+ for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context)) return usage;
9416
9093
  return null;
9417
9094
  };
9418
9095
  const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
@@ -11285,11 +10962,6 @@ If the missing value is recreated every render, move it inside the hook or stabi
11285
10962
  return { CallExpression(node) {
11286
10963
  const hookName = getHookName(node.callee, context.scopes);
11287
10964
  if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
11288
- if (HOOKS_REQUIRING_DEPS_MATCH.has(hookName) && !isReactApiCall(node, HOOKS_REQUIRING_DEPS_MATCH, context.scopes, {
11289
- allowGlobalReactNamespace: true,
11290
- allowUnboundBareCalls: true,
11291
- resolveNamedAliases: true
11292
- })) return;
11293
10965
  const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
11294
10966
  const depsArgumentIndex = getDepsArgumentIndex(hookName);
11295
10967
  const callbackArgument = node.arguments[callbackArgumentIndex];
@@ -23217,73 +22889,61 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
23217
22889
  "Array",
23218
22890
  "BigInt",
23219
22891
  "Boolean",
23220
- "encodeURIComponent",
23221
22892
  "Number",
23222
22893
  "Object",
23223
22894
  "String",
23224
22895
  "parseFloat",
23225
- "parseInt",
23226
- "structuredClone"
23227
- ]);
23228
- const PURE_GLOBAL_CONSTRUCTOR_NAMES = new Set(["Date", "Set"]);
23229
- const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([
23230
- ["Array", new Set(["from"])],
23231
- ["JSON", new Set([
23232
- "isRawJSON",
23233
- "parse",
23234
- "rawJSON",
23235
- "stringify"
23236
- ])],
23237
- ["Math", new Set([
23238
- "abs",
23239
- "acos",
23240
- "acosh",
23241
- "asin",
23242
- "asinh",
23243
- "atan",
23244
- "atan2",
23245
- "atanh",
23246
- "cbrt",
23247
- "ceil",
23248
- "clz32",
23249
- "cos",
23250
- "cosh",
23251
- "exp",
23252
- "floor",
23253
- "fround",
23254
- "hypot",
23255
- "imul",
23256
- "log",
23257
- "log10",
23258
- "log1p",
23259
- "log2",
23260
- "max",
23261
- "min",
23262
- "pow",
23263
- "round",
23264
- "sign",
23265
- "sin",
23266
- "sinh",
23267
- "sqrt",
23268
- "tan",
23269
- "tanh",
23270
- "trunc"
23271
- ])],
23272
- ["Object", new Set(["assign"])]
22896
+ "parseInt"
23273
22897
  ]);
22898
+ const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
22899
+ const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
22900
+ "isRawJSON",
22901
+ "parse",
22902
+ "rawJSON",
22903
+ "stringify"
22904
+ ])], ["Math", new Set([
22905
+ "abs",
22906
+ "acos",
22907
+ "acosh",
22908
+ "asin",
22909
+ "asinh",
22910
+ "atan",
22911
+ "atan2",
22912
+ "atanh",
22913
+ "cbrt",
22914
+ "ceil",
22915
+ "clz32",
22916
+ "cos",
22917
+ "cosh",
22918
+ "exp",
22919
+ "floor",
22920
+ "fround",
22921
+ "hypot",
22922
+ "imul",
22923
+ "log",
22924
+ "log10",
22925
+ "log1p",
22926
+ "log2",
22927
+ "max",
22928
+ "min",
22929
+ "pow",
22930
+ "round",
22931
+ "sign",
22932
+ "sin",
22933
+ "sinh",
22934
+ "sqrt",
22935
+ "tan",
22936
+ "tanh",
22937
+ "trunc"
22938
+ ])]]);
23274
22939
  const PURE_MEMBER_TRANSFORM_NAMES = new Set([
23275
22940
  "concat",
23276
22941
  "filter",
23277
- "flatMap",
23278
22942
  "join",
23279
22943
  "map",
23280
- "reduce",
23281
- "replace",
23282
- "slice",
23283
22944
  "split",
23284
22945
  "toLowerCase",
23285
22946
  "toString",
23286
- "toSorted",
23287
22947
  "toUpperCase",
23288
22948
  "trim"
23289
22949
  ]);
@@ -23665,11 +23325,6 @@ const analyzeHelperExpression = (expression, environment, usedParameterIndices)
23665
23325
  const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
23666
23326
  return callbackSummary.isValid && !callbackSummary.canContinue;
23667
23327
  }
23668
- if (isNodeOfType(node, "NewExpression")) {
23669
- const callee = stripParenExpression(node.callee);
23670
- if (!isNodeOfType(callee, "Identifier") || !PURE_GLOBAL_CONSTRUCTOR_NAMES.has(callee.name) || environment.parameterIndices.has(callee.name) || environment.recursiveNames.has(callee.name) || environment.shadowedGlobalNames.has(callee.name)) return false;
23671
- return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
23672
- }
23673
23328
  if (isNodeOfType(node, "CallExpression")) {
23674
23329
  const callee = stripParenExpression(node.callee);
23675
23330
  const calleeRoot = getMemberRoot(callee);
@@ -23872,7 +23527,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23872
23527
  }
23873
23528
  const callee = stripParenExpression(node.callee);
23874
23529
  const calleeRoot = getMemberRoot(callee);
23875
- const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(callee, "MemberExpression") && isNodeOfType(calleeRoot, "Identifier") && getIdentifierBindingIdentity(analysis, calleeRoot) === null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(calleeRoot.name)?.has(getStaticMemberName$1(callee) ?? "") === true;
23530
+ const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(calleeRoot, "Identifier") && PURE_GLOBAL_NAMESPACE_NAMES.has(calleeRoot.name) && getIdentifierBindingIdentity(analysis, calleeRoot) === null;
23876
23531
  const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
23877
23532
  if (isPureGlobalCall || isPureMemberTransform) {
23878
23533
  if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
@@ -23925,15 +23580,6 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23925
23580
  }
23926
23581
  return evidence;
23927
23582
  }
23928
- if (isNodeOfType(node, "NewExpression")) {
23929
- const callee = stripParenExpression(node.callee);
23930
- if (!isNodeOfType(callee, "Identifier") || !PURE_GLOBAL_CONSTRUCTOR_NAMES.has(callee.name) || getIdentifierBindingIdentity(analysis, callee) !== null) {
23931
- evidence.hasUnknownSource = true;
23932
- return evidence;
23933
- }
23934
- for (const argument of node.arguments ?? []) mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
23935
- return evidence;
23936
- }
23937
23583
  if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
23938
23584
  evidence.hasUnknownSource = true;
23939
23585
  return evidence;
@@ -24352,26 +23998,6 @@ const STATEFUL_HTML_DESCENDANT_TAGS = new Set([
24352
23998
  "dialog",
24353
23999
  "canvas"
24354
24000
  ]);
24355
- const STATEFUL_HTML_ATTRIBUTE_NAMES = new Set([
24356
- "autofocus",
24357
- "contenteditable",
24358
- "draggable",
24359
- "tabindex"
24360
- ]);
24361
- const isStaticallyFalseAttributeValue = (attribute) => {
24362
- if (!isNodeOfType(attribute, "JSXAttribute") || !attribute.value) return false;
24363
- const value = isNodeOfType(attribute.value, "JSXExpressionContainer") ? attribute.value.expression : attribute.value;
24364
- return isNodeOfType(value, "Literal") && (value.value === false || value.value === "false");
24365
- };
24366
- const hasStatefulHtmlAttribute = (openingElement) => {
24367
- if (!isNodeOfType(openingElement, "JSXOpeningElement")) return false;
24368
- return openingElement.attributes.some((attribute) => {
24369
- if (!isNodeOfType(attribute, "JSXAttribute") || !isNodeOfType(attribute.name, "JSXIdentifier")) return false;
24370
- const attributeName = attribute.name.name.toLowerCase();
24371
- if (!STATEFUL_HTML_ATTRIBUTE_NAMES.has(attributeName)) return false;
24372
- return attributeName === "tabindex" || !isStaticallyFalseAttributeValue(attribute);
24373
- });
24374
- };
24375
24001
  const STATEFUL_DESCENDANT_SCAN_BUDGET = 200;
24376
24002
  const rootIdentifierNameOf = (expression) => {
24377
24003
  let object = expression;
@@ -24389,8 +24015,7 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
24389
24015
  budget -= 1;
24390
24016
  const node = stack.pop();
24391
24017
  if (isNodeOfType(node, "JSXElement")) {
24392
- const opening = node.openingElement;
24393
- const name = opening.name;
24018
+ const name = node.openingElement.name;
24394
24019
  if (name && isNodeOfType(name, "JSXIdentifier")) {
24395
24020
  const tagName = name.name;
24396
24021
  const firstChar = tagName.charCodeAt(0);
@@ -24398,7 +24023,6 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
24398
24023
  if (STATEFUL_HTML_DESCENDANT_TAGS.has(tagName)) return true;
24399
24024
  }
24400
24025
  if (name && isNodeOfType(name, "JSXMemberExpression")) return true;
24401
- if (hasStatefulHtmlAttribute(opening)) return true;
24402
24026
  const children = node.children ?? [];
24403
24027
  for (const child of children) stack.push(child);
24404
24028
  continue;
@@ -24958,14 +24582,6 @@ const templateHasOuterMemberIdentity = (template, bindingFunction) => {
24958
24582
  }
24959
24583
  return false;
24960
24584
  };
24961
- const findBareItemNamesReferencedByTemplate = (template, itemNames) => {
24962
- const referencedItemNames = /* @__PURE__ */ new Set();
24963
- for (const expression of template.expressions ?? []) {
24964
- const unwrappedExpression = stripParenExpression(expression);
24965
- if (isNodeOfType(unwrappedExpression, "Identifier") && itemNames.has(unwrappedExpression.name)) referencedItemNames.add(unwrappedExpression.name);
24966
- }
24967
- return referencedItemNames;
24968
- };
24969
24585
  const forLoopTestReadsDataLength = (test) => {
24970
24586
  let didFindLengthRead = false;
24971
24587
  walkAst(test, (child) => {
@@ -25102,11 +24718,9 @@ const noArrayIndexAsKey = defineRule({
25102
24718
  } else if (STATELESS_HTML_LEAF_TAGS.has(elementName.name)) {
25103
24719
  const jsxElement = openingElement.parent;
25104
24720
  if (jsxElement && isNodeOfType(jsxElement, "JSXElement")) {
25105
- const isInlineTextRun = INLINE_TEXT_LEAF_TAGS.has(elementName.name);
25106
- const primitiveItemNames = keyTemplate ? findBareItemNamesReferencedByTemplate(keyTemplate, itemNames) : EMPTY_NAME_SET;
25107
24721
  if (!containsStatefulDescendant(jsxElement, {
25108
- memberRootNames: isInlineTextRun ? itemNames : EMPTY_NAME_SET,
25109
- bareIdentifierNames: primitiveItemNames.size > 0 ? new Set([...derivedNames, ...primitiveItemNames]) : derivedNames
24722
+ memberRootNames: INLINE_TEXT_LEAF_TAGS.has(elementName.name) ? itemNames : EMPTY_NAME_SET,
24723
+ bareIdentifierNames: derivedNames
25110
24724
  })) return;
25111
24725
  }
25112
24726
  }
@@ -25274,11 +24888,7 @@ const noAsyncEffectCallback = defineRule({
25274
24888
  severity: "warn",
25275
24889
  recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
25276
24890
  create: (context) => ({ CallExpression(node) {
25277
- if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
25278
- allowGlobalReactNamespace: true,
25279
- allowUnboundBareCalls: true,
25280
- resolveNamedAliases: true
25281
- })) return;
24891
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
25282
24892
  const callback = getEffectCallback(node);
25283
24893
  if (!callback) return;
25284
24894
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
@@ -26476,19 +26086,6 @@ const noCloneElement = defineRule({
26476
26086
  } })
26477
26087
  });
26478
26088
  //#endregion
26479
- //#region src/plugin/utils/get-call-method-name.ts
26480
- /**
26481
- * Returns the static method name of a call's callee when it's a
26482
- * non-computed MemberExpression (`obj.method` → `"method"`), or
26483
- * `null` otherwise. Used by `no-pass-data-to-parent` and
26484
- * `no-pass-live-state-to-parent` to match the receiver-bound
26485
- * method-call shape against an allow/block list.
26486
- */
26487
- const getCallMethodName = (callee) => {
26488
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
26489
- return null;
26490
- };
26491
- //#endregion
26492
26089
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
26493
26090
  const MESSAGE$31 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
26494
26091
  const CONTEXT_MODULES = [
@@ -26496,35 +26093,23 @@ const CONTEXT_MODULES = [
26496
26093
  "use-context-selector",
26497
26094
  "react-tracked"
26498
26095
  ];
26499
- const getSupportedContextImportSource = (symbol) => {
26500
- if (symbol?.kind !== "import") return null;
26501
- const importDeclaration = symbol.declarationNode.parent;
26502
- if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration") || typeof importDeclaration.source.value !== "string" || !CONTEXT_MODULES.includes(importDeclaration.source.value)) return null;
26503
- return importDeclaration.source.value;
26504
- };
26505
- const isSupportedNamespaceSymbol = (symbol) => getSupportedContextImportSource(symbol) !== null && Boolean(symbol && (isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")));
26506
- const isDestructuredCreateContextBinding = (identifier, scopes) => {
26507
- if (!isNodeOfType(identifier, "Identifier")) return false;
26508
- const symbol = scopes.symbolFor(identifier);
26509
- if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
26510
- const property = symbol.bindingIdentifier.parent;
26511
- if (!property || !isNodeOfType(property, "Property") || getStaticPropertyKeyName(property, { allowComputedString: true }) !== "createContext") return false;
26512
- const initializer = stripParenExpression(symbol.initializer);
26513
- return isNodeOfType(initializer, "Identifier") && isSupportedNamespaceSymbol(resolveConstIdentifierAlias(initializer, scopes));
26514
- };
26515
- const isCreateContextCall = (node, scopes) => {
26516
- const callee = stripParenExpression(node.callee);
26096
+ const isCreateContextCallee = (callee) => {
26517
26097
  if (isNodeOfType(callee, "Identifier")) {
26518
- if (isDestructuredCreateContextBinding(callee, scopes)) return true;
26519
- const symbol = resolveConstIdentifierAlias(callee, scopes);
26520
- return getSupportedContextImportSource(symbol) !== null && Boolean(symbol && getImportedName(symbol.declarationNode) === "createContext");
26098
+ const binding = getImportBindingForName(callee, callee.name);
26099
+ return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
26521
26100
  }
26522
- if (!isNodeOfType(callee, "MemberExpression")) return false;
26523
- if ((getCallMethodName(callee) ?? (callee.computed && isNodeOfType(callee.property, "Literal") && typeof callee.property.value === "string" ? callee.property.value : null)) !== "createContext") return false;
26524
- const receiver = stripParenExpression(callee.object);
26525
- if (!isNodeOfType(receiver, "Identifier")) return false;
26526
- if (receiver.name === "React" && scopes.isGlobalReference(receiver)) return true;
26527
- return isSupportedNamespaceSymbol(resolveConstIdentifierAlias(receiver, scopes));
26101
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
26102
+ const namespaceIdentifier = callee.object;
26103
+ const propertyIdentifier = callee.property;
26104
+ if (!isNodeOfType(namespaceIdentifier, "Identifier")) return false;
26105
+ if (!isNodeOfType(propertyIdentifier, "Identifier")) return false;
26106
+ if (propertyIdentifier.name !== "createContext") return false;
26107
+ const namespaceName = namespaceIdentifier.name;
26108
+ if (isCanonicalReactNamespaceName(namespaceName)) return true;
26109
+ const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
26110
+ return importSource !== null && CONTEXT_MODULES.includes(importSource);
26111
+ }
26112
+ return false;
26528
26113
  };
26529
26114
  const noCreateContextInRender = defineRule({
26530
26115
  id: "no-create-context-in-render",
@@ -26533,7 +26118,7 @@ const noCreateContextInRender = defineRule({
26533
26118
  category: "Correctness",
26534
26119
  recommendation: "Move `createContext(...)` outside the component, to the top level of the file, so it stays the same on every render.",
26535
26120
  create: (context) => ({ CallExpression(node) {
26536
- if (!isCreateContextCall(node, context.scopes)) return;
26121
+ if (!isCreateContextCallee(node.callee)) return;
26537
26122
  const componentOrHookName = enclosingComponentOrHookName(node);
26538
26123
  if (!componentOrHookName) return;
26539
26124
  context.report({
@@ -28483,31 +28068,20 @@ const noDocumentStartViewTransition = defineRule({
28483
28068
  } })
28484
28069
  });
28485
28070
  //#endregion
28486
- //#region src/plugin/utils/get-static-property-name.ts
28487
- const getStaticPropertyName = (memberExpression) => {
28488
- const property = memberExpression.property;
28489
- if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
28490
- if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
28491
- if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
28492
- return null;
28493
- };
28494
- //#endregion
28495
28071
  //#region src/plugin/rules/js-performance/no-document-write.ts
28496
28072
  const MESSAGE$24 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
28497
28073
  const WRITE_METHODS = new Set(["write", "writeln"]);
28498
- const isGlobalDocumentReference = (node, context) => node.name === "document" && context.scopes.isGlobalReference(node);
28499
28074
  const noDocumentWrite = defineRule({
28500
28075
  id: "no-document-write",
28501
28076
  title: "document.write/writeln",
28502
28077
  severity: "warn",
28503
28078
  recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
28504
28079
  create: (context) => ({ CallExpression(node) {
28505
- const callee = stripParenExpression(node.callee);
28506
- if (!isNodeOfType(callee, "MemberExpression")) return;
28080
+ const callee = node.callee;
28081
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
28507
28082
  const receiver = stripParenExpression(callee.object);
28508
- if (!isNodeOfType(receiver, "Identifier") || !isGlobalDocumentReference(receiver, context)) return;
28509
- const methodName = getStaticPropertyName(callee);
28510
- if (methodName === null || !WRITE_METHODS.has(methodName)) return;
28083
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
28084
+ if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
28511
28085
  context.report({
28512
28086
  node,
28513
28087
  message: MESSAGE$24
@@ -29314,14 +28888,6 @@ const noEffectWithFreshDeps = defineRule({
29314
28888
  //#endregion
29315
28889
  //#region src/plugin/rules/security/no-eval.ts
29316
28890
  const SANDBOX_SURFACE_PATH_PATTERN = /(?:^|[/-])sandbox(?:$|[/-])|(?:^|\/)[\w.]+-sandbox(?:\/|\.[cm]?[jt]sx?$)/i;
29317
- const getExecutableGlobalName = (node, context) => {
29318
- const expression = stripParenExpression(node);
29319
- if (isNodeOfType(expression, "Identifier")) return context.scopes.isGlobalReference(expression) ? expression.name : null;
29320
- if (!isNodeOfType(expression, "MemberExpression")) return null;
29321
- const receiver = stripParenExpression(expression.object);
29322
- if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "globalThis" || !context.scopes.isGlobalReference(receiver)) return null;
29323
- return getStaticPropertyName(expression);
29324
- };
29325
28891
  const noEval = defineRule({
29326
28892
  id: "no-eval",
29327
28893
  title: "eval() runs untrusted code strings",
@@ -29331,21 +28897,20 @@ const noEval = defineRule({
29331
28897
  if (SANDBOX_SURFACE_PATH_PATTERN.test(normalizeFilename(context.filename ?? ""))) return {};
29332
28898
  return {
29333
28899
  CallExpression(node) {
29334
- const executableGlobalName = getExecutableGlobalName(node.callee, context);
29335
- if (executableGlobalName === "eval") {
28900
+ if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "eval") {
29336
28901
  context.report({
29337
28902
  node,
29338
28903
  message: "eval() is a code-injection vulnerability: it runs any string as code."
29339
28904
  });
29340
28905
  return;
29341
28906
  }
29342
- if ((executableGlobalName === "setTimeout" || executableGlobalName === "setInterval") && isNodeOfType(node.arguments?.[0], "Literal") && typeof node.arguments[0].value === "string") context.report({
28907
+ if (isNodeOfType(node.callee, "Identifier") && (node.callee.name === "setTimeout" || node.callee.name === "setInterval") && isNodeOfType(node.arguments?.[0], "Literal") && typeof node.arguments[0].value === "string") context.report({
29343
28908
  node,
29344
- message: `Passing a string to ${executableGlobalName}() is a code-injection vulnerability, since it runs that string as code.`
28909
+ message: `Passing a string to ${node.callee.name}() is a code-injection vulnerability, since it runs that string as code.`
29345
28910
  });
29346
28911
  },
29347
28912
  NewExpression(node) {
29348
- if (getExecutableGlobalName(node.callee, context) === "Function") {
28913
+ if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "Function") {
29349
28914
  const onlyArgument = node.arguments.length === 1 ? node.arguments[0] : void 0;
29350
28915
  if (isNodeOfType(onlyArgument, "Literal") && typeof onlyArgument.value === "string" && onlyArgument.value.trim() === "return this") return;
29351
28916
  context.report({
@@ -30604,11 +30169,7 @@ const noFetchInEffect = defineRule({
30604
30169
  severity: "warn",
30605
30170
  recommendation: "Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from `useEffect`.",
30606
30171
  create: (context) => ({ CallExpression(node) {
30607
- if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
30608
- allowGlobalReactNamespace: true,
30609
- allowUnboundBareCalls: true,
30610
- resolveNamedAliases: true
30611
- })) return;
30172
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
30612
30173
  const callback = getEffectCallback(node);
30613
30174
  if (!callback) return;
30614
30175
  const analysisFunctions = collectEffectAnalysisFunctions(callback, context);
@@ -34818,6 +34379,19 @@ const DATA_SINK_METHOD_NAMES = new Set([
34818
34379
  "deserialize"
34819
34380
  ]);
34820
34381
  //#endregion
34382
+ //#region src/plugin/utils/get-call-method-name.ts
34383
+ /**
34384
+ * Returns the static method name of a call's callee when it's a
34385
+ * non-computed MemberExpression (`obj.method` → `"method"`), or
34386
+ * `null` otherwise. Used by `no-pass-data-to-parent` and
34387
+ * `no-pass-live-state-to-parent` to match the receiver-bound
34388
+ * method-call shape against an allow/block list.
34389
+ */
34390
+ const getCallMethodName = (callee) => {
34391
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
34392
+ return null;
34393
+ };
34394
+ //#endregion
34821
34395
  //#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
34822
34396
  const isUseStateIdentifier = (identifier) => {
34823
34397
  if (!isNodeOfType(identifier, "Identifier")) return false;
@@ -35443,30 +35017,6 @@ const guardsRenderShape = (comparison) => {
35443
35017
  }
35444
35018
  return false;
35445
35019
  };
35446
- const isPropsChildrenLength = (node, scopes) => {
35447
- const unwrappedNode = stripParenExpression(node);
35448
- return isNodeOfType(unwrappedNode, "MemberExpression") && !unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Identifier") && unwrappedNode.property.name === "length" && resolvesToPropsChildren(stripParenExpression(unwrappedNode.object), scopes);
35449
- };
35450
- const isLargeTextLengthComparison = (node, scopes) => {
35451
- const unwrappedNode = stripParenExpression(node);
35452
- if (!isNodeOfType(unwrappedNode, "BinaryExpression")) return false;
35453
- const leftIsLength = isPropsChildrenLength(unwrappedNode.left, scopes);
35454
- const rightIsLength = isPropsChildrenLength(unwrappedNode.right, scopes);
35455
- const thresholdNode = leftIsLength ? unwrappedNode.right : unwrappedNode.left;
35456
- if (!leftIsLength && !rightIsLength || !isNodeOfType(thresholdNode, "Literal")) return false;
35457
- if (typeof thresholdNode.value !== "number" || thresholdNode.value < 1e3) return false;
35458
- return leftIsLength ? unwrappedNode.operator === ">" || unwrappedNode.operator === ">=" : unwrappedNode.operator === "<" || unwrappedNode.operator === "<=";
35459
- };
35460
- const isLargeStringOptimizationGuard = (comparison, scopes) => {
35461
- let current = findTransparentExpressionRoot(comparison);
35462
- while (current.parent) {
35463
- const parent = current.parent;
35464
- if (!isNodeOfType(parent, "LogicalExpression") || parent.operator !== "&&") return false;
35465
- if (isLargeTextLengthComparison(parent.left === current ? parent.right : parent.left, scopes)) return true;
35466
- current = findTransparentExpressionRoot(parent);
35467
- }
35468
- return false;
35469
- };
35470
35020
  const noPolymorphicChildren = defineRule({
35471
35021
  id: "no-polymorphic-children",
35472
35022
  title: "Children type checked at runtime",
@@ -35480,7 +35030,6 @@ const noPolymorphicChildren = defineRule({
35480
35030
  const isStringLiteral = (operand) => isNodeOfType(operand, "Literal") && operand.value === "string";
35481
35031
  if (!isStringLiteral(node.left) && !isStringLiteral(node.right)) return;
35482
35032
  if (!guardsRenderShape(node)) return;
35483
- if (isLargeStringOptimizationGuard(node, context.scopes)) return;
35484
35033
  context.report({
35485
35034
  node,
35486
35035
  message: "Your users hit inconsistent behavior because `typeof children === \"string\"` makes this component switch on what callers pass, so add clear subcomponents like `<Button.Text>` instead."
@@ -54906,6 +54455,12 @@ const webhookSignatureRisk = defineRule({
54906
54455
  //#region src/plugin/rules/zod/utils/zod-ast.ts
54907
54456
  const ZOD_MODULE = "zod";
54908
54457
  const ZOD_MODULE_SOURCES = [ZOD_MODULE];
54458
+ const getStaticPropertyName = (member) => {
54459
+ const property = member.property;
54460
+ if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
54461
+ if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
54462
+ return null;
54463
+ };
54909
54464
  const importInfoCache = /* @__PURE__ */ new WeakMap();
54910
54465
  const getImportInfoForIdentifier = (identifier) => {
54911
54466
  if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.5-dev.22bb155",
3
+ "version": "0.7.5-dev.3075e10",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",