Skip to content

Commit

Permalink
Handle memo for function declarations / function assignments [publish] (
Browse files Browse the repository at this point in the history
  • Loading branch information
SukkaW committed Jul 21, 2024
1 parent a10b96b commit 897a11d
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.4.9

- Support `function Foo() {}; export default memo(Foo)` (fixes #44)

## 0.4.8

- Support `export const foo = -1` with `allowConstantExport` (fixes #43)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-react-refresh",
"version": "0.4.8",
"version": "0.4.9",
"type": "module",
"license": "MIT",
"scripts": {
Expand Down
8 changes: 8 additions & 0 deletions src/only-export-components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ const valid = [
name: "export default memo function",
code: "export default memo(function Foo () {});",
},
{
name: "export default memo function assignment",
code: "const Foo = () => {}; export default memo(Foo);",
},
{
name: "export default memo function declaration",
code: "function Foo() {}; export default memo(Foo);",
},
{
name: "export type *",
code: "export type * from './module';",
Expand Down
34 changes: 26 additions & 8 deletions src/only-export-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export const onlyExportComponents: TSESLint.RuleModule<
(checkJS && filename.endsWith(".js"));
if (!shouldScan) return {};

const allowExportNamesSet = allowExportNames
? new Set(allowExportNames)
: undefined;

return {
Program(program) {
let hasExports = false;
Expand All @@ -98,7 +102,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
nonComponentExports.push(identifierNode);
return;
}
if (allowExportNames?.includes(identifierNode.name)) return;
if (allowExportNamesSet?.has(identifierNode.name)) return;
if (
allowConstantExport &&
init &&
Expand All @@ -109,6 +113,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
) {
return;
}

if (isFunction) {
if (possibleReactExportRE.test(identifierNode.name)) {
mayHaveReactExport = true;
Expand All @@ -119,7 +124,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
if (
init &&
// Switch to allowList?
notReactComponentExpression.includes(init.type)
notReactComponentExpression.has(init.type)
) {
nonComponentExports.push(identifierNode);
return;
Expand Down Expand Up @@ -153,12 +158,23 @@ export const onlyExportComponents: TSESLint.RuleModule<
}
} else if (node.type === "CallExpression") {
if (
node.callee.type === "Identifier" &&
reactHOCs.includes(node.callee.name) &&
node.callee.type !== "Identifier" ||
!reactHOCs.has(node.callee.name)
) {
// we rule out non HoC first
context.report({ messageId: "anonymousExport", node });
} else if (
node.arguments[0]?.type === "FunctionExpression" &&
node.arguments[0].id
) {
// export default memo(function Foo() {})
handleExportIdentifier(node.arguments[0].id, true);
} else if (node.arguments[0]?.type === "Identifier") {
// const Foo = () => {}; export default memo(Foo);
// No need to check further, the identifier has necessarily a named,
// and it would throw at runtime if it's not a React component.
// We have React exports since we are exporting return value of HoC
mayHaveReactExport = true;
} else {
context.report({ messageId: "anonymousExport", node });
}
Expand Down Expand Up @@ -234,18 +250,20 @@ export const onlyExportComponents: TSESLint.RuleModule<
},
};

const reactHOCs = ["memo", "forwardRef"];
const reactHOCs = new Set(["memo", "forwardRef"]);
const canBeReactFunctionComponent = (init: TSESTree.Expression | null) => {
if (!init) return false;
if (init.type === "ArrowFunctionExpression") return true;
if (init.type === "CallExpression" && init.callee.type === "Identifier") {
return reactHOCs.includes(init.callee.name);
return reactHOCs.has(init.callee.name);
}
return false;
};

type ToString<T> = T extends `${infer V}` ? V : never;
const notReactComponentExpression: ToString<TSESTree.Expression["type"]>[] = [
const notReactComponentExpression = new Set<
ToString<TSESTree.Expression["type"]>
>([
"ArrayExpression",
"AwaitExpression",
"BinaryExpression",
Expand All @@ -258,4 +276,4 @@ const notReactComponentExpression: ToString<TSESTree.Expression["type"]>[] = [
"ThisExpression",
"UnaryExpression",
"UpdateExpression",
];
]);

0 comments on commit 897a11d

Please sign in to comment.