oxlint-plugin-react-doctor 0.7.6-dev.0d11bd5 → 0.7.6-dev.1d7b6e2

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 +918 -91
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6890,6 +6890,27 @@ const createMethodMutationAnalysis = (context) => {
6890
6890
  };
6891
6891
  };
6892
6892
  //#endregion
6893
+ //#region src/plugin/utils/collect-function-return-statements.ts
6894
+ const collectFunctionReturnStatements = (functionNode) => {
6895
+ if (!isFunctionLike$2(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return [];
6896
+ const returnStatements = [];
6897
+ walkAst(functionNode.body, (node) => {
6898
+ if (node !== functionNode.body && (isFunctionLike$2(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
6899
+ if (isNodeOfType(node, "ReturnStatement")) returnStatements.push(node);
6900
+ });
6901
+ return returnStatements;
6902
+ };
6903
+ //#endregion
6904
+ //#region src/plugin/utils/find-enclosing-function.ts
6905
+ const findEnclosingFunction = (node) => {
6906
+ let cursor = node.parent;
6907
+ while (cursor) {
6908
+ if (isFunctionLike$2(cursor)) return cursor;
6909
+ cursor = cursor.parent ?? null;
6910
+ }
6911
+ return null;
6912
+ };
6913
+ //#endregion
6893
6914
  //#region src/plugin/utils/statement-always-exits.ts
6894
6915
  const statementAlwaysExits = (statement) => {
6895
6916
  if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement")) return true;
@@ -6899,17 +6920,109 @@ const statementAlwaysExits = (statement) => {
6899
6920
  };
6900
6921
  //#endregion
6901
6922
  //#region src/plugin/utils/function-returns-matching-expression.ts
6923
+ const REASSIGNABLE_BINDING_KINDS = new Set(["let", "var"]);
6924
+ const CONDITIONAL_EXPRESSION_TYPES = new Set(["ConditionalExpression", "LogicalExpression"]);
6902
6925
  const collectReturnedExpressions = (functionNode) => {
6903
- if (!isFunctionLike$2(functionNode)) return [];
6904
- const body = functionNode.body;
6905
- if (!body) return [];
6906
- if (!isNodeOfType(body, "BlockStatement")) return [body];
6907
- const returnedExpressions = [];
6908
- walkAst(body, (node) => {
6909
- if (node !== body && (isFunctionLike$2(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
6910
- if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
6911
- });
6912
- return returnedExpressions;
6926
+ if (!isFunctionLike$2(functionNode) || !functionNode.body) return [];
6927
+ if (!isNodeOfType(functionNode.body, "BlockStatement")) return [functionNode.body];
6928
+ return collectFunctionReturnStatements(functionNode).flatMap((returnStatement) => returnStatement.argument ? [returnStatement.argument] : []);
6929
+ };
6930
+ const getAssignedExpressionForWrite = (writeIdentifier) => {
6931
+ let assignmentTarget = writeIdentifier;
6932
+ let parent = assignmentTarget.parent;
6933
+ while (parent && stripParenExpression(parent) === writeIdentifier) {
6934
+ assignmentTarget = parent;
6935
+ parent = assignmentTarget.parent;
6936
+ }
6937
+ return parent && isNodeOfType(parent, "AssignmentExpression") && parent.operator === "=" && parent.left === assignmentTarget ? parent.right : null;
6938
+ };
6939
+ const isConditionalWithinBlock = (node, block, functionControlFlow) => {
6940
+ let current = node.parent ?? null;
6941
+ while (current && functionControlFlow.blockOf(current) === block) {
6942
+ if (CONDITIONAL_EXPRESSION_TYPES.has(current.type)) return true;
6943
+ current = current.parent ?? null;
6944
+ }
6945
+ return false;
6946
+ };
6947
+ const haveSameDefinitions = (left, right) => left.size === right.size && [...left].every((definition) => right.has(definition));
6948
+ const applyDefinitions = (incomingDefinitions, definitions) => {
6949
+ let currentDefinitions = new Set(incomingDefinitions);
6950
+ for (const definition of definitions) {
6951
+ if (!definition.isConditionalWithinBlock) currentDefinitions = /* @__PURE__ */ new Set();
6952
+ currentDefinitions.add(definition);
6953
+ }
6954
+ return currentDefinitions;
6955
+ };
6956
+ const collectPossibleAssignedExpressions = (symbol, referenceNode, controlFlow) => {
6957
+ if (!REASSIGNABLE_BINDING_KINDS.has(symbol.kind)) return symbol.initializer ? [symbol.initializer] : [];
6958
+ if (!controlFlow) return [];
6959
+ const referenceFunction = findEnclosingFunction(referenceNode);
6960
+ if (findEnclosingFunction(symbol.bindingIdentifier) !== referenceFunction) return [];
6961
+ if (!referenceFunction) return [];
6962
+ const functionControlFlow = controlFlow.cfgFor(referenceFunction);
6963
+ if (!functionControlFlow) return [];
6964
+ const referenceBlock = functionControlFlow.blockOf(referenceNode);
6965
+ if (!referenceBlock) return [];
6966
+ const referencePosition = getRangeStart(referenceNode);
6967
+ const bindingPosition = getRangeStart(symbol.bindingIdentifier);
6968
+ if (referencePosition === null || bindingPosition === null) return [];
6969
+ const definitionsByBlock = /* @__PURE__ */ new Map();
6970
+ const addDefinition = (expression, definitionNode, position) => {
6971
+ const block = functionControlFlow.blockOf(definitionNode);
6972
+ if (!block) return;
6973
+ const definitions = definitionsByBlock.get(block) ?? [];
6974
+ definitions.push({
6975
+ expression,
6976
+ position,
6977
+ isConditionalWithinBlock: isConditionalWithinBlock(definitionNode, block, functionControlFlow)
6978
+ });
6979
+ definitionsByBlock.set(block, definitions);
6980
+ };
6981
+ if (symbol.initializer) addDefinition(symbol.initializer, symbol.bindingIdentifier, bindingPosition);
6982
+ for (const reference of symbol.references) {
6983
+ const writePosition = getRangeStart(reference.identifier);
6984
+ if (reference.flag === "read" || findEnclosingFunction(reference.identifier) !== referenceFunction || writePosition === null || writePosition >= referencePosition) continue;
6985
+ const assignedExpression = getAssignedExpressionForWrite(reference.identifier);
6986
+ if (!assignedExpression) continue;
6987
+ addDefinition(assignedExpression, reference.identifier, writePosition);
6988
+ }
6989
+ for (const definitions of definitionsByBlock.values()) definitions.sort((left, right) => left.position - right.position);
6990
+ const incomingDefinitionsByBlock = /* @__PURE__ */ new Map();
6991
+ const outgoingDefinitionsByBlock = /* @__PURE__ */ new Map();
6992
+ const reachableBlocks = new Set([functionControlFlow.entry]);
6993
+ const pendingBlocks = [functionControlFlow.entry];
6994
+ while (pendingBlocks.length > 0) {
6995
+ const block = pendingBlocks.pop();
6996
+ if (!block) break;
6997
+ for (const edge of block.successors) {
6998
+ if (reachableBlocks.has(edge.to)) continue;
6999
+ reachableBlocks.add(edge.to);
7000
+ pendingBlocks.push(edge.to);
7001
+ }
7002
+ }
7003
+ if (!reachableBlocks.has(referenceBlock)) return [];
7004
+ let didDefinitionsChange = true;
7005
+ while (didDefinitionsChange) {
7006
+ didDefinitionsChange = false;
7007
+ for (const block of functionControlFlow.blocks) {
7008
+ if (!reachableBlocks.has(block)) continue;
7009
+ const incomingDefinitions = /* @__PURE__ */ new Set();
7010
+ for (const predecessor of block.predecessors) {
7011
+ if (!reachableBlocks.has(predecessor.from)) continue;
7012
+ for (const definition of outgoingDefinitionsByBlock.get(predecessor.from) ?? []) incomingDefinitions.add(definition);
7013
+ }
7014
+ const outgoingDefinitions = applyDefinitions(incomingDefinitions, definitionsByBlock.get(block) ?? []);
7015
+ const previousIncomingDefinitions = incomingDefinitionsByBlock.get(block) ?? /* @__PURE__ */ new Set();
7016
+ const previousOutgoingDefinitions = outgoingDefinitionsByBlock.get(block) ?? /* @__PURE__ */ new Set();
7017
+ if (!haveSameDefinitions(incomingDefinitions, previousIncomingDefinitions) || !haveSameDefinitions(outgoingDefinitions, previousOutgoingDefinitions)) {
7018
+ incomingDefinitionsByBlock.set(block, incomingDefinitions);
7019
+ outgoingDefinitionsByBlock.set(block, outgoingDefinitions);
7020
+ didDefinitionsChange = true;
7021
+ }
7022
+ }
7023
+ }
7024
+ const definitionsBeforeReference = (definitionsByBlock.get(referenceBlock) ?? []).filter((definition) => definition.position < referencePosition);
7025
+ return [...applyDefinitions(incomingDefinitionsByBlock.get(referenceBlock) ?? /* @__PURE__ */ new Set(), definitionsBeforeReference)].map((definition) => definition.expression);
6913
7026
  };
6914
7027
  const functionHasBareReturn = (functionNode) => {
6915
7028
  if (!isFunctionLike$2(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return false;
@@ -6921,7 +7034,7 @@ const functionHasBareReturn = (functionNode) => {
6921
7034
  });
6922
7035
  return didFindBareReturn;
6923
7036
  };
6924
- const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression, matchMode = "some") => {
7037
+ const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression, controlFlow, matchMode = "some") => {
6925
7038
  const visitedExpressions = /* @__PURE__ */ new Set();
6926
7039
  const visitedFunctions = /* @__PURE__ */ new Set();
6927
7040
  const functionMatches = (candidateFunction) => {
@@ -6938,10 +7051,11 @@ const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpressi
6938
7051
  if (matchesExpression(unwrappedExpression)) return true;
6939
7052
  if (isNodeOfType(unwrappedExpression, "Identifier")) {
6940
7053
  const symbol = scopes.symbolFor(unwrappedExpression);
6941
- if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
6942
- const initializer = stripParenExpression(symbol.initializer);
6943
- if (isFunctionLike$2(initializer)) return false;
6944
- return expressionMatches(initializer);
7054
+ if (!symbol || symbol.kind !== "const" && !REASSIGNABLE_BINDING_KINDS.has(symbol.kind)) return false;
7055
+ return collectPossibleAssignedExpressions(symbol, unwrappedExpression, controlFlow).some((assignedExpression) => {
7056
+ const assignedValue = stripParenExpression(assignedExpression);
7057
+ return !isFunctionLike$2(assignedValue) && expressionMatches(assignedValue);
7058
+ });
6945
7059
  }
6946
7060
  if (isNodeOfType(unwrappedExpression, "CallExpression")) {
6947
7061
  if (unwrappedExpression.arguments.length !== 0) return false;
@@ -7342,7 +7456,7 @@ const isProvenBrowserApiReceiver = (receiver, receiverKind, scopes, visitedSymbo
7342
7456
  const callee = stripParenExpression(expression.callee);
7343
7457
  if (isDomEventTarget && isNodeOfType(callee, "MemberExpression") && DOM_EVENT_TARGET_FACTORY_METHOD_NAMES.has(getStaticPropertyName(callee) ?? "") && isProvenBrowserApiReceiver(callee.object, receiverKind, scopes, visitedSymbolIds)) return true;
7344
7458
  const calledFunction = getSameFileCalledFunction(expression, scopes, visitedSymbolIds);
7345
- return Boolean(calledFunction && functionReturnsMatchingExpression(calledFunction, scopes, (returnedExpression) => isProvenBrowserApiReceiver(returnedExpression, receiverKind, scopes, new Set(visitedSymbolIds)), "every"));
7459
+ return Boolean(calledFunction && functionReturnsMatchingExpression(calledFunction, scopes, (returnedExpression) => isProvenBrowserApiReceiver(returnedExpression, receiverKind, scopes, new Set(visitedSymbolIds)), void 0, "every"));
7346
7460
  }
7347
7461
  if (!isNodeOfType(expression, "MemberExpression")) return false;
7348
7462
  const classMember = getClassMemberDefinition(expression);
@@ -10816,16 +10930,6 @@ const componentOrHookDisplayNameForFunction = (functionNode) => {
10816
10930
  return displayNameFromFunctionBinding(functionNode);
10817
10931
  };
10818
10932
  //#endregion
10819
- //#region src/plugin/utils/find-enclosing-function.ts
10820
- const findEnclosingFunction = (node) => {
10821
- let cursor = node.parent;
10822
- while (cursor) {
10823
- if (isFunctionLike$2(cursor)) return cursor;
10824
- cursor = cursor.parent ?? null;
10825
- }
10826
- return null;
10827
- };
10828
- //#endregion
10829
10933
  //#region src/plugin/utils/enclosing-component-or-hook-name.ts
10830
10934
  const enclosingComponentOrHookName = (node) => {
10831
10935
  const functionNode = findEnclosingFunction(node);
@@ -16539,13 +16643,211 @@ const createLoopAwareVisitors = (innerVisitors, options = {}) => {
16539
16643
  //#region src/plugin/rules/js-performance/js-hoist-regexp.ts
16540
16644
  const isStaticPattern = (argument) => {
16541
16645
  if (!argument) return false;
16542
- if (isNodeOfType(argument, "Literal")) return true;
16543
- return isNodeOfType(argument, "TemplateLiteral") && (argument.expressions?.length ?? 0) === 0;
16646
+ const unwrappedArgument = stripParenExpression(argument);
16647
+ return isNodeOfType(unwrappedArgument, "Literal") || isNodeOfType(unwrappedArgument, "TemplateLiteral") && (unwrappedArgument.expressions?.length ?? 0) === 0;
16648
+ };
16649
+ const STATEFUL_REGEXP_FLAGS_PATTERN = /[gy]/;
16650
+ const VALID_REGEXP_FLAGS_PATTERN = /^[dgimsuvy]*$/;
16651
+ const GLOBAL_OBJECT_NAMES = new Set([
16652
+ "globalThis",
16653
+ "global",
16654
+ "window",
16655
+ "self"
16656
+ ]);
16657
+ const GLOBAL_BUILTIN_NAMES = new Set([
16658
+ "Object",
16659
+ "Reflect",
16660
+ "String",
16661
+ "RegExp"
16662
+ ]);
16663
+ const STRING_PROTOTYPE_PATH = "String.prototype";
16664
+ const REGEXP_PROTOTYPE_PATH = "RegExp.prototype";
16665
+ const getStaticStringValue = (argument) => {
16666
+ if (!argument) return null;
16667
+ const unwrappedArgument = stripParenExpression(argument);
16668
+ if (isNodeOfType(unwrappedArgument, "Literal") && typeof unwrappedArgument.value === "string") return unwrappedArgument.value;
16669
+ if (isNodeOfType(unwrappedArgument, "TemplateLiteral")) return getStaticTemplateLiteralValue(unwrappedArgument);
16670
+ return null;
16671
+ };
16672
+ const getEffectiveRegExpFlags = (patternArgument, flagsArgument) => {
16673
+ if (flagsArgument) return getStaticStringValue(flagsArgument);
16674
+ if (!patternArgument) return "";
16675
+ const unwrappedPattern = stripParenExpression(patternArgument);
16676
+ if (isNodeOfType(unwrappedPattern, "Literal") && unwrappedPattern.value instanceof RegExp) return unwrappedPattern.value.flags;
16677
+ return "";
16678
+ };
16679
+ const hasValidRegExpFlags = (flags) => VALID_REGEXP_FLAGS_PATTERN.test(flags) && new Set(flags).size === flags.length && !(flags.includes("u") && flags.includes("v"));
16680
+ const globSyncReturnsStringPaths = (node, context) => {
16681
+ if (node.arguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return false;
16682
+ const callee = stripParenExpression(node.callee);
16683
+ let isGlobSyncImport = false;
16684
+ if (isNodeOfType(callee, "Identifier")) isGlobSyncImport = context.scopes.symbolFor(callee)?.kind === "import" && getImportedNameFromModule(callee, callee.name, "glob") === "globSync";
16685
+ else if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "globSync") isGlobSyncImport = context.scopes.symbolFor(callee.object)?.kind === "import" && isNamespaceImportFromModule(callee.object, callee.object.name, "glob");
16686
+ if (!isGlobSyncImport) return false;
16687
+ const options = node.arguments[1];
16688
+ if (!options) return true;
16689
+ const unwrappedOptions = stripParenExpression(options);
16690
+ if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) return false;
16691
+ for (const property of unwrappedOptions.properties) {
16692
+ if (!isNodeOfType(property, "Property")) return false;
16693
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
16694
+ if (propertyName === null) return false;
16695
+ if (propertyName !== "withFileTypes") continue;
16696
+ const propertyValue = stripParenExpression(property.value);
16697
+ if (!isNodeOfType(propertyValue, "Literal") || propertyValue.value !== false) return false;
16698
+ }
16699
+ return true;
16700
+ };
16701
+ const isGlobSyncStringIterationBinding = (bindingIdentifier, context) => {
16702
+ const declarator = bindingIdentifier.parent;
16703
+ if (!isNodeOfType(declarator, "VariableDeclarator") || declarator.id !== bindingIdentifier) return false;
16704
+ const declaration = declarator.parent;
16705
+ const loop = declaration?.parent;
16706
+ if (!isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const" || !isNodeOfType(loop, "ForOfStatement") || loop.left !== declaration) return false;
16707
+ const iteratedValue = stripParenExpression(loop.right);
16708
+ return isNodeOfType(iteratedValue, "CallExpression") && globSyncReturnsStringPaths(iteratedValue, context);
16709
+ };
16710
+ const isProvenNativeStringReceiver = (node, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
16711
+ const receiver = stripParenExpression(node);
16712
+ if (isNodeOfType(receiver, "Literal")) return typeof receiver.value === "string";
16713
+ if (isNodeOfType(receiver, "TemplateLiteral")) return true;
16714
+ if (isNodeOfType(receiver, "CallExpression") && isNodeOfType(receiver.callee, "Identifier") && receiver.callee.name === "String" && context.scopes.isGlobalReference(receiver.callee)) return true;
16715
+ if (!isNodeOfType(receiver, "Identifier")) return false;
16716
+ const symbol = context.scopes.symbolFor(receiver);
16717
+ if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
16718
+ if (isNodeOfType(symbol.bindingIdentifier, "Identifier") && isNodeOfType(symbol.bindingIdentifier.typeAnnotation?.typeAnnotation, "TSStringKeyword")) return true;
16719
+ if (isGlobSyncStringIterationBinding(symbol.bindingIdentifier, context)) return true;
16720
+ if (symbol.kind !== "const" || !symbol.initializer) return false;
16721
+ visitedSymbolIds.add(symbol.id);
16722
+ return isProvenNativeStringReceiver(symbol.initializer, context, visitedSymbolIds);
16723
+ };
16724
+ const isSafeStatefulReplaceAllSearch = (node, flags, context) => {
16725
+ if (!flags.includes("g")) return false;
16726
+ const searchArgument = findTransparentExpressionRoot(node);
16727
+ const replaceAllCall = searchArgument.parent;
16728
+ if (!isNodeOfType(replaceAllCall, "CallExpression") || replaceAllCall.arguments[0] !== searchArgument) return false;
16729
+ const callee = stripParenExpression(replaceAllCall.callee);
16730
+ return isNodeOfType(callee, "MemberExpression") && !callee.computed && !callee.optional && !replaceAllCall.optional && isNodeOfType(callee.property, "Identifier") && callee.property.name === "replaceAll" && isProvenNativeStringReceiver(callee.object, context);
16731
+ };
16732
+ const getDestructuredBindingPropertyName = (bindingIdentifier) => {
16733
+ let bindingNode = bindingIdentifier;
16734
+ if (isNodeOfType(bindingNode.parent, "AssignmentPattern") && bindingNode.parent.left === bindingNode) bindingNode = bindingNode.parent;
16735
+ const property = bindingNode.parent;
16736
+ if (!isNodeOfType(property, "Property") || property.value !== bindingNode || !isNodeOfType(property.parent, "ObjectPattern")) return null;
16737
+ return getStaticPropertyKeyName(property, { allowComputedString: true });
16738
+ };
16739
+ const extendGlobalPath = (basePath, propertyName) => {
16740
+ if (basePath === "global") {
16741
+ if (propertyName === null || GLOBAL_OBJECT_NAMES.has(propertyName)) return "global";
16742
+ return GLOBAL_BUILTIN_NAMES.has(propertyName) ? propertyName : null;
16743
+ }
16744
+ if ((basePath === "Object" || basePath === "Reflect") && propertyName) return `${basePath}.${propertyName}`;
16745
+ if ((basePath === "String" || basePath === "RegExp") && propertyName === "prototype") return `${basePath}.prototype`;
16746
+ if ((basePath === STRING_PROTOTYPE_PATH || basePath === REGEXP_PROTOTYPE_PATH) && propertyName) return `${basePath}.${propertyName}`;
16747
+ return null;
16748
+ };
16749
+ const getGlobalPath = (node, context, symbolCache) => {
16750
+ const unwrappedNode = stripParenExpression(node);
16751
+ if (isNodeOfType(unwrappedNode, "Identifier")) {
16752
+ if (context.scopes.isGlobalReference(unwrappedNode)) {
16753
+ if (GLOBAL_OBJECT_NAMES.has(unwrappedNode.name)) return "global";
16754
+ return GLOBAL_BUILTIN_NAMES.has(unwrappedNode.name) ? unwrappedNode.name : null;
16755
+ }
16756
+ const symbol = context.scopes.symbolFor(unwrappedNode);
16757
+ if (symbol?.kind !== "const" || !symbol.initializer) return null;
16758
+ const cachedResult = symbolCache.get(symbol.id);
16759
+ if (cachedResult !== void 0) return cachedResult || null;
16760
+ symbolCache.set(symbol.id, false);
16761
+ if (!symbol.references.every((reference) => reference.flag === "read")) return null;
16762
+ const initializerPath = getGlobalPath(symbol.initializer, context, symbolCache);
16763
+ const destructuredPropertyName = getDestructuredBindingPropertyName(symbol.bindingIdentifier);
16764
+ const resolvedPath = destructuredPropertyName ? initializerPath && extendGlobalPath(initializerPath, destructuredPropertyName) : initializerPath;
16765
+ symbolCache.set(symbol.id, resolvedPath ?? false);
16766
+ return resolvedPath;
16767
+ }
16768
+ if (!isNodeOfType(unwrappedNode, "MemberExpression")) return null;
16769
+ const objectPath = getGlobalPath(unwrappedNode.object, context, symbolCache);
16770
+ if (!objectPath) return null;
16771
+ return extendGlobalPath(objectPath, getStaticPropertyName(unwrappedNode));
16544
16772
  };
16545
- const isStaticRegExpConstruction = (node) => {
16773
+ const strongerRegExpEnvironmentHazard = (first, second) => {
16774
+ if (first === "globalRegExpReplaced" || second === "globalRegExpReplaced") return "globalRegExpReplaced";
16775
+ if (first === "replaceAllIntegrityLost" || second === "replaceAllIntegrityLost") return "replaceAllIntegrityLost";
16776
+ return "none";
16777
+ };
16778
+ const getAssignmentTargetRegExpHazard = (node, context, symbolCache) => {
16779
+ if (!node) return "none";
16780
+ const target = stripParenExpression(node);
16781
+ if (isNodeOfType(target, "MemberExpression")) {
16782
+ const objectPath = getGlobalPath(target.object, context, symbolCache);
16783
+ const propertyName = getStaticPropertyName(target);
16784
+ if (objectPath === "global") return propertyName === null || propertyName === "RegExp" ? "globalRegExpReplaced" : "none";
16785
+ if (objectPath === REGEXP_PROTOTYPE_PATH) return "replaceAllIntegrityLost";
16786
+ return objectPath === STRING_PROTOTYPE_PATH && (propertyName === null || propertyName === "replaceAll") ? "replaceAllIntegrityLost" : "none";
16787
+ }
16788
+ if (isNodeOfType(target, "AssignmentPattern")) return getAssignmentTargetRegExpHazard(target.left, context, symbolCache);
16789
+ if (isNodeOfType(target, "RestElement")) return getAssignmentTargetRegExpHazard(target.argument, context, symbolCache);
16790
+ if (isNodeOfType(target, "ArrayPattern")) return target.elements.reduce((strongest, element) => strongerRegExpEnvironmentHazard(strongest, getAssignmentTargetRegExpHazard(element, context, symbolCache)), "none");
16791
+ if (isNodeOfType(target, "ObjectPattern")) return target.properties.reduce((strongest, property) => strongerRegExpEnvironmentHazard(strongest, getAssignmentTargetRegExpHazard(isNodeOfType(property, "Property") ? property.value : property, context, symbolCache)), "none");
16792
+ return "none";
16793
+ };
16794
+ const getWriteTarget = (node) => {
16795
+ if (isNodeOfType(node, "AssignmentExpression")) return node.left;
16796
+ if (isNodeOfType(node, "UpdateExpression")) return node.argument;
16797
+ if (isNodeOfType(node, "UnaryExpression") && node.operator === "delete") return node.argument;
16798
+ if (isNodeOfType(node, "ForInStatement") || isNodeOfType(node, "ForOfStatement")) return node.left;
16799
+ return null;
16800
+ };
16801
+ const objectExpressionMayDefineProperty = (node, targetPropertyName) => {
16802
+ if (!node) return true;
16803
+ const unwrappedNode = stripParenExpression(node);
16804
+ if (!isNodeOfType(unwrappedNode, "ObjectExpression")) return true;
16805
+ return unwrappedNode.properties.some((property) => {
16806
+ if (!isNodeOfType(property, "Property")) return true;
16807
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
16808
+ return propertyName === null || propertyName === targetPropertyName;
16809
+ });
16810
+ };
16811
+ const getCallRegExpHazard = (node, context, symbolCache) => {
16812
+ const methodName = getGlobalPath(node.callee, context, symbolCache);
16813
+ const target = node.arguments?.[0];
16814
+ if (!target) return "none";
16815
+ const targetPath = getGlobalPath(target, context, symbolCache);
16816
+ const isSinglePropertyMutation = methodName === "Object.defineProperty" || methodName === "Reflect.set" || methodName === "Reflect.defineProperty" || methodName === "Reflect.deleteProperty";
16817
+ const isPropertyCollectionMutation = methodName === "Object.defineProperties" || methodName === "Object.assign";
16818
+ if (targetPath === REGEXP_PROTOTYPE_PATH) return isSinglePropertyMutation || isPropertyCollectionMutation || methodName === "Object.setPrototypeOf" || methodName === "Reflect.setPrototypeOf" ? "replaceAllIntegrityLost" : "none";
16819
+ if (targetPath !== STRING_PROTOTYPE_PATH && targetPath !== "global") return "none";
16820
+ const mutationHazard = targetPath === "global" ? "globalRegExpReplaced" : "replaceAllIntegrityLost";
16821
+ const guardedPropertyName = targetPath === "global" ? "RegExp" : "replaceAll";
16822
+ if (isSinglePropertyMutation) {
16823
+ const propertyName = getStaticStringValue(node.arguments?.[1]);
16824
+ return propertyName === null || propertyName === guardedPropertyName ? mutationHazard : "none";
16825
+ }
16826
+ if (!isPropertyCollectionMutation) return "none";
16827
+ return (methodName === "Object.defineProperties" && targetPath === "global" ? [node.arguments?.[1]] : node.arguments?.slice(1) ?? []).some((source) => objectExpressionMayDefineProperty(source, guardedPropertyName)) ? mutationHazard : "none";
16828
+ };
16829
+ const scopeTreeWritesGlobalRegExp = (scope) => scope.references.some((reference) => reference.resolvedSymbol === null && reference.flag !== "read" && isNodeOfType(reference.identifier, "Identifier") && reference.identifier.name === "RegExp") || scope.children.some(scopeTreeWritesGlobalRegExp);
16830
+ const scanRegExpEnvironmentHazard = (context) => {
16831
+ if (scopeTreeWritesGlobalRegExp(context.scopes.rootScope)) return "globalRegExpReplaced";
16832
+ let strongestHazard = "none";
16833
+ const symbolCache = /* @__PURE__ */ new Map();
16834
+ walkAst(context.scopes.rootScope.node, (node) => {
16835
+ if (strongestHazard === "globalRegExpReplaced") return false;
16836
+ const writeTarget = getWriteTarget(node);
16837
+ const nodeHazard = writeTarget ? getAssignmentTargetRegExpHazard(writeTarget, context, symbolCache) : isNodeOfType(node, "CallExpression") ? getCallRegExpHazard(node, context, symbolCache) : "none";
16838
+ strongestHazard = strongerRegExpEnvironmentHazard(strongestHazard, nodeHazard);
16839
+ if (strongestHazard === "globalRegExpReplaced") return false;
16840
+ });
16841
+ return strongestHazard;
16842
+ };
16843
+ const getHoistableRegExpConstructionKind = (node, context) => {
16546
16844
  const patternArgument = node.arguments?.[0];
16547
16845
  const flagsArgument = node.arguments?.[1];
16548
- return isNodeOfType(node.callee, "Identifier") && node.callee.name === "RegExp" && isStaticPattern(patternArgument) && (flagsArgument === void 0 || isStaticPattern(flagsArgument));
16846
+ const callee = stripParenExpression(node.callee);
16847
+ const effectiveFlags = getEffectiveRegExpFlags(patternArgument, flagsArgument);
16848
+ if (!isNodeOfType(callee, "Identifier") || callee.name !== "RegExp" || !context.scopes.isGlobalReference(callee) || !isStaticPattern(patternArgument) || effectiveFlags === null || !hasValidRegExpFlags(effectiveFlags)) return null;
16849
+ if (!STATEFUL_REGEXP_FLAGS_PATTERN.test(effectiveFlags)) return "stateless";
16850
+ return isSafeStatefulReplaceAllSearch(node, effectiveFlags, context) ? "statefulReplaceAll" : null;
16549
16851
  };
16550
16852
  const MESSAGE$48 = "`new RegExp()` rebuilds the pattern on every loop pass. Move it to a constant outside the loop.";
16551
16853
  const jsHoistRegexp = defineRule({
@@ -16554,20 +16856,24 @@ const jsHoistRegexp = defineRule({
16554
16856
  tags: ["test-noise"],
16555
16857
  severity: "warn",
16556
16858
  recommendation: "Move `new RegExp(...)` (or large regex literals) to a constant outside the loop so it isn't rebuilt on every pass",
16557
- create: (context) => createLoopAwareVisitors({
16558
- NewExpression(node) {
16559
- if (isStaticRegExpConstruction(node)) context.report({
16560
- node,
16561
- message: MESSAGE$48
16562
- });
16563
- },
16564
- CallExpression(node) {
16565
- if (isStaticRegExpConstruction(node)) context.report({
16859
+ create: (context) => {
16860
+ let cachedEnvironmentHazard = null;
16861
+ const reportHoistableRegExpConstruction = (node) => {
16862
+ const constructionKind = getHoistableRegExpConstructionKind(node, context);
16863
+ if (constructionKind === null) return;
16864
+ cachedEnvironmentHazard ??= scanRegExpEnvironmentHazard(context);
16865
+ if (cachedEnvironmentHazard === "globalRegExpReplaced") return;
16866
+ if (constructionKind === "statefulReplaceAll" && cachedEnvironmentHazard === "replaceAllIntegrityLost") return;
16867
+ context.report({
16566
16868
  node,
16567
16869
  message: MESSAGE$48
16568
16870
  });
16569
- }
16570
- }, { treatIteratorCallbacksAsLoops: true })
16871
+ };
16872
+ return createLoopAwareVisitors({
16873
+ NewExpression: reportHoistableRegExpConstruction,
16874
+ CallExpression: reportHoistableRegExpConstruction
16875
+ }, { treatIteratorCallbacksAsLoops: true });
16876
+ }
16571
16877
  });
16572
16878
  //#endregion
16573
16879
  //#region src/plugin/utils/resolve-first-argument-binding.ts
@@ -16971,12 +17277,23 @@ const jsLengthCheckFirst = defineRule({
16971
17277
  });
16972
17278
  //#endregion
16973
17279
  //#region src/plugin/rules/js-performance/js-min-max-loop.ts
17280
+ const builtinMutationByProgram = /* @__PURE__ */ new WeakMap();
17281
+ const RUNTIMELESS_SYMBOL_KINDS = new Set(["ts-interface", "ts-type-alias"]);
17282
+ const PROTOTYPE_METHOD_NAMES = new Set(["getPrototypeOf"]);
17283
+ const OBJECT_ASSIGN_METHOD_NAMES = new Set(["assign"]);
17284
+ const OBJECT_DEFINE_PROPERTIES_METHOD_NAMES = new Set(["defineProperties"]);
17285
+ const KEYED_MUTATION_METHOD_NAMES = new Set([
17286
+ "defineProperty",
17287
+ "deleteProperty",
17288
+ "set"
17289
+ ]);
16974
17290
  const numericComparatorDirection = (comparator) => {
16975
- if (!isInlineFunctionExpression(comparator)) return null;
17291
+ if (!isInlineFunctionExpression(comparator) || comparator.async || comparator.generator) return null;
16976
17292
  const parameters = comparator.params ?? [];
16977
17293
  if (parameters.length !== 2) return null;
16978
17294
  const [firstParameter, secondParameter] = parameters;
16979
17295
  if (!isNodeOfType(firstParameter, "Identifier") || !isNodeOfType(secondParameter, "Identifier")) return null;
17296
+ if (firstParameter.name === secondParameter.name) return null;
16980
17297
  let comparisonExpression = null;
16981
17298
  const comparatorBody = stripParenExpression(comparator.body);
16982
17299
  if (isNodeOfType(comparatorBody, "BinaryExpression")) comparisonExpression = comparatorBody;
@@ -16994,6 +17311,150 @@ const numericComparatorDirection = (comparator) => {
16994
17311
  if (leftName === secondParameter.name && rightName === firstParameter.name) return "descending";
16995
17312
  return null;
16996
17313
  };
17314
+ const getStaticFiniteNumericValue = (expression) => {
17315
+ const strippedExpression = stripParenExpression(expression);
17316
+ if (isNodeOfType(strippedExpression, "Literal")) return typeof strippedExpression.value === "number" && Number.isFinite(strippedExpression.value) ? strippedExpression.value : null;
17317
+ if (!isNodeOfType(strippedExpression, "UnaryExpression") || strippedExpression.operator !== "+" && strippedExpression.operator !== "-") return null;
17318
+ const argumentValue = getStaticFiniteNumericValue(strippedExpression.argument);
17319
+ if (argumentValue === null) return null;
17320
+ return strippedExpression.operator === "-" ? -argumentValue : argumentValue;
17321
+ };
17322
+ const isSafeFreshNumericArray = (arrayExpression) => {
17323
+ if (arrayExpression.elements.length === 0 || arrayExpression.elements.length > 1024) return false;
17324
+ let didFindPositiveZero = false;
17325
+ let didFindNegativeZero = false;
17326
+ for (const element of arrayExpression.elements) {
17327
+ if (!element || isNodeOfType(element, "SpreadElement")) return false;
17328
+ const numericValue = getStaticFiniteNumericValue(element);
17329
+ if (numericValue === null) return false;
17330
+ if (Object.is(numericValue, 0)) didFindPositiveZero = true;
17331
+ if (Object.is(numericValue, -0)) didFindNegativeZero = true;
17332
+ }
17333
+ return !(didFindPositiveZero && didFindNegativeZero);
17334
+ };
17335
+ const isGlobalObjectReference = (expression, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
17336
+ const strippedExpression = stripParenExpression(expression);
17337
+ if (!isNodeOfType(strippedExpression, "Identifier")) return false;
17338
+ if ((strippedExpression.name === "globalThis" || strippedExpression.name === "window" || strippedExpression.name === "self" || strippedExpression.name === "global") && scopes.isGlobalReference(strippedExpression)) return true;
17339
+ const symbol = scopes.symbolFor(strippedExpression);
17340
+ if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbols.has(symbol.id)) return false;
17341
+ visitedSymbols.add(symbol.id);
17342
+ return isGlobalObjectReference(symbol.initializer, scopes, visitedSymbols);
17343
+ };
17344
+ const resolvesToGlobalNamespace = (expression, namespaceName, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
17345
+ const strippedExpression = stripParenExpression(expression);
17346
+ if (isNodeOfType(strippedExpression, "Identifier")) {
17347
+ if (strippedExpression.name === namespaceName && scopes.isGlobalReference(strippedExpression)) return true;
17348
+ const symbol = scopes.symbolFor(strippedExpression);
17349
+ if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbols.has(symbol.id)) return false;
17350
+ visitedSymbols.add(symbol.id);
17351
+ return resolvesToGlobalNamespace(symbol.initializer, namespaceName, scopes, visitedSymbols);
17352
+ }
17353
+ return isNodeOfType(strippedExpression, "MemberExpression") && getStaticPropertyName(strippedExpression) === namespaceName && isGlobalObjectReference(strippedExpression.object, scopes);
17354
+ };
17355
+ const resolvesToGlobalMethod = (expression, namespaceName, methodNames, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
17356
+ const strippedExpression = stripParenExpression(expression);
17357
+ if (isNodeOfType(strippedExpression, "Identifier")) {
17358
+ const symbol = scopes.symbolFor(strippedExpression);
17359
+ if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbols.has(symbol.id)) return false;
17360
+ visitedSymbols.add(symbol.id);
17361
+ return resolvesToGlobalMethod(symbol.initializer, namespaceName, methodNames, scopes, visitedSymbols);
17362
+ }
17363
+ return isNodeOfType(strippedExpression, "MemberExpression") && methodNames.has(getStaticPropertyName(strippedExpression) ?? "") && resolvesToGlobalNamespace(strippedExpression.object, namespaceName, scopes);
17364
+ };
17365
+ const resolvesToNativeArrayPrototype = (expression, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
17366
+ const strippedExpression = stripParenExpression(expression);
17367
+ if (isNodeOfType(strippedExpression, "Identifier")) {
17368
+ const symbol = scopes.symbolFor(strippedExpression);
17369
+ if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbols.has(symbol.id)) return false;
17370
+ visitedSymbols.add(symbol.id);
17371
+ return resolvesToNativeArrayPrototype(symbol.initializer, scopes, visitedSymbols);
17372
+ }
17373
+ if (isNodeOfType(strippedExpression, "MemberExpression")) {
17374
+ const propertyName = getStaticPropertyName(strippedExpression);
17375
+ if (propertyName === "prototype") return resolvesToGlobalNamespace(strippedExpression.object, "Array", scopes);
17376
+ return propertyName === "__proto__" && isNodeOfType(stripParenExpression(strippedExpression.object), "ArrayExpression");
17377
+ }
17378
+ if (!isNodeOfType(strippedExpression, "CallExpression")) return false;
17379
+ if (!resolvesToGlobalMethod(strippedExpression.callee, "Object", PROTOTYPE_METHOD_NAMES, scopes) && !resolvesToGlobalMethod(strippedExpression.callee, "Reflect", PROTOTYPE_METHOD_NAMES, scopes)) return false;
17380
+ const argument = strippedExpression.arguments[0];
17381
+ return Boolean(argument && isNodeOfType(stripParenExpression(argument), "ArrayExpression"));
17382
+ };
17383
+ const isGlobalNamespaceReplacementTarget = (target, namespaceName, scopes) => {
17384
+ const strippedTarget = stripParenExpression(target);
17385
+ if (isNodeOfType(strippedTarget, "Identifier")) return strippedTarget.name === namespaceName && scopes.isGlobalReference(strippedTarget);
17386
+ return isNodeOfType(strippedTarget, "MemberExpression") && getStaticPropertyName(strippedTarget) === namespaceName && isGlobalObjectReference(strippedTarget.object, scopes);
17387
+ };
17388
+ const isUnsafeBuiltinMemberTarget = (target, targetFunction, scopes) => {
17389
+ const strippedTarget = stripParenExpression(target);
17390
+ if (!isNodeOfType(strippedTarget, "MemberExpression")) return false;
17391
+ const propertyName = getStaticPropertyName(strippedTarget);
17392
+ if (resolvesToNativeArrayPrototype(strippedTarget.object, scopes)) return propertyName === null || propertyName === "sort";
17393
+ if (resolvesToGlobalNamespace(strippedTarget.object, "Math", scopes)) return propertyName === null || propertyName === targetFunction;
17394
+ return isGlobalObjectReference(strippedTarget.object, scopes) && (propertyName === null || propertyName === "Math");
17395
+ };
17396
+ const isUnsafeBuiltinMutationApiCall = (callExpression, targetFunction, scopes) => {
17397
+ const target = callExpression.arguments[0];
17398
+ if (!target) return false;
17399
+ let propertyName = null;
17400
+ if (resolvesToNativeArrayPrototype(target, scopes)) propertyName = "sort";
17401
+ else if (resolvesToGlobalNamespace(target, "Math", scopes)) propertyName = targetFunction;
17402
+ else if (isGlobalObjectReference(target, scopes)) propertyName = "Math";
17403
+ if (!propertyName) return false;
17404
+ const canObjectExpressionSetProperty = (properties) => {
17405
+ if (!isNodeOfType(properties, "ObjectExpression")) return true;
17406
+ return properties.properties.some((property) => {
17407
+ if (isNodeOfType(property, "SpreadElement")) return true;
17408
+ const keyName = getStaticPropertyKeyName(property, { allowComputedString: true });
17409
+ return keyName === propertyName || keyName === null;
17410
+ });
17411
+ };
17412
+ if (resolvesToGlobalMethod(callExpression.callee, "Object", OBJECT_ASSIGN_METHOD_NAMES, scopes)) return callExpression.arguments.slice(1).some((properties) => canObjectExpressionSetProperty(properties));
17413
+ if (resolvesToGlobalMethod(callExpression.callee, "Object", OBJECT_DEFINE_PROPERTIES_METHOD_NAMES, scopes)) {
17414
+ const properties = callExpression.arguments[1];
17415
+ return !properties || canObjectExpressionSetProperty(properties);
17416
+ }
17417
+ if (!resolvesToGlobalMethod(callExpression.callee, "Object", KEYED_MUTATION_METHOD_NAMES, scopes) && !resolvesToGlobalMethod(callExpression.callee, "Reflect", KEYED_MUTATION_METHOD_NAMES, scopes)) return false;
17418
+ const propertyArgument = callExpression.arguments[1];
17419
+ if (!propertyArgument) return true;
17420
+ return isNodeOfType(propertyArgument, "Literal") ? propertyArgument.value === propertyName : true;
17421
+ };
17422
+ const hasUnsafeBuiltinMutation = (node, targetFunction, scopes) => {
17423
+ const programRoot = findProgramRoot(node);
17424
+ if (!programRoot) return true;
17425
+ let mutationByTargetFunction = builtinMutationByProgram.get(programRoot);
17426
+ if (!mutationByTargetFunction) {
17427
+ mutationByTargetFunction = /* @__PURE__ */ new Map();
17428
+ builtinMutationByProgram.set(programRoot, mutationByTargetFunction);
17429
+ }
17430
+ const cachedResult = mutationByTargetFunction.get(targetFunction);
17431
+ if (cachedResult !== void 0) return cachedResult;
17432
+ let didFindUnsafeMutation = false;
17433
+ walkAst(programRoot, (candidate) => {
17434
+ if (didFindUnsafeMutation) return false;
17435
+ if (isNodeOfType(candidate, "CallExpression")) {
17436
+ didFindUnsafeMutation = isUnsafeBuiltinMutationApiCall(candidate, targetFunction, scopes);
17437
+ return;
17438
+ }
17439
+ let mutationTarget = null;
17440
+ if (isNodeOfType(candidate, "AssignmentExpression")) mutationTarget = candidate.left;
17441
+ if (isNodeOfType(candidate, "UpdateExpression")) mutationTarget = candidate.argument;
17442
+ if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator === "delete") mutationTarget = candidate.argument;
17443
+ if (!mutationTarget) return;
17444
+ didFindUnsafeMutation = isUnsafeBuiltinMemberTarget(mutationTarget, targetFunction, scopes) || isGlobalNamespaceReplacementTarget(mutationTarget, "Math", scopes);
17445
+ });
17446
+ mutationByTargetFunction.set(targetFunction, didFindUnsafeMutation);
17447
+ return didFindUnsafeMutation;
17448
+ };
17449
+ const hasUnsafeMathBinding = (node, scopes) => {
17450
+ let scope = scopes.scopeFor(node);
17451
+ while (scope) {
17452
+ const symbol = scope.symbolsByName.get("Math");
17453
+ if (symbol && !RUNTIMELESS_SYMBOL_KINDS.has(symbol.kind)) return !(symbol.kind === "const" && symbol.initializer && resolvesToGlobalNamespace(symbol.initializer, "Math", scopes));
17454
+ scope = scope.parent;
17455
+ }
17456
+ return false;
17457
+ };
16997
17458
  const jsMinMaxLoop = defineRule({
16998
17459
  id: "js-min-max-loop",
16999
17460
  title: "sort() to find min or max",
@@ -17004,18 +17465,18 @@ const jsMinMaxLoop = defineRule({
17004
17465
  if (!node.computed) return;
17005
17466
  const object = node.object;
17006
17467
  if (!isNodeOfType(object, "CallExpression") || !isMemberProperty(object.callee, "sort")) return;
17468
+ const sortReceiver = stripParenExpression(object.callee.object);
17469
+ if (!isNodeOfType(sortReceiver, "ArrayExpression") || !isSafeFreshNumericArray(sortReceiver)) return;
17007
17470
  const comparator = object.arguments?.[0];
17008
17471
  const direction = numericComparatorDirection(comparator);
17009
17472
  if (!direction) return;
17010
- const isFirstElement = isNodeOfType(node.property, "Literal") && node.property.value === 0;
17011
- const isLastElement = isNodeOfType(node.property, "BinaryExpression") && node.property.operator === "-" && isNodeOfType(node.property.right, "Literal") && node.property.right.value === 1;
17012
- if (isFirstElement || isLastElement) {
17013
- const targetFunction = (direction === "ascending" ? isFirstElement : isLastElement) ? "min" : "max";
17014
- context.report({
17015
- node,
17016
- message: `This is slow because array.sort()[${isFirstElement ? "0" : "length-1"}] sorts the whole list just to grab the smallest or largest, so use Math.${targetFunction}(...array) instead`
17017
- });
17018
- }
17473
+ if (!(isNodeOfType(node.property, "Literal") && node.property.value === 0)) return;
17474
+ const targetFunction = direction === "ascending" ? "min" : "max";
17475
+ if (hasUnsafeMathBinding(node, context.scopes) || hasUnsafeBuiltinMutation(node, targetFunction, context.scopes)) return;
17476
+ context.report({
17477
+ node,
17478
+ message: `This is slow because array.sort()[0] sorts the whole list just to grab the smallest or largest, so use Math.${targetFunction}(...array) instead`
17479
+ });
17019
17480
  } })
17020
17481
  });
17021
17482
  //#endregion
@@ -27489,6 +27950,152 @@ const noBarrelImport = defineRule({
27489
27950
  }
27490
27951
  });
27491
27952
  //#endregion
27953
+ //#region src/plugin/utils/has-static-property-write-before.ts
27954
+ const equivalentSymbolsByAnalysis = /* @__PURE__ */ new WeakMap();
27955
+ const CONDITIONAL_EXECUTION_NODE_TYPES = new Set([
27956
+ "CatchClause",
27957
+ "ConditionalExpression",
27958
+ "DoWhileStatement",
27959
+ "ForInStatement",
27960
+ "ForOfStatement",
27961
+ "ForStatement",
27962
+ "IfStatement",
27963
+ "LogicalExpression",
27964
+ "SwitchCase",
27965
+ "SwitchStatement",
27966
+ "TryStatement",
27967
+ "WhileStatement"
27968
+ ]);
27969
+ const getResolvedStaticPropertyName = (memberExpression, scopes) => {
27970
+ if (!isNodeOfType(memberExpression, "MemberExpression")) return null;
27971
+ const directPropertyName = getStaticPropertyName(memberExpression);
27972
+ if (directPropertyName || !memberExpression.computed) return directPropertyName;
27973
+ const property = stripParenExpression(memberExpression.property);
27974
+ if (!isNodeOfType(property, "Identifier")) return null;
27975
+ const propertySymbol = resolveConstIdentifierAlias(property, scopes);
27976
+ const initializer = propertySymbol?.initializer ? stripParenExpression(propertySymbol.initializer) : null;
27977
+ return initializer && isNodeOfType(initializer, "Literal") && typeof initializer.value === "string" ? initializer.value : null;
27978
+ };
27979
+ const collectScopeSymbols = (scope, symbols) => {
27980
+ symbols.push(...scope.symbols);
27981
+ for (const childScope of scope.children) collectScopeSymbols(childScope, symbols);
27982
+ };
27983
+ const getEquivalentSymbols = (identifier, scopes) => {
27984
+ const rootSymbol = resolveConstIdentifierAlias(identifier, scopes);
27985
+ if (!rootSymbol) return [];
27986
+ let symbolsByRootId = equivalentSymbolsByAnalysis.get(scopes);
27987
+ if (!symbolsByRootId) {
27988
+ symbolsByRootId = /* @__PURE__ */ new Map();
27989
+ equivalentSymbolsByAnalysis.set(scopes, symbolsByRootId);
27990
+ }
27991
+ const cachedSymbols = symbolsByRootId.get(rootSymbol.id);
27992
+ if (cachedSymbols) return cachedSymbols;
27993
+ const allSymbols = [];
27994
+ collectScopeSymbols(scopes.rootScope, allSymbols);
27995
+ const equivalentSymbols = allSymbols.filter((symbol) => resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes)?.id === rootSymbol.id);
27996
+ symbolsByRootId.set(rootSymbol.id, equivalentSymbols);
27997
+ return equivalentSymbols;
27998
+ };
27999
+ const findExecutionBoundary = (node) => {
28000
+ let current = node;
28001
+ while (current) {
28002
+ if (isFunctionLike$2(current) || isNodeOfType(current, "Program")) return current;
28003
+ current = current.parent ?? null;
28004
+ }
28005
+ return null;
28006
+ };
28007
+ const isOnUnconditionalPath = (node, boundary) => {
28008
+ let current = node.parent ?? null;
28009
+ while (current && current !== boundary) {
28010
+ if (CONDITIONAL_EXECUTION_NODE_TYPES.has(current.type)) return false;
28011
+ current = current.parent ?? null;
28012
+ }
28013
+ return current === boundary;
28014
+ };
28015
+ const findFunctionBindingIdentifier = (functionNode) => {
28016
+ if (isNodeOfType(functionNode, "FunctionDeclaration")) return functionNode.id ?? null;
28017
+ const parent = functionNode.parent;
28018
+ if (parent && isNodeOfType(parent, "VariableDeclarator") && parent.init === functionNode && isNodeOfType(parent.id, "Identifier")) return parent.id;
28019
+ return null;
28020
+ };
28021
+ const findDirectCall = (identifier) => {
28022
+ let callee = identifier;
28023
+ let parent = callee.parent;
28024
+ while (parent && stripParenExpression(parent) === identifier) {
28025
+ callee = parent;
28026
+ parent = callee.parent;
28027
+ }
28028
+ return parent && isNodeOfType(parent, "CallExpression") && parent.callee === callee ? parent : null;
28029
+ };
28030
+ const isFunctionSynchronouslyInvokedBefore = (functionNode, referenceNode, scopes, visitedFunctionNodes = /* @__PURE__ */ new Set()) => {
28031
+ if (visitedFunctionNodes.has(functionNode) || !isFunctionLike$2(functionNode) || functionNode.generator) return false;
28032
+ visitedFunctionNodes.add(functionNode);
28033
+ const referenceBoundary = findExecutionBoundary(referenceNode);
28034
+ if (!referenceBoundary) return false;
28035
+ const invocationCalls = [];
28036
+ const bindingIdentifier = findFunctionBindingIdentifier(functionNode);
28037
+ if (bindingIdentifier) for (const symbol of getEquivalentSymbols(bindingIdentifier, scopes)) for (const reference of symbol.references) {
28038
+ const call = findDirectCall(reference.identifier);
28039
+ if (call) invocationCalls.push(call);
28040
+ }
28041
+ else {
28042
+ const call = findDirectCall(functionNode);
28043
+ if (call) invocationCalls.push(call);
28044
+ }
28045
+ return invocationCalls.some((call) => {
28046
+ if (call.range[0] >= referenceNode.range[0]) return false;
28047
+ const callBoundary = findExecutionBoundary(call);
28048
+ if (!callBoundary) return false;
28049
+ if (callBoundary === referenceBoundary) return true;
28050
+ if (!isFunctionLike$2(callBoundary)) return false;
28051
+ return isFunctionSynchronouslyInvokedBefore(callBoundary, referenceNode, scopes, new Set(visitedFunctionNodes));
28052
+ });
28053
+ };
28054
+ const isMemberWriteTarget = (memberExpression) => {
28055
+ const parent = memberExpression.parent;
28056
+ if (!parent) return false;
28057
+ if (isNodeOfType(parent, "AssignmentExpression")) return parent.left === memberExpression;
28058
+ if (isNodeOfType(parent, "UpdateExpression")) return parent.argument === memberExpression;
28059
+ return isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === memberExpression;
28060
+ };
28061
+ const symbolHasStaticPropertyWriteBefore = (symbol, propertyName, referenceNode, scopes) => symbol.references.some((reference) => {
28062
+ let parent = reference.identifier.parent;
28063
+ while (parent && stripParenExpression(parent) === reference.identifier) parent = parent.parent;
28064
+ if (!parent || !isNodeOfType(parent, "MemberExpression") || stripParenExpression(parent.object) !== reference.identifier || getResolvedStaticPropertyName(parent, scopes) !== propertyName || !isMemberWriteTarget(parent)) return false;
28065
+ const writeBoundary = findExecutionBoundary(parent);
28066
+ const referenceBoundary = findExecutionBoundary(referenceNode);
28067
+ if (!writeBoundary || !referenceBoundary || !isOnUnconditionalPath(parent, writeBoundary)) return false;
28068
+ if (writeBoundary === referenceBoundary) return parent.range[0] < referenceNode.range[0];
28069
+ return isFunctionLike$2(writeBoundary) && isFunctionSynchronouslyInvokedBefore(writeBoundary, referenceNode, scopes);
28070
+ });
28071
+ const hasStaticPropertyWriteBefore = (identifier, propertyName, referenceNode, scopes) => {
28072
+ if (!isNodeOfType(identifier, "Identifier")) return false;
28073
+ return getEquivalentSymbols(identifier, scopes).some((symbol) => symbolHasStaticPropertyWriteBefore(symbol, propertyName, referenceNode, scopes));
28074
+ };
28075
+ //#endregion
28076
+ //#region src/plugin/utils/has-symbol-write-before.ts
28077
+ const hasSymbolWriteBefore = (symbol, referenceNode, scopes) => symbol.references.some((reference) => {
28078
+ if (reference.flag === "read") return false;
28079
+ const writeFunction = findEnclosingFunction(reference.identifier);
28080
+ if (writeFunction === findEnclosingFunction(referenceNode)) return reference.identifier.range[0] < referenceNode.range[0];
28081
+ if (!writeFunction) return true;
28082
+ return isFunctionSynchronouslyInvokedBefore(writeFunction, referenceNode, scopes);
28083
+ });
28084
+ //#endregion
28085
+ //#region src/plugin/utils/has-stable-call-target.ts
28086
+ const hasStableCallTarget = (callExpression, scopes) => {
28087
+ if (!isNodeOfType(callExpression, "CallExpression")) return false;
28088
+ const callee = stripParenExpression(callExpression.callee);
28089
+ if (isNodeOfType(callee, "Identifier")) {
28090
+ const symbol = scopes.symbolFor(callee);
28091
+ return Boolean(symbol && !hasSymbolWriteBefore(symbol, callee, scopes));
28092
+ }
28093
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
28094
+ const propertyName = getStaticPropertyName(callee);
28095
+ const receiver = stripParenExpression(callee.object);
28096
+ return Boolean(propertyName && isNodeOfType(receiver, "Identifier") && !hasStaticPropertyWriteBefore(receiver, propertyName, callee, scopes));
28097
+ };
28098
+ //#endregion
27492
28099
  //#region src/plugin/utils/function-contains-react-render-output.ts
27493
28100
  const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
27494
28101
  "FunctionDeclaration",
@@ -27501,19 +28108,68 @@ const REACT_CREATE_ELEMENT_OPTIONS = {
27501
28108
  allowGlobalReactNamespace: false,
27502
28109
  allowUnboundBareCalls: false
27503
28110
  };
27504
- const isCallArgumentFunctionExpression = (node) => {
28111
+ const LODASH_SORT_BY_MODULE_SOURCES = new Set([
28112
+ "lodash/sortBy",
28113
+ "lodash/sortBy.js",
28114
+ "lodash-es/sortBy",
28115
+ "lodash-es/sortBy.js"
28116
+ ]);
28117
+ const isArrayTypeAnnotation = (node) => {
28118
+ if (isNodeOfType(node, "TSArrayType") || isNodeOfType(node, "TSTupleType")) return true;
28119
+ if (!isNodeOfType(node, "TSTypeReference")) return false;
28120
+ return isNodeOfType(node.typeName, "Identifier") && (node.typeName.name === "Array" || node.typeName.name === "ReadonlyArray");
28121
+ };
28122
+ const hasArrayTypeAnnotation = (identifier) => {
28123
+ if (!isNodeOfType(identifier, "Identifier")) return false;
28124
+ const typeAnnotation = identifier.typeAnnotation;
28125
+ return Boolean(typeAnnotation && isNodeOfType(typeAnnotation, "TSTypeAnnotation") && isArrayTypeAnnotation(typeAnnotation.typeAnnotation));
28126
+ };
28127
+ const isProvenArrayProducerCall = (node, scopes) => {
28128
+ if (!isNodeOfType(node, "CallExpression")) return false;
28129
+ const callee = stripParenExpression(node.callee);
28130
+ if (!isNodeOfType(callee, "Identifier")) return false;
28131
+ const symbol = scopes.symbolFor(callee);
28132
+ if (!symbol || hasSymbolWriteBefore(symbol, callee, scopes)) return false;
28133
+ if (!isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") && !isNodeOfType(symbol.declarationNode, "ImportSpecifier")) return false;
28134
+ const importBinding = getImportBindingForName(callee, callee.name);
28135
+ if (!importBinding || importBinding.isNamespace) return false;
28136
+ if (LODASH_SORT_BY_MODULE_SOURCES.has(importBinding.source) && importBinding.exportedName === "default") return true;
28137
+ return (importBinding.source === "lodash" || importBinding.source === "lodash-es") && importBinding.exportedName === "sortBy";
28138
+ };
28139
+ const isProvenArrayExpression = (expression, scopes, referenceNode, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
28140
+ const candidate = stripParenExpression(expression);
28141
+ if (isNodeOfType(candidate, "ArrayExpression")) return true;
28142
+ if (isProvenArrayProducerCall(candidate, scopes)) return true;
28143
+ if (!isNodeOfType(candidate, "Identifier")) return false;
28144
+ const symbol = scopes.symbolFor(candidate);
28145
+ if (!symbol || visitedSymbolIds.has(symbol.id) || hasSymbolWriteBefore(symbol, referenceNode, scopes)) return false;
28146
+ if (hasArrayTypeAnnotation(symbol.bindingIdentifier)) return true;
28147
+ if (!symbol.initializer) return false;
28148
+ visitedSymbolIds.add(symbol.id);
28149
+ return isProvenArrayExpression(symbol.initializer, scopes, referenceNode, visitedSymbolIds);
28150
+ };
28151
+ const isProvenArrayMapCall = (node, scopes) => {
28152
+ if (!isNodeOfType(node, "CallExpression")) return false;
28153
+ const callee = stripParenExpression(node.callee);
28154
+ if (!isNodeOfType(callee, "MemberExpression") || getStaticPropertyName(callee) !== "map") return false;
28155
+ const receiver = stripParenExpression(callee.object);
28156
+ if (!isProvenArrayExpression(receiver, scopes, node)) return false;
28157
+ return !(isNodeOfType(receiver, "Identifier") && hasStaticPropertyWriteBefore(receiver, "map", node, scopes));
28158
+ };
28159
+ const isRenderPreservingCallArgumentFunction = (node, scopes) => {
27505
28160
  if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") return false;
27506
28161
  const parent = node.parent;
27507
28162
  if (!isNodeOfType(parent, "CallExpression")) return false;
27508
- return parent.arguments.some((argumentNode) => argumentNode === node);
28163
+ if (isReactApiCall(parent, "useMemo", scopes, { resolveNamedAliases: true }) && hasStableCallTarget(parent, scopes)) return parent.arguments[0] === node;
28164
+ return parent.arguments.some((argumentNode) => argumentNode === node) && isProvenArrayMapCall(parent, scopes);
27509
28165
  };
27510
- const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
28166
+ const isNestedRenderEvidenceBoundary = (node, scopes) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isRenderPreservingCallArgumentFunction(node, scopes);
27511
28167
  const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
27512
28168
  const containsRenderOutput$1 = (rootNode, scopes) => {
27513
28169
  let hasRenderOutput = false;
27514
28170
  walkAst(rootNode, (node) => {
27515
28171
  if (hasRenderOutput) return false;
27516
- if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
28172
+ if (node !== rootNode && isNestedRenderEvidenceBoundary(node, scopes)) return false;
27517
28173
  if (isRenderOutputExpression(node, scopes)) {
27518
28174
  hasRenderOutput = true;
27519
28175
  return false;
@@ -27522,12 +28178,13 @@ const containsRenderOutput$1 = (rootNode, scopes) => {
27522
28178
  return hasRenderOutput;
27523
28179
  };
27524
28180
  const renderOutputCache = /* @__PURE__ */ new WeakMap();
27525
- const functionContainsReactRenderOutput = (functionNode, scopes) => {
28181
+ const functionContainsReactRenderOutput = (functionNode, scopes, controlFlow) => {
27526
28182
  const cachedEntry = renderOutputCache.get(functionNode);
27527
- if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
27528
- const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
28183
+ if (cachedEntry && cachedEntry.scopes === scopes && cachedEntry.controlFlow === controlFlow) return cachedEntry.hasRenderOutput;
28184
+ const hasRenderOutput = functionReturnsMatchingExpression(functionNode, scopes, (expression) => containsRenderOutput$1(expression, scopes), controlFlow);
27529
28185
  renderOutputCache.set(functionNode, {
27530
28186
  scopes,
28187
+ controlFlow,
27531
28188
  hasRenderOutput
27532
28189
  });
27533
28190
  return hasRenderOutput;
@@ -27543,8 +28200,8 @@ const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration
27543
28200
  const message = (name) => `\`${name}\` is a component, so calling it as a plain function (\`${name}(...)\`) runs it outside React: its hooks break, it gets no fiber/state, and memoization is lost. Render it as \`<${name} />\` instead.`;
27544
28201
  const symbolIsLocalComponent = (symbol, context) => {
27545
28202
  const declaration = symbol.declarationNode;
27546
- if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
27547
- if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
28203
+ if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes, context.cfg);
28204
+ if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes, context.cfg);
27548
28205
  return false;
27549
28206
  };
27550
28207
  const isModuleScopeDeclaration = (declaration) => {
@@ -28162,7 +28819,7 @@ const noCreateRefInFunctionComponent = defineRule({
28162
28819
  if (!enclosingFunction) return;
28163
28820
  const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
28164
28821
  if (!displayName) return;
28165
- if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
28822
+ if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes, context.cfg))) return;
28166
28823
  context.report({
28167
28824
  node,
28168
28825
  message: MESSAGE$30
@@ -32651,7 +33308,7 @@ const noGiantComponent = defineRule({
32651
33308
  FunctionDeclaration(node) {
32652
33309
  if (!node.id?.name || !isUppercaseName(node.id.name)) return;
32653
33310
  if (getOversizedComponentLineCount(node) === null) return;
32654
- if (!functionContainsReactRenderOutput(node, context.scopes)) return;
33311
+ if (!functionContainsReactRenderOutput(node, context.scopes, context.cfg)) return;
32655
33312
  reportOversizedComponent(node.id, node.id.name);
32656
33313
  },
32657
33314
  VariableDeclarator(node) {
@@ -32659,7 +33316,7 @@ const noGiantComponent = defineRule({
32659
33316
  const functionNode = unwrapReactHocFunction(node.init);
32660
33317
  if (!functionNode) return;
32661
33318
  if (getOversizedComponentLineCount(functionNode) === null) return;
32662
- if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
33319
+ if (!functionContainsReactRenderOutput(functionNode, context.scopes, context.cfg)) return;
32663
33320
  reportOversizedComponent(node.id, node.id.name);
32664
33321
  }
32665
33322
  };
@@ -34939,7 +35596,7 @@ const noManyBooleanProps = defineRule({
34939
35596
  if (!param) return;
34940
35597
  const propsBinding = resolveFirstArgumentBinding(param);
34941
35598
  if (!propsBinding) return;
34942
- if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
35599
+ if (!functionContainsReactRenderOutput(functionNode, context.scopes, context.cfg)) return;
34943
35600
  if (isNodeOfType(propsBinding, "ObjectPattern")) {
34944
35601
  const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
34945
35602
  const booleanLikePropNames = [];
@@ -37901,6 +38558,174 @@ const noPropCallbackInRender = defineRule({
37901
38558
  } })
37902
38559
  });
37903
38560
  //#endregion
38561
+ //#region src/plugin/utils/is-proven-react-class-component.ts
38562
+ const REACT_COMPONENT_CLASS_NAMES = new Set(["Component", "PureComponent"]);
38563
+ const isReactComponentClassValue = (node, scopes, visitedClassNodes, visitedSymbolIds) => {
38564
+ const expression = stripParenExpression(node);
38565
+ if (isNodeOfType(expression, "MemberExpression")) {
38566
+ const propertyName = getStaticPropertyName(expression);
38567
+ const receiver = stripParenExpression(expression.object);
38568
+ return Boolean(propertyName && REACT_COMPONENT_CLASS_NAMES.has(propertyName) && isNodeOfType(receiver, "Identifier") && !hasStaticPropertyWriteBefore(receiver, propertyName, expression, scopes) && isReactNamespaceImport(receiver, scopes));
38569
+ }
38570
+ if (isNodeOfType(expression, "ClassExpression")) return isProvenReactClassComponent(expression, scopes, visitedClassNodes, visitedSymbolIds);
38571
+ if (!isNodeOfType(expression, "Identifier")) return false;
38572
+ const symbol = scopes.symbolFor(expression);
38573
+ if (!symbol || visitedSymbolIds.has(symbol.id) || hasSymbolWriteBefore(symbol, expression, scopes)) return false;
38574
+ visitedSymbolIds.add(symbol.id);
38575
+ if (isImportedFromReact(symbol)) {
38576
+ const importedName = getImportedName(symbol.declarationNode);
38577
+ return Boolean(importedName && REACT_COMPONENT_CLASS_NAMES.has(importedName));
38578
+ }
38579
+ if (isNodeOfType(symbol.declarationNode, "ClassDeclaration") || isNodeOfType(symbol.declarationNode, "ClassExpression")) return isProvenReactClassComponent(symbol.declarationNode, scopes, visitedClassNodes, visitedSymbolIds);
38580
+ return Boolean(symbol.kind === "const" && symbol.initializer && isReactComponentClassValue(symbol.initializer, scopes, visitedClassNodes, visitedSymbolIds));
38581
+ };
38582
+ const isProvenReactClassComponent = (classNode, scopes, visitedClassNodes = /* @__PURE__ */ new Set(), visitedSymbolIds = /* @__PURE__ */ new Set()) => {
38583
+ if (!isNodeOfType(classNode, "ClassDeclaration") && !isNodeOfType(classNode, "ClassExpression") || visitedClassNodes.has(classNode) || !classNode.superClass) return false;
38584
+ visitedClassNodes.add(classNode);
38585
+ return isReactComponentClassValue(classNode.superClass, scopes, visitedClassNodes, visitedSymbolIds);
38586
+ };
38587
+ //#endregion
38588
+ //#region src/plugin/utils/function-contains-proven-react-hook-call.ts
38589
+ const functionContainsProvenReactHookCall = (functionNode, scopes) => {
38590
+ if (!isFunctionLike$2(functionNode)) return false;
38591
+ let containsReactHookCall = false;
38592
+ walkAst(functionNode.body, (node) => {
38593
+ if (containsReactHookCall) return false;
38594
+ if (node !== functionNode.body && (isFunctionLike$2(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
38595
+ if (isNodeOfType(node, "CallExpression") && isReactApiCall(node, BUILTIN_HOOK_NAMES, scopes, { resolveNamedAliases: true })) {
38596
+ containsReactHookCall = true;
38597
+ return false;
38598
+ }
38599
+ });
38600
+ return containsReactHookCall;
38601
+ };
38602
+ //#endregion
38603
+ //#region src/plugin/utils/function-returns-props-children.ts
38604
+ const functionReturnsPropsChildren = (functionNode, scopes, controlFlow) => {
38605
+ if (!isFunctionLike$2(functionNode) || functionNode.params.length === 0) return false;
38606
+ const firstParameter = stripParenExpression(functionNode.params[0]);
38607
+ const firstParameterPattern = isNodeOfType(firstParameter, "AssignmentPattern") ? stripParenExpression(firstParameter.left) : firstParameter;
38608
+ const propsParameterSymbol = isNodeOfType(firstParameterPattern, "Identifier") ? scopes.symbolFor(firstParameterPattern) : null;
38609
+ const childrenBindingSymbolIds = /* @__PURE__ */ new Set();
38610
+ if (isNodeOfType(firstParameterPattern, "ObjectPattern")) {
38611
+ for (const property of firstParameterPattern.properties) if (isNodeOfType(property, "Property") && getStaticPropertyKeyName(property, { allowComputedString: true }) === "children") {
38612
+ const propertyValue = stripParenExpression(property.value);
38613
+ const childrenBinding = isNodeOfType(propertyValue, "AssignmentPattern") ? stripParenExpression(propertyValue.left) : propertyValue;
38614
+ if (!isNodeOfType(childrenBinding, "Identifier")) continue;
38615
+ const childrenBindingSymbol = scopes.symbolFor(childrenBinding);
38616
+ if (childrenBindingSymbol) childrenBindingSymbolIds.add(childrenBindingSymbol.id);
38617
+ }
38618
+ }
38619
+ return functionReturnsMatchingExpression(functionNode, scopes, (expression) => {
38620
+ const candidate = stripParenExpression(expression);
38621
+ if (isNodeOfType(candidate, "Identifier")) {
38622
+ const symbol = scopes.symbolFor(candidate);
38623
+ return Boolean(symbol && childrenBindingSymbolIds.has(symbol.id) && !hasSymbolWriteBefore(symbol, candidate, scopes));
38624
+ }
38625
+ if (!isNodeOfType(candidate, "MemberExpression")) return false;
38626
+ if (getStaticPropertyName(candidate) !== "children") return false;
38627
+ const receiver = stripParenExpression(candidate.object);
38628
+ if (!isNodeOfType(receiver, "Identifier")) return false;
38629
+ const receiverSymbol = scopes.symbolFor(receiver);
38630
+ if (!receiverSymbol || !propsParameterSymbol) return false;
38631
+ return receiverSymbol.id === propsParameterSymbol.id && !hasSymbolWriteBefore(receiverSymbol, candidate, scopes) && !hasStaticPropertyWriteBefore(receiver, "children", candidate, scopes);
38632
+ }, controlFlow);
38633
+ };
38634
+ //#endregion
38635
+ //#region src/plugin/utils/function-returns-only-null.ts
38636
+ const isNullExpression = (expression) => {
38637
+ const candidate = stripParenExpression(expression);
38638
+ return isNodeOfType(candidate, "Literal") && candidate.value === null;
38639
+ };
38640
+ const functionReturnsOnlyNull = (functionNode) => {
38641
+ if (!isFunctionLike$2(functionNode)) return false;
38642
+ if (!isNodeOfType(functionNode.body, "BlockStatement")) return isNullExpression(functionNode.body);
38643
+ const returnStatements = collectFunctionReturnStatements(functionNode);
38644
+ return returnStatements.length > 0 && returnStatements.every((returnStatement) => Boolean(returnStatement.argument && isNullExpression(returnStatement.argument)));
38645
+ };
38646
+ //#endregion
38647
+ //#region src/plugin/utils/is-proven-styled-component-expression.ts
38648
+ const findFactoryRoot = (node) => {
38649
+ const candidate = stripParenExpression(node);
38650
+ if (isNodeOfType(candidate, "Identifier")) return candidate;
38651
+ if (isNodeOfType(candidate, "MemberExpression")) return findFactoryRoot(candidate.object);
38652
+ if (isNodeOfType(candidate, "CallExpression")) return findFactoryRoot(candidate.callee);
38653
+ return null;
38654
+ };
38655
+ const findFactoryPropertyName = (node) => {
38656
+ const candidate = stripParenExpression(node);
38657
+ if (isNodeOfType(candidate, "MemberExpression")) return findFactoryPropertyName(candidate.object) ?? getStaticPropertyName(candidate);
38658
+ if (isNodeOfType(candidate, "CallExpression")) return findFactoryPropertyName(candidate.callee);
38659
+ return null;
38660
+ };
38661
+ const isProvenStyledComponentExpression = (expression, scopes) => {
38662
+ const candidate = stripParenExpression(expression);
38663
+ if (!isNodeOfType(candidate, "TaggedTemplateExpression")) return false;
38664
+ const factoryRoot = findFactoryRoot(candidate.tag);
38665
+ if (!factoryRoot) return false;
38666
+ const factoryPropertyName = findFactoryPropertyName(candidate.tag);
38667
+ if (factoryPropertyName && hasStaticPropertyWriteBefore(factoryRoot, factoryPropertyName, candidate, scopes)) return false;
38668
+ const symbol = resolveConstIdentifierAlias(factoryRoot, scopes);
38669
+ if (!symbol || symbol.kind !== "import") return false;
38670
+ if (!(isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || getImportedName(symbol.declarationNode) === "styled")) return false;
38671
+ const importDeclaration = symbol.declarationNode.parent;
38672
+ return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "styled-components");
38673
+ };
38674
+ //#endregion
38675
+ //#region src/plugin/utils/is-proven-react-component-symbol.ts
38676
+ const REACT_COMPONENT_HOC_NAMES = new Set(["memo", "forwardRef"]);
38677
+ const functionHasComponentEvidence = (functionNode, scopes, controlFlow) => functionContainsReactRenderOutput(functionNode, scopes, controlFlow) || functionReturnsPropsChildren(functionNode, scopes, controlFlow) || functionContainsProvenReactHookCall(functionNode, scopes) && functionReturnsOnlyNull(functionNode);
38678
+ const isProvenReactComponentExpression = (expression, scopes, controlFlow, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
38679
+ const candidate = stripParenExpression(expression);
38680
+ if (isInlineFunctionExpression(candidate)) return functionHasComponentEvidence(candidate, scopes, controlFlow);
38681
+ if (isNodeOfType(candidate, "ClassExpression")) return isProvenReactClassComponent(candidate, scopes);
38682
+ if (isProvenStyledComponentExpression(candidate, scopes)) return true;
38683
+ if (isNodeOfType(candidate, "Identifier")) {
38684
+ const symbol = scopes.symbolFor(candidate);
38685
+ if (!symbol || visitedSymbolIds.has(symbol.id) || hasSymbolWriteBefore(symbol, candidate, scopes)) return false;
38686
+ visitedSymbolIds.add(symbol.id);
38687
+ if (isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return functionHasComponentEvidence(symbol.declarationNode, scopes, controlFlow);
38688
+ if (isNodeOfType(symbol.declarationNode, "ClassDeclaration") || isNodeOfType(symbol.declarationNode, "ClassExpression")) return isProvenReactClassComponent(symbol.declarationNode, scopes);
38689
+ return Boolean(symbol.initializer && isProvenReactComponentExpression(symbol.initializer, scopes, controlFlow, visitedSymbolIds));
38690
+ }
38691
+ if (!isNodeOfType(candidate, "CallExpression")) return false;
38692
+ if (!hasStableCallTarget(candidate, scopes)) return false;
38693
+ if (isReactApiCall(candidate, REACT_COMPONENT_HOC_NAMES, scopes, { resolveNamedAliases: true })) {
38694
+ const wrappedComponent = candidate.arguments[0];
38695
+ return Boolean(wrappedComponent && !isNodeOfType(wrappedComponent, "SpreadElement") && isProvenReactComponentExpression(wrappedComponent, scopes, controlFlow, visitedSymbolIds));
38696
+ }
38697
+ if (!isReactApiCall(candidate, "useMemo", scopes, { resolveNamedAliases: true })) return false;
38698
+ const factory = candidate.arguments[0];
38699
+ if (!factory || isNodeOfType(factory, "SpreadElement")) return false;
38700
+ const unwrappedFactory = stripParenExpression(factory);
38701
+ if (!isInlineFunctionExpression(unwrappedFactory)) return false;
38702
+ if (!isNodeOfType(unwrappedFactory.body, "BlockStatement")) return isProvenReactComponentExpression(unwrappedFactory.body, scopes, controlFlow, visitedSymbolIds);
38703
+ const returnStatements = collectFunctionReturnStatements(unwrappedFactory);
38704
+ const returnedExpression = returnStatements[0]?.argument;
38705
+ return Boolean(returnStatements.length === 1 && returnedExpression && isProvenReactComponentExpression(returnedExpression, scopes, controlFlow, visitedSymbolIds));
38706
+ };
38707
+ const isProvenReactComponentSymbol = (symbol, scopes, controlFlow, componentReference) => {
38708
+ const candidateSymbols = symbol.kind === "ts-module" ? symbol.scope.symbols.filter((candidateSymbol) => candidateSymbol.name === symbol.name && candidateSymbol.kind !== "ts-module") : [symbol];
38709
+ for (const candidateSymbol of candidateSymbols) {
38710
+ if (hasSymbolWriteBefore(candidateSymbol, componentReference, scopes)) continue;
38711
+ if (isComponentDeclaration(candidateSymbol.declarationNode)) {
38712
+ if (functionHasComponentEvidence(candidateSymbol.declarationNode, scopes, controlFlow)) return true;
38713
+ continue;
38714
+ }
38715
+ const initializer = candidateSymbol.initializer ? stripParenExpression(candidateSymbol.initializer) : null;
38716
+ if (isNodeOfType(candidateSymbol.declarationNode, "VariableDeclarator") && isNodeOfType(candidateSymbol.declarationNode.id, "Identifier") && isUppercaseName(candidateSymbol.declarationNode.id.name) && initializer) {
38717
+ if (isProvenReactComponentExpression(initializer, scopes, controlFlow)) return true;
38718
+ continue;
38719
+ }
38720
+ if (isNodeOfType(candidateSymbol.declarationNode, "ClassDeclaration") || isNodeOfType(candidateSymbol.declarationNode, "ClassExpression")) {
38721
+ if (isProvenReactClassComponent(candidateSymbol.declarationNode, scopes)) return true;
38722
+ continue;
38723
+ }
38724
+ if (initializer && isNodeOfType(initializer, "ClassExpression") && isProvenReactClassComponent(initializer, scopes)) return true;
38725
+ }
38726
+ return false;
38727
+ };
38728
+ //#endregion
37904
38729
  //#region src/plugin/rules/architecture/no-prop-types.ts
37905
38730
  const PROP_TYPES_PROPERTY = "propTypes";
37906
38731
  const isPropTypesKey = (key, computed) => {
@@ -37908,20 +38733,19 @@ const isPropTypesKey = (key, computed) => {
37908
38733
  if (computed) return isNodeOfType(key, "Literal") && key.value === PROP_TYPES_PROPERTY;
37909
38734
  return isNodeOfType(key, "Identifier") && key.name === PROP_TYPES_PROPERTY;
37910
38735
  };
37911
- const getComponentNameFromPropTypesAssignment = (left) => {
38736
+ const getComponentFromPropTypesAssignment = (left) => {
37912
38737
  if (!isNodeOfType(left, "MemberExpression")) return null;
37913
38738
  if (!isPropTypesKey(left.property, Boolean(left.computed))) return null;
37914
- if (!isNodeOfType(left.object, "Identifier")) return null;
37915
- if (!isUppercaseName(left.object.name)) return null;
37916
- return left.object.name;
38739
+ const receiver = stripParenExpression(left.object);
38740
+ if (!isNodeOfType(receiver, "Identifier")) return null;
38741
+ if (!isUppercaseName(receiver.name)) return null;
38742
+ return receiver;
37917
38743
  };
37918
- const getComponentNameFromClassProperty = (node) => {
38744
+ const getComponentNameFromClassProperty = (node, scopes) => {
37919
38745
  if (!node.static) return null;
37920
38746
  if (!isPropTypesKey(node.key, Boolean(node.computed))) return null;
37921
- const classBody = node.parent;
37922
- if (!isNodeOfType(classBody, "ClassBody")) return null;
37923
- const classNode = classBody.parent;
37924
- if (!classNode) return null;
38747
+ const classNode = isNodeOfType(node.parent, "ClassBody") ? node.parent.parent : null;
38748
+ if (!classNode || !isProvenReactClassComponent(classNode, scopes)) return null;
37925
38749
  if ((isNodeOfType(classNode, "ClassDeclaration") || isNodeOfType(classNode, "ClassExpression")) && classNode.id?.name && isUppercaseName(classNode.id.name)) return classNode.id.name;
37926
38750
  if (!isNodeOfType(classNode, "ClassExpression")) return null;
37927
38751
  const declarator = classNode.parent;
@@ -37942,15 +38766,17 @@ const noPropTypes = defineRule({
37942
38766
  create: (context) => ({
37943
38767
  AssignmentExpression(node) {
37944
38768
  if (node.operator !== "=") return;
37945
- const componentName = getComponentNameFromPropTypesAssignment(node.left);
37946
- if (!componentName) return;
38769
+ const component = getComponentFromPropTypesAssignment(node.left);
38770
+ if (!component) return;
38771
+ const symbol = context.scopes.symbolFor(component);
38772
+ if (!symbol || !isProvenReactComponentSymbol(symbol, context.scopes, context.cfg, component)) return;
37947
38773
  context.report({
37948
38774
  node: node.left,
37949
- message: buildMessage$11(componentName)
38775
+ message: buildMessage$11(component.name)
37950
38776
  });
37951
38777
  },
37952
38778
  PropertyDefinition(node) {
37953
- const componentName = getComponentNameFromClassProperty(node);
38779
+ const componentName = getComponentNameFromClassProperty(node, context.scopes);
37954
38780
  if (!componentName) return;
37955
38781
  context.report({
37956
38782
  node: node.key,
@@ -40428,7 +41254,7 @@ const noThisInSfc = defineRule({
40428
41254
  const enclosingFunction = findEnclosingFunctionComponent(node);
40429
41255
  if (!enclosingFunction) return;
40430
41256
  if (!looksLikeFunctionComponent(enclosingFunction)) return;
40431
- if (!functionContainsReactRenderOutput(enclosingFunction, context.scopes)) return;
41257
+ if (!functionContainsReactRenderOutput(enclosingFunction, context.scopes, context.cfg)) return;
40432
41258
  if (functionHasOwnThisMemberWrite(enclosingFunction)) return;
40433
41259
  context.report({
40434
41260
  node,
@@ -42093,21 +42919,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
42093
42919
  });
42094
42920
  return found;
42095
42921
  };
42096
- const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
42922
+ const functionContainsJsxOrCreateElement = (functionNode, scopes, controlFlow) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement, controlFlow);
42097
42923
  const isReactClassComponent = (classNode) => {
42098
42924
  if (isEs6Component(classNode)) return true;
42099
42925
  return expressionContainsJsxOrCreateElement(classNode);
42100
42926
  };
42101
- const findEnclosingComponent = (node, scopes) => {
42927
+ const findEnclosingComponent = (node, scopes, controlFlow) => {
42102
42928
  let walker = node.parent;
42103
42929
  while (walker) {
42104
42930
  if (isFunctionLike$2(walker)) {
42105
42931
  const componentName = inferFunctionLikeName(walker);
42106
- if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
42932
+ if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes, controlFlow)) return {
42107
42933
  component: walker,
42108
42934
  name: componentName
42109
42935
  };
42110
- if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
42936
+ if (!componentName && functionContainsJsxOrCreateElement(walker, scopes, controlFlow) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
42111
42937
  component: walker,
42112
42938
  name: null
42113
42939
  };
@@ -42346,7 +43172,7 @@ const noUnstableNestedComponents = defineRule({
42346
43172
  if (renderPropRegex.test(propInfo.propName)) return;
42347
43173
  if (settings.allowAsProps) return;
42348
43174
  }
42349
- const enclosing = findEnclosingComponent(candidateNode, context.scopes);
43175
+ const enclosing = findEnclosingComponent(candidateNode, context.scopes, context.cfg);
42350
43176
  if (!enclosing) return;
42351
43177
  const gatedName = propInfo ? null : requiredInstantiationName;
42352
43178
  queuedReports.push({
@@ -42357,7 +43183,7 @@ const noUnstableNestedComponents = defineRule({
42357
43183
  });
42358
43184
  };
42359
43185
  const checkFunctionLike = (node) => {
42360
- if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
43186
+ if (!functionContainsJsxOrCreateElement(node, context.scopes, context.cfg)) return;
42361
43187
  const inferredName = inferFunctionLikeName(node);
42362
43188
  const propInfo = isComponentDeclaredInProp(node);
42363
43189
  const isObjectCallback = isObjectCallbackCandidate(node);
@@ -42968,7 +43794,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
42968
43794
  continue;
42969
43795
  }
42970
43796
  if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
42971
- if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
43797
+ if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes, state.controlFlow)) return true;
42972
43798
  if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
42973
43799
  }
42974
43800
  return false;
@@ -43078,11 +43904,12 @@ const onlyExportComponents = defineRule({
43078
43904
  allowExportNames: new Set(settings.allowExportNames),
43079
43905
  allowConstantExport: settings.allowConstantExport,
43080
43906
  localComponentNames,
43081
- scopes: context.scopes
43907
+ scopes: context.scopes,
43908
+ controlFlow: context.cfg
43082
43909
  };
43083
43910
  for (const child of componentCandidates) {
43084
43911
  if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
43085
- if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
43912
+ if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes, context.cfg)) localComponentNames.add(child.id.name);
43086
43913
  }
43087
43914
  if (isNodeOfType(child, "ClassDeclaration") && child.id) {
43088
43915
  if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
@@ -43091,7 +43918,7 @@ const onlyExportComponents = defineRule({
43091
43918
  const initializer = child.init;
43092
43919
  if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
43093
43920
  const expression = initializer ? skipTsExpression(initializer) : null;
43094
- if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
43921
+ if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes, context.cfg)) localComponentNames.add(child.id.name);
43095
43922
  }
43096
43923
  }
43097
43924
  }
@@ -47313,10 +48140,10 @@ const rerenderLazyStateInit = defineRule({
47313
48140
  //#endregion
47314
48141
  //#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
47315
48142
  const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
47316
- const callbackReturnsJsx = (callback, scopes) => {
48143
+ const callbackReturnsJsx = (callback, scopes, controlFlow) => {
47317
48144
  if (!callback) return false;
47318
48145
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
47319
- return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
48146
+ return functionReturnsMatchingExpression(callback, scopes, isJsxExpression, controlFlow);
47320
48147
  };
47321
48148
  const returnArgumentUsesAnyName = (returnStatement, names) => {
47322
48149
  if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
@@ -47394,7 +48221,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
47394
48221
  if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
47395
48222
  for (const declarator of stmt.declarations ?? []) {
47396
48223
  const init = declarator.init;
47397
- if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes)) {
48224
+ if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes, context.cfg)) {
47398
48225
  memoNode = declarator;
47399
48226
  callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
47400
48227
  if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.6-dev.0d11bd5",
3
+ "version": "0.7.6-dev.1d7b6e2",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",