Skip to content

Commit 897a11d

Browse files
authored
Handle memo for function declarations / function assignments [publish] (#45)
1 parent a10b96b commit 897a11d

File tree

4 files changed

+39
-9
lines changed

4 files changed

+39
-9
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.4.9
4+
5+
- Support `function Foo() {}; export default memo(Foo)` (fixes #44)
6+
37
## 0.4.8
48

59
- Support `export const foo = -1` with `allowConstantExport` (fixes #43)

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "eslint-plugin-react-refresh",
3-
"version": "0.4.8",
3+
"version": "0.4.9",
44
"type": "module",
55
"license": "MIT",
66
"scripts": {

src/only-export-components.test.ts

+8
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ const valid = [
8989
name: "export default memo function",
9090
code: "export default memo(function Foo () {});",
9191
},
92+
{
93+
name: "export default memo function assignment",
94+
code: "const Foo = () => {}; export default memo(Foo);",
95+
},
96+
{
97+
name: "export default memo function declaration",
98+
code: "function Foo() {}; export default memo(Foo);",
99+
},
92100
{
93101
name: "export type *",
94102
code: "export type * from './module';",

src/only-export-components.ts

+26-8
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ export const onlyExportComponents: TSESLint.RuleModule<
7272
(checkJS && filename.endsWith(".js"));
7373
if (!shouldScan) return {};
7474

75+
const allowExportNamesSet = allowExportNames
76+
? new Set(allowExportNames)
77+
: undefined;
78+
7579
return {
7680
Program(program) {
7781
let hasExports = false;
@@ -98,7 +102,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
98102
nonComponentExports.push(identifierNode);
99103
return;
100104
}
101-
if (allowExportNames?.includes(identifierNode.name)) return;
105+
if (allowExportNamesSet?.has(identifierNode.name)) return;
102106
if (
103107
allowConstantExport &&
104108
init &&
@@ -109,6 +113,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
109113
) {
110114
return;
111115
}
116+
112117
if (isFunction) {
113118
if (possibleReactExportRE.test(identifierNode.name)) {
114119
mayHaveReactExport = true;
@@ -119,7 +124,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
119124
if (
120125
init &&
121126
// Switch to allowList?
122-
notReactComponentExpression.includes(init.type)
127+
notReactComponentExpression.has(init.type)
123128
) {
124129
nonComponentExports.push(identifierNode);
125130
return;
@@ -153,12 +158,23 @@ export const onlyExportComponents: TSESLint.RuleModule<
153158
}
154159
} else if (node.type === "CallExpression") {
155160
if (
156-
node.callee.type === "Identifier" &&
157-
reactHOCs.includes(node.callee.name) &&
161+
node.callee.type !== "Identifier" ||
162+
!reactHOCs.has(node.callee.name)
163+
) {
164+
// we rule out non HoC first
165+
context.report({ messageId: "anonymousExport", node });
166+
} else if (
158167
node.arguments[0]?.type === "FunctionExpression" &&
159168
node.arguments[0].id
160169
) {
170+
// export default memo(function Foo() {})
161171
handleExportIdentifier(node.arguments[0].id, true);
172+
} else if (node.arguments[0]?.type === "Identifier") {
173+
// const Foo = () => {}; export default memo(Foo);
174+
// No need to check further, the identifier has necessarily a named,
175+
// and it would throw at runtime if it's not a React component.
176+
// We have React exports since we are exporting return value of HoC
177+
mayHaveReactExport = true;
162178
} else {
163179
context.report({ messageId: "anonymousExport", node });
164180
}
@@ -234,18 +250,20 @@ export const onlyExportComponents: TSESLint.RuleModule<
234250
},
235251
};
236252

237-
const reactHOCs = ["memo", "forwardRef"];
253+
const reactHOCs = new Set(["memo", "forwardRef"]);
238254
const canBeReactFunctionComponent = (init: TSESTree.Expression | null) => {
239255
if (!init) return false;
240256
if (init.type === "ArrowFunctionExpression") return true;
241257
if (init.type === "CallExpression" && init.callee.type === "Identifier") {
242-
return reactHOCs.includes(init.callee.name);
258+
return reactHOCs.has(init.callee.name);
243259
}
244260
return false;
245261
};
246262

247263
type ToString<T> = T extends `${infer V}` ? V : never;
248-
const notReactComponentExpression: ToString<TSESTree.Expression["type"]>[] = [
264+
const notReactComponentExpression = new Set<
265+
ToString<TSESTree.Expression["type"]>
266+
>([
249267
"ArrayExpression",
250268
"AwaitExpression",
251269
"BinaryExpression",
@@ -258,4 +276,4 @@ const notReactComponentExpression: ToString<TSESTree.Expression["type"]>[] = [
258276
"ThisExpression",
259277
"UnaryExpression",
260278
"UpdateExpression",
261-
];
279+
]);

0 commit comments

Comments
 (0)