@@ -80,9 +80,13 @@ export const onlyExportComponents: TSESLint.RuleModule<
80
80
const reactHOCs = [ "memo" , "forwardRef" , ...customHOCs ] ;
81
81
const canBeReactFunctionComponent = ( init : TSESTree . Expression | null ) => {
82
82
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 ) ;
86
90
}
87
91
return false ;
88
92
} ;
@@ -153,6 +157,42 @@ export const onlyExportComponents: TSESLint.RuleModule<
153
157
}
154
158
} ;
155
159
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
+
156
196
const handleExportDeclaration = ( node : TSESTree . ExportDeclaration ) => {
157
197
if ( node . type === "VariableDeclaration" ) {
158
198
for ( const variable of node . declarations ) {
@@ -169,41 +209,8 @@ export const onlyExportComponents: TSESLint.RuleModule<
169
209
handleExportIdentifier ( node . id , true ) ;
170
210
}
171
211
} 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 ) {
207
214
hasReactExport = true ;
208
215
} else {
209
216
context . report ( { messageId : "anonymousExport" , node } ) ;
@@ -237,7 +244,9 @@ export const onlyExportComponents: TSESLint.RuleModule<
237
244
} else if ( node . type === "ExportNamedDeclaration" ) {
238
245
if ( node . exportKind === "type" ) continue ;
239
246
hasExports = true ;
240
- if ( node . declaration ) handleExportDeclaration ( node . declaration ) ;
247
+ if ( node . declaration ) {
248
+ handleExportDeclaration ( skipTSWrapper ( node . declaration ) ) ;
249
+ }
241
250
for ( const specifier of node . specifiers ) {
242
251
handleExportIdentifier (
243
252
specifier . exported . type === "Identifier" &&
0 commit comments