@@ -15,12 +15,16 @@ export type Options = [{
1515 allowConstantExport ?: boolean ;
1616 checkJS ?: boolean ;
1717 allowExportNames ?: string [ ] ;
18+ customHOCs ?: string [ ] ;
1819} ] ;
1920
20- const defaultOptions : Options = [ { } ] ;
21- const possibleRegex = / ^ [ A - Z ] [ a - z A - Z 0 - 9 ] * $ / u;
22- const strictRegex = / ^ [ A - Z ] [ \d A - Z ] * [ a - z ] [ \d A - Z a - z ] * $ / u;
23- const reactHOCs = new Set ( [ 'forwardRef' , 'memo' ] ) ;
21+ const defaultOptions : Options = [ {
22+ allowConstantExport : false ,
23+ checkJS : false ,
24+ allowExportNames : [ ] ,
25+ customHOCs : [ ] ,
26+ } ] ;
27+ const reactComponentNameRE = / ^ [ A - Z ] [ a - z A - Z 0 - 9 ] * $ / u;
2428type ToString < Type > = Type extends `${infer String } ` ? String : never ;
2529const notReactComponentExpression : Set < ToString < TSESTree . Expression [ 'type' ] > > = new Set ( [
2630 'ArrayExpression' ,
@@ -49,20 +53,30 @@ export default createEslintRule<Options, MessageIds>({
4953
5054 return {
5155 Program : ( program ) => {
52- const ruleContext = {
53- hasExports : false ,
54- mayHaveReactExport : false ,
55- reactIsInScope : false ,
56- } ;
56+ const ruleContext = { hasExports : false , hasReactExport : false , reactIsInScope : false } ;
5757 const localComponents : TSESTree . Identifier [ ] = [ ] ;
5858 const nonComponentExports : Array < TSESTree . BindingName | TSESTree . StringLiteral > = [ ] ;
59- const allowExportNamesSet = options . allowExportNames ? new Set ( options . allowExportNames ) : undefined ;
59+ const allowExportNames = new Set ( options . allowExportNames ) ;
60+ const reactHOCs = new Set ( [ 'forwardRef' , 'memo' , ...options . customHOCs ! ] ) ;
6061 const reactContextExports : TSESTree . Identifier [ ] = [ ] ;
6162
63+ const canBeReactFunctionComponent = ( init : TSESTree . VariableDeclaratorMaybeInit [ 'init' ] ) : boolean => {
64+ if ( ! init )
65+ return false ;
66+
67+ if ( init . type === 'ArrowFunctionExpression' )
68+ return true ;
69+
70+ if ( init . type === 'CallExpression' && init . callee . type === 'Identifier' )
71+ return reactHOCs . has ( init . callee . name ) ;
72+
73+ return false ;
74+ } ;
75+
6276 const handleLocalIdentifier = ( id : TSESTree . BindingName ) : void => {
6377 if ( id . type !== 'Identifier' )
6478 return ;
65- if ( possibleRegex . test ( id . name ) )
79+ if ( reactComponentNameRE . test ( id . name ) )
6680 localComponents . push ( id ) ;
6781 } ;
6882
@@ -72,16 +86,16 @@ export default createEslintRule<Options, MessageIds>({
7286 return ;
7387 }
7488
75- if ( allowExportNamesSet ? .has ( id . name ) )
89+ if ( allowExportNames . has ( id . name ) )
7690 return ;
7791
7892 // Literal: 1, 'foo', UnaryExpression: -1, TemplateLiteral: `Some ${template}`, BinaryExpression: 24 * 60.
7993 if ( options . allowConstantExport && init && [ 'BinaryExpression' , 'Literal' , 'TemplateLiteral' , 'UnaryExpression' ] . includes ( init . type ) )
8094 return ;
8195
8296 if ( isFn ) {
83- if ( possibleRegex . test ( id . name ) )
84- ruleContext . mayHaveReactExport = true ;
97+ if ( reactComponentNameRE . test ( id . name ) )
98+ ruleContext . hasReactExport = true ;
8599 else nonComponentExports . push ( id ) ;
86100 }
87101 else {
@@ -100,9 +114,9 @@ export default createEslintRule<Options, MessageIds>({
100114 nonComponentExports . push ( id ) ;
101115 return ;
102116 }
103- if ( ! ruleContext . mayHaveReactExport && possibleRegex . test ( id . name ) )
104- ruleContext . mayHaveReactExport = true ;
105- if ( ! strictRegex . test ( id . name ) )
117+ if ( reactComponentNameRE . test ( id . name ) )
118+ ruleContext . hasReactExport = true ;
119+ else
106120 nonComponentExports . push ( id ) ;
107121 }
108122 } ;
@@ -116,17 +130,17 @@ export default createEslintRule<Options, MessageIds>({
116130 else handleExportIdentifier ( node . id , true ) ;
117131 else if ( node . type === 'CallExpression' )
118132 if ( node . callee . type === 'CallExpression' && node . callee . callee . type === 'Identifier' && node . callee . callee . name === 'connect' )
119- ruleContext . mayHaveReactExport = true ;
133+ ruleContext . hasReactExport = true ;
120134 else if ( node . callee . type !== 'Identifier' )
121135 if ( node . callee . type === 'MemberExpression' && node . callee . property . type === 'Identifier' && reactHOCs . has ( node . callee . property . name ) )
122- ruleContext . mayHaveReactExport = true ;
136+ ruleContext . hasReactExport = true ;
123137 else context . report ( { messageId : 'anonymousExport' , node } ) ;
124138 else if ( ! reactHOCs . has ( node . callee . name ) )
125139 context . report ( { messageId : 'anonymousExport' , node } ) ;
126140 else if ( node . arguments [ 0 ] . type === 'FunctionExpression' && node . arguments [ 0 ] . id )
127141 handleExportIdentifier ( node . arguments [ 0 ] . id , true ) ;
128142 else if ( node . arguments [ 0 ] ?. type === 'Identifier' )
129- ruleContext . mayHaveReactExport = true ;
143+ ruleContext . hasReactExport = true ;
130144 else context . report ( { messageId : 'anonymousExport' , node } ) ;
131145 else if ( node . type === 'TSEnumDeclaration' )
132146 nonComponentExports . push ( node . id ) ;
@@ -141,11 +155,10 @@ export default createEslintRule<Options, MessageIds>({
141155 }
142156 else if ( node . type === 'ExportDefaultDeclaration' ) {
143157 ruleContext . hasExports = true ;
144- const declaration
145- = node . declaration . type === 'TSAsExpression'
158+ const declaration = node . declaration . type === 'TSAsExpression'
146159 || node . declaration . type === 'TSSatisfiesExpression'
147- ? node . declaration . expression
148- : node . declaration ;
160+ ? node . declaration . expression
161+ : node . declaration ;
149162 if (
150163 declaration . type === 'VariableDeclaration'
151164 || declaration . type === 'FunctionDeclaration'
@@ -181,7 +194,7 @@ export default createEslintRule<Options, MessageIds>({
181194 return ;
182195
183196 if ( ruleContext . hasExports )
184- if ( ruleContext . mayHaveReactExport ) {
197+ if ( ruleContext . hasReactExport ) {
185198 nonComponentExports . forEach ( node => context . report ( { messageId : 'namedExport' , node } ) ) ;
186199 reactContextExports . forEach ( node => context . report ( { messageId : 'reactContext' , node } ) ) ;
187200 }
@@ -214,21 +227,11 @@ export default createEslintRule<Options, MessageIds>({
214227 allowConstantExport : { type : 'boolean' } ,
215228 allowExportNames : { items : { type : 'string' } , type : 'array' } ,
216229 checkJS : { type : 'boolean' } ,
230+ customHOCs : { type : 'array' , items : { type : 'string' } } ,
217231 } satisfies Readonly < Record < keyof Options [ 0 ] , JSONSchema4 > > ,
218232 type : 'object' ,
219233 } ] ,
220234 type : 'problem' ,
221235 } ,
222236 name : RULE_NAME ,
223237} ) ;
224-
225- function canBeReactFunctionComponent ( init : TSESTree . Expression | null ) : boolean {
226- if ( ! init )
227- return false ;
228- if ( init . type === 'ArrowFunctionExpression' )
229- return true ;
230- if ( init . type === 'CallExpression' && init . callee . type === 'Identifier' )
231- return [ 'forwardRef' , 'memo' ] . includes ( init . callee . name ) ;
232-
233- return false ;
234- }
0 commit comments