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.
- package/dist/index.js +918 -91
- 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
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
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"
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
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
|
-
|
|
16543
|
-
return isNodeOfType(
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
16558
|
-
|
|
16559
|
-
|
|
16560
|
-
|
|
16561
|
-
|
|
16562
|
-
|
|
16563
|
-
|
|
16564
|
-
|
|
16565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17011
|
-
const
|
|
17012
|
-
if (
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
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
|
|
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
|
-
|
|
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) && !
|
|
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 =
|
|
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
|
|
38736
|
+
const getComponentFromPropTypesAssignment = (left) => {
|
|
37912
38737
|
if (!isNodeOfType(left, "MemberExpression")) return null;
|
|
37913
38738
|
if (!isPropTypesKey(left.property, Boolean(left.computed))) return null;
|
|
37914
|
-
|
|
37915
|
-
if (!
|
|
37916
|
-
|
|
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
|
|
37922
|
-
if (!
|
|
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
|
|
37946
|
-
if (!
|
|
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(
|
|
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);
|