@@ -15,12 +15,16 @@ export type Options = [{
15
15
allowConstantExport ?: boolean ;
16
16
checkJS ?: boolean ;
17
17
allowExportNames ?: string [ ] ;
18
+ customHOCs ?: string [ ] ;
18
19
} ] ;
19
20
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;
24
28
type ToString < Type > = Type extends `${infer String } ` ? String : never ;
25
29
const notReactComponentExpression : Set < ToString < TSESTree . Expression [ 'type' ] > > = new Set ( [
26
30
'ArrayExpression' ,
@@ -49,20 +53,30 @@ export default createEslintRule<Options, MessageIds>({
49
53
50
54
return {
51
55
Program : ( program ) => {
52
- const ruleContext = {
53
- hasExports : false ,
54
- mayHaveReactExport : false ,
55
- reactIsInScope : false ,
56
- } ;
56
+ const ruleContext = { hasExports : false , hasReactExport : false , reactIsInScope : false } ;
57
57
const localComponents : TSESTree . Identifier [ ] = [ ] ;
58
58
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 ! ] ) ;
60
61
const reactContextExports : TSESTree . Identifier [ ] = [ ] ;
61
62
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
+
62
76
const handleLocalIdentifier = ( id : TSESTree . BindingName ) : void => {
63
77
if ( id . type !== 'Identifier' )
64
78
return ;
65
- if ( possibleRegex . test ( id . name ) )
79
+ if ( reactComponentNameRE . test ( id . name ) )
66
80
localComponents . push ( id ) ;
67
81
} ;
68
82
@@ -72,16 +86,16 @@ export default createEslintRule<Options, MessageIds>({
72
86
return ;
73
87
}
74
88
75
- if ( allowExportNamesSet ? .has ( id . name ) )
89
+ if ( allowExportNames . has ( id . name ) )
76
90
return ;
77
91
78
92
// Literal: 1, 'foo', UnaryExpression: -1, TemplateLiteral: `Some ${template}`, BinaryExpression: 24 * 60.
79
93
if ( options . allowConstantExport && init && [ 'BinaryExpression' , 'Literal' , 'TemplateLiteral' , 'UnaryExpression' ] . includes ( init . type ) )
80
94
return ;
81
95
82
96
if ( isFn ) {
83
- if ( possibleRegex . test ( id . name ) )
84
- ruleContext . mayHaveReactExport = true ;
97
+ if ( reactComponentNameRE . test ( id . name ) )
98
+ ruleContext . hasReactExport = true ;
85
99
else nonComponentExports . push ( id ) ;
86
100
}
87
101
else {
@@ -100,9 +114,9 @@ export default createEslintRule<Options, MessageIds>({
100
114
nonComponentExports . push ( id ) ;
101
115
return ;
102
116
}
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
106
120
nonComponentExports . push ( id ) ;
107
121
}
108
122
} ;
@@ -116,17 +130,17 @@ export default createEslintRule<Options, MessageIds>({
116
130
else handleExportIdentifier ( node . id , true ) ;
117
131
else if ( node . type === 'CallExpression' )
118
132
if ( node . callee . type === 'CallExpression' && node . callee . callee . type === 'Identifier' && node . callee . callee . name === 'connect' )
119
- ruleContext . mayHaveReactExport = true ;
133
+ ruleContext . hasReactExport = true ;
120
134
else if ( node . callee . type !== 'Identifier' )
121
135
if ( node . callee . type === 'MemberExpression' && node . callee . property . type === 'Identifier' && reactHOCs . has ( node . callee . property . name ) )
122
- ruleContext . mayHaveReactExport = true ;
136
+ ruleContext . hasReactExport = true ;
123
137
else context . report ( { messageId : 'anonymousExport' , node } ) ;
124
138
else if ( ! reactHOCs . has ( node . callee . name ) )
125
139
context . report ( { messageId : 'anonymousExport' , node } ) ;
126
140
else if ( node . arguments [ 0 ] . type === 'FunctionExpression' && node . arguments [ 0 ] . id )
127
141
handleExportIdentifier ( node . arguments [ 0 ] . id , true ) ;
128
142
else if ( node . arguments [ 0 ] ?. type === 'Identifier' )
129
- ruleContext . mayHaveReactExport = true ;
143
+ ruleContext . hasReactExport = true ;
130
144
else context . report ( { messageId : 'anonymousExport' , node } ) ;
131
145
else if ( node . type === 'TSEnumDeclaration' )
132
146
nonComponentExports . push ( node . id ) ;
@@ -141,11 +155,10 @@ export default createEslintRule<Options, MessageIds>({
141
155
}
142
156
else if ( node . type === 'ExportDefaultDeclaration' ) {
143
157
ruleContext . hasExports = true ;
144
- const declaration
145
- = node . declaration . type === 'TSAsExpression'
158
+ const declaration = node . declaration . type === 'TSAsExpression'
146
159
|| node . declaration . type === 'TSSatisfiesExpression'
147
- ? node . declaration . expression
148
- : node . declaration ;
160
+ ? node . declaration . expression
161
+ : node . declaration ;
149
162
if (
150
163
declaration . type === 'VariableDeclaration'
151
164
|| declaration . type === 'FunctionDeclaration'
@@ -181,7 +194,7 @@ export default createEslintRule<Options, MessageIds>({
181
194
return ;
182
195
183
196
if ( ruleContext . hasExports )
184
- if ( ruleContext . mayHaveReactExport ) {
197
+ if ( ruleContext . hasReactExport ) {
185
198
nonComponentExports . forEach ( node => context . report ( { messageId : 'namedExport' , node } ) ) ;
186
199
reactContextExports . forEach ( node => context . report ( { messageId : 'reactContext' , node } ) ) ;
187
200
}
@@ -214,21 +227,11 @@ export default createEslintRule<Options, MessageIds>({
214
227
allowConstantExport : { type : 'boolean' } ,
215
228
allowExportNames : { items : { type : 'string' } , type : 'array' } ,
216
229
checkJS : { type : 'boolean' } ,
230
+ customHOCs : { type : 'array' , items : { type : 'string' } } ,
217
231
} satisfies Readonly < Record < keyof Options [ 0 ] , JSONSchema4 > > ,
218
232
type : 'object' ,
219
233
} ] ,
220
234
type : 'problem' ,
221
235
} ,
222
236
name : RULE_NAME ,
223
237
} ) ;
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