Skip to content

Commit 4ddbb1f

Browse files
committed
Don't warn on nested HOC calls [publish]
1 parent b7efe8d commit 4ddbb1f

File tree

4 files changed

+63
-41
lines changed

4 files changed

+63
-41
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Changelog
22

3-
## Unreleased
3+
## 0.4.20
44

5+
- Don't warn on nested HOC calls (fixes [#79](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/issues/79))
56
- Fix false positive with `as const` (fixes [#80](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/issues/80))
67

78
## 0.4.19

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.19",
3+
"version": "0.4.20",
44
"type": "module",
55
"license": "MIT",
66
"scripts": {

src/only-export-components.test.ts

+12
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,18 @@ const valid = [
203203
code: "export const MyComponent = () => {}; export const MENU_WIDTH = 232 as const;",
204204
options: [{ allowConstantExport: true }],
205205
},
206+
{
207+
name: "Type assertion in memo export",
208+
code: "export const MyComponent = () => {}; export default memo(MyComponent as any);",
209+
},
210+
{
211+
name: "Type assertion for memo export",
212+
code: "export const MyComponent = () => {}; export default memo(MyComponent) as any;",
213+
},
214+
{
215+
name: "Nested memo HOC",
216+
code: "export const MyComponent = () => {}; export default memo(forwardRef(MyComponent));",
217+
},
206218
];
207219

208220
const invalid = [

src/only-export-components.ts

+48-39
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,13 @@ export const onlyExportComponents: TSESLint.RuleModule<
8080
const reactHOCs = ["memo", "forwardRef", ...customHOCs];
8181
const canBeReactFunctionComponent = (init: TSESTree.Expression | null) => {
8282
if (!init) return false;
83-
if (init.type === "ArrowFunctionExpression") return true;
84-
if (init.type === "CallExpression" && init.callee.type === "Identifier") {
85-
return reactHOCs.includes(init.callee.name);
83+
const jsInit = skipTSWrapper(init);
84+
if (jsInit.type === "ArrowFunctionExpression") return true;
85+
if (
86+
jsInit.type === "CallExpression" &&
87+
jsInit.callee.type === "Identifier"
88+
) {
89+
return reactHOCs.includes(jsInit.callee.name);
8690
}
8791
return false;
8892
};
@@ -153,6 +157,42 @@ export const onlyExportComponents: TSESLint.RuleModule<
153157
}
154158
};
155159

160+
const isHOCCallExpression = (
161+
node: TSESTree.CallExpression,
162+
): boolean => {
163+
const isCalleeHOC =
164+
// support for react-redux
165+
// export default connect(mapStateToProps, mapDispatchToProps)(...)
166+
(node.callee.type === "CallExpression" &&
167+
node.callee.callee.type === "Identifier" &&
168+
node.callee.callee.name === "connect") ||
169+
// React.memo(...)
170+
(node.callee.type === "MemberExpression" &&
171+
node.callee.property.type === "Identifier" &&
172+
reactHOCs.includes(node.callee.property.name)) ||
173+
// memo(...)
174+
(node.callee.type === "Identifier" &&
175+
reactHOCs.includes(node.callee.name));
176+
if (!isCalleeHOC) return false;
177+
if (node.arguments.length === 0) return false;
178+
const arg = skipTSWrapper(node.arguments[0]);
179+
switch (arg.type) {
180+
case "Identifier":
181+
// memo(Component)
182+
return true;
183+
case "FunctionExpression":
184+
if (!arg.id) return false;
185+
// memo(function Component() {})
186+
handleExportIdentifier(arg.id, true);
187+
return true;
188+
case "CallExpression":
189+
// memo(forwardRef(...))
190+
return isHOCCallExpression(arg);
191+
default:
192+
return false;
193+
}
194+
};
195+
156196
const handleExportDeclaration = (node: TSESTree.ExportDeclaration) => {
157197
if (node.type === "VariableDeclaration") {
158198
for (const variable of node.declarations) {
@@ -169,41 +209,8 @@ export const onlyExportComponents: TSESLint.RuleModule<
169209
handleExportIdentifier(node.id, true);
170210
}
171211
} else if (node.type === "CallExpression") {
172-
if (
173-
node.callee.type === "CallExpression" &&
174-
node.callee.callee.type === "Identifier" &&
175-
node.callee.callee.name === "connect"
176-
) {
177-
// support for react-redux
178-
// export default connect(mapStateToProps, mapDispatchToProps)(Comp)
179-
hasReactExport = true;
180-
} else if (node.callee.type !== "Identifier") {
181-
// we rule out non HoC first
182-
// export default React.memo(function Foo() {})
183-
// export default Preact.memo(function Foo() {})
184-
if (
185-
node.callee.type === "MemberExpression" &&
186-
node.callee.property.type === "Identifier" &&
187-
reactHOCs.includes(node.callee.property.name)
188-
) {
189-
hasReactExport = true;
190-
} else {
191-
context.report({ messageId: "anonymousExport", node });
192-
}
193-
} else if (!reactHOCs.includes(node.callee.name)) {
194-
// we rule out non HoC first
195-
context.report({ messageId: "anonymousExport", node });
196-
} else if (
197-
node.arguments[0]?.type === "FunctionExpression" &&
198-
node.arguments[0].id
199-
) {
200-
// export default memo(function Foo() {})
201-
handleExportIdentifier(node.arguments[0].id, true);
202-
} else if (node.arguments[0]?.type === "Identifier") {
203-
// const Foo = () => {}; export default memo(Foo);
204-
// No need to check further, the identifier has necessarily a named,
205-
// and it would throw at runtime if it's not a React component.
206-
// We have React exports since we are exporting return value of HoC
212+
const isValid = isHOCCallExpression(node);
213+
if (isValid) {
207214
hasReactExport = true;
208215
} else {
209216
context.report({ messageId: "anonymousExport", node });
@@ -237,7 +244,9 @@ export const onlyExportComponents: TSESLint.RuleModule<
237244
} else if (node.type === "ExportNamedDeclaration") {
238245
if (node.exportKind === "type") continue;
239246
hasExports = true;
240-
if (node.declaration) handleExportDeclaration(node.declaration);
247+
if (node.declaration) {
248+
handleExportDeclaration(skipTSWrapper(node.declaration));
249+
}
241250
for (const specifier of node.specifiers) {
242251
handleExportIdentifier(
243252
specifier.exported.type === "Identifier" &&

0 commit comments

Comments
 (0)