eslint-plugin-react-19-upgrade
Version:
An ESLint plugin to identify and fix breaking changes when upgrading React 18 to React 19
210 lines (180 loc) • 5.31 kB
JavaScript
function findVariable(scope, name) {
let currentScope = scope;
while (currentScope) {
const variable = currentScope.variables.find((item) => item.name === name);
if (variable) {
return variable;
}
currentScope = currentScope.upper;
}
return null;
}
function isUppercaseName(name) {
return typeof name === "string" && /^[A-Z]/.test(name);
}
function getEnclosingFunction(node) {
let current = node.parent;
while (current) {
if (
current.type === "FunctionDeclaration" ||
current.type === "FunctionExpression" ||
current.type === "ArrowFunctionExpression"
) {
return current;
}
current = current.parent;
}
return null;
}
function isLikelyReactModuleFactory(node) {
const enclosingFunction = getEnclosingFunction(node);
if (!enclosingFunction) {
return false;
}
if (enclosingFunction.id && isUppercaseName(enclosingFunction.id.name)) {
return true;
}
if (
enclosingFunction.parent &&
enclosingFunction.parent.type === "VariableDeclarator" &&
enclosingFunction.parent.id.type === "Identifier"
) {
return isUppercaseName(enclosingFunction.parent.id.name);
}
return false;
}
function isRequireReact(node) {
return (
node &&
node.type === "CallExpression" &&
node.callee.type === "Identifier" &&
node.callee.name === "require" &&
node.arguments.length === 1 &&
node.arguments[0].type === "Literal" &&
node.arguments[0].value === "react"
);
}
function isReactLikeObject(node) {
if (!node) return false;
if (node.type === "Identifier" && node.name === "React") return true;
return isRequireReact(node);
}
function isReactCreateFactoryMemberExpression(node) {
return (
node &&
node.type === "MemberExpression" &&
!node.computed &&
isReactLikeObject(node.object) &&
node.property.type === "Identifier" &&
node.property.name === "createFactory"
);
}
function resolvesToReactCreateFactory(context, node) {
if (node.callee.type !== "Identifier") {
return false;
}
const sourceCode = context.sourceCode ?? context.getSourceCode();
const scope = sourceCode.getScope
? sourceCode.getScope(node)
: context.getScope();
const variable = findVariable(scope, node.callee.name);
if (!variable || variable.defs.length === 0) {
return false;
}
const definitionNode = variable.defs[0].node;
if (!definitionNode || definitionNode.type !== "VariableDeclarator") {
return false;
}
if (
definitionNode.id.type === "Identifier" &&
isReactCreateFactoryMemberExpression(definitionNode.init)
) {
return true;
}
if (
definitionNode.id.type === "ObjectPattern" &&
isReactLikeObject(definitionNode.init)
) {
return definitionNode.id.properties.some((property) => {
if (property.type !== "Property" || property.computed) {
return false;
}
const keyName =
property.key.type === "Identifier" ? property.key.name : property.key.value;
const localName =
property.value.type === "Identifier"
? property.value.name
: property.value.left && property.value.left.type === "Identifier"
? property.value.left.name
: null;
return keyName === "createFactory" && localName === node.callee.name;
});
}
return false;
}
module.exports = {
meta: {
type: "problem",
docs: {
description: "Disallow module pattern factories and React.createFactory",
url: "https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-module-pattern-factories",
},
messages: {
noModulePattern:
"Module pattern factories are removed in React 19. Use regular functions instead.",
noCreateFactory:
"React.createFactory is removed in React 19. Use JSX instead.",
},
schema: [],
ruleId: "no-factories",
hasSuggestions: true,
},
create(context) {
return {
ReturnStatement(node) {
if (
node.argument &&
node.argument.type === "ObjectExpression" &&
isLikelyReactModuleFactory(node)
) {
const hasRenderMethod = node.argument.properties.some(
(property) =>
property.type === "Property" &&
property.key.type === "Identifier" &&
property.key.name === "render" &&
property.value.type === "FunctionExpression",
);
if (hasRenderMethod) {
context.report({
node,
messageId: "noModulePattern",
});
}
}
},
ImportDeclaration(node) {
if (node.source.value === "react" && node.specifiers) {
node.specifiers.forEach((spec) => {
if (spec.imported && spec.imported.name === "createFactory") {
context.report({
node: spec,
messageId: "noCreateFactory",
});
}
});
}
},
CallExpression(node) {
if (
isReactCreateFactoryMemberExpression(node.callee) ||
resolvesToReactCreateFactory(context, node)
) {
context.report({
node,
messageId: "noCreateFactory",
});
}
},
};
},
};