@@ -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