@@ -13,8 +13,13 @@ const report = require('../util/report');
13
13
// Rule Definition
14
14
// ------------------------------------------------------------------------------
15
15
16
+ function isNodeDestructuring ( node ) {
17
+ return node && ( node . type === 'ArrayPattern' || node . type === 'ObjectPattern' ) ;
18
+ }
19
+
16
20
const messages = {
17
21
useStateErrorMessage : 'useState call is not destructured into value + setter pair' ,
22
+ useStateErrorMessageOrAddOption : 'useState call is not destructured into value + setter pair (you can allow destructuring by enabling "allowDestructuredState" option)' ,
18
23
} ;
19
24
20
25
module . exports = {
@@ -26,135 +31,166 @@ module.exports = {
26
31
url : docsUrl ( 'hook-use-state' ) ,
27
32
} ,
28
33
messages,
29
- schema : [ ] ,
34
+ schema : [ {
35
+ type : 'object' ,
36
+ properties : {
37
+ allowDestructuredState : {
38
+ default : false ,
39
+ type : 'boolean' ,
40
+ } ,
41
+ } ,
42
+ additionalProperties : false ,
43
+ } ] ,
30
44
type : 'suggestion' ,
31
45
hasSuggestions : true ,
32
46
} ,
33
47
34
- create : Components . detect ( ( context , components , util ) => ( {
35
- CallExpression ( node ) {
36
- const isImmediateReturn = node . parent
37
- && node . parent . type === 'ReturnStatement' ;
38
-
39
- if ( isImmediateReturn || ! util . isReactHookCall ( node , [ 'useState' ] ) ) {
40
- return ;
41
- }
42
-
43
- const isDestructuringDeclarator = node . parent
44
- && node . parent . type === 'VariableDeclarator'
45
- && node . parent . id . type === 'ArrayPattern' ;
46
-
47
- if ( ! isDestructuringDeclarator ) {
48
- report (
49
- context ,
50
- messages . useStateErrorMessage ,
51
- 'useStateErrorMessage' ,
52
- { node }
53
- ) ;
54
- return ;
55
- }
56
-
57
- const variableNodes = node . parent . id . elements ;
58
- const valueVariable = variableNodes [ 0 ] ;
59
- const setterVariable = variableNodes [ 1 ] ;
60
-
61
- const valueVariableName = valueVariable
62
- ? valueVariable . name
63
- : undefined ;
64
-
65
- const setterVariableName = setterVariable
66
- ? setterVariable . name
67
- : undefined ;
68
-
69
- const caseCandidateMatch = valueVariableName ? valueVariableName . match ( / ( ^ [ a - z ] + ) ( .* ) / ) : undefined ;
70
- const upperCaseCandidatePrefix = caseCandidateMatch ? caseCandidateMatch [ 1 ] : undefined ;
71
- const caseCandidateSuffix = caseCandidateMatch ? caseCandidateMatch [ 2 ] : undefined ;
72
- const expectedSetterVariableNames = upperCaseCandidatePrefix ? [
73
- `set${ upperCaseCandidatePrefix . charAt ( 0 ) . toUpperCase ( ) } ${ upperCaseCandidatePrefix . slice ( 1 ) } ${ caseCandidateSuffix } ` ,
74
- `set${ upperCaseCandidatePrefix . toUpperCase ( ) } ${ caseCandidateSuffix } ` ,
75
- ] : [ ] ;
76
-
77
- const isSymmetricGetterSetterPair = valueVariable
78
- && setterVariable
79
- && expectedSetterVariableNames . indexOf ( setterVariableName ) !== - 1
80
- && variableNodes . length === 2 ;
81
-
82
- if ( ! isSymmetricGetterSetterPair ) {
83
- const suggestions = [
84
- {
85
- desc : 'Destructure useState call into value + setter pair' ,
86
- fix : ( fixer ) => {
87
- if ( expectedSetterVariableNames . length === 0 ) {
88
- return ;
89
- }
48
+ create : Components . detect ( ( context , components , util ) => {
49
+ const configuration = context . options [ 0 ] || { } ;
50
+ const allowDestructuredState = configuration . allowDestructuredState || false ;
90
51
91
- const fix = fixer . replaceTextRange (
92
- node . parent . id . range ,
93
- `[ ${ valueVariableName } , ${ expectedSetterVariableNames [ 0 ] } ]`
94
- ) ;
52
+ return {
53
+ CallExpression ( node ) {
54
+ const isImmediateReturn = node . parent
55
+ && node . parent . type === 'ReturnStatement' ;
95
56
96
- return fix ;
97
- } ,
98
- } ,
99
- ] ;
57
+ if ( isImmediateReturn || ! util . isReactHookCall ( node , [ 'useState' ] ) ) {
58
+ return ;
59
+ }
100
60
101
- const defaultReactImports = components . getDefaultReactImports ( ) ;
102
- const defaultReactImportSpecifier = defaultReactImports
103
- ? defaultReactImports [ 0 ]
104
- : undefined ;
61
+ const isDestructuringDeclarator = node . parent
62
+ && node . parent . type === 'VariableDeclarator'
63
+ && node . parent . id . type === 'ArrayPattern' ;
64
+
65
+ if ( ! isDestructuringDeclarator ) {
66
+ report (
67
+ context ,
68
+ messages . useStateErrorMessage ,
69
+ 'useStateErrorMessage' ,
70
+ { node }
71
+ ) ;
72
+ return ;
73
+ }
74
+
75
+ const variableNodes = node . parent . id . elements ;
76
+ const valueVariable = variableNodes [ 0 ] ;
77
+ const setterVariable = variableNodes [ 1 ] ;
78
+ const isOnlyValueDestructuring = isNodeDestructuring ( valueVariable ) && ! isNodeDestructuring ( setterVariable ) ;
79
+
80
+ if ( allowDestructuredState && isOnlyValueDestructuring ) {
81
+ return ;
82
+ }
105
83
106
- const defaultReactImportName = defaultReactImportSpecifier
107
- ? defaultReactImportSpecifier . local . name
84
+ const valueVariableName = valueVariable
85
+ ? valueVariable . name
108
86
: undefined ;
109
87
110
- const namedReactImports = components . getNamedReactImports ( ) ;
111
- const useStateReactImportSpecifier = namedReactImports
112
- ? namedReactImports . find ( ( specifier ) => specifier . imported . name === 'useState' )
88
+ const setterVariableName = setterVariable
89
+ ? setterVariable . name
113
90
: undefined ;
114
91
115
- const isSingleGetter = valueVariable && variableNodes . length === 1 ;
116
- const isUseStateCalledWithSingleArgument = node . arguments . length === 1 ;
117
- if ( isSingleGetter && isUseStateCalledWithSingleArgument ) {
118
- const useMemoReactImportSpecifier = namedReactImports
119
- && namedReactImports . find ( ( specifier ) => specifier . imported . name === 'useMemo' ) ;
120
-
121
- let useMemoCode ;
122
- if ( useMemoReactImportSpecifier ) {
123
- useMemoCode = useMemoReactImportSpecifier . local . name ;
124
- } else if ( defaultReactImportName ) {
125
- useMemoCode = `${ defaultReactImportName } .useMemo` ;
126
- } else {
127
- useMemoCode = 'useMemo' ;
92
+ const caseCandidateMatch = valueVariableName ? valueVariableName . match ( / ( ^ [ a - z ] + ) ( .* ) / ) : undefined ;
93
+ const upperCaseCandidatePrefix = caseCandidateMatch ? caseCandidateMatch [ 1 ] : undefined ;
94
+ const caseCandidateSuffix = caseCandidateMatch ? caseCandidateMatch [ 2 ] : undefined ;
95
+ const expectedSetterVariableNames = upperCaseCandidatePrefix ? [
96
+ `set${ upperCaseCandidatePrefix . charAt ( 0 ) . toUpperCase ( ) } ${ upperCaseCandidatePrefix . slice ( 1 ) } ${ caseCandidateSuffix } ` ,
97
+ `set${ upperCaseCandidatePrefix . toUpperCase ( ) } ${ caseCandidateSuffix } ` ,
98
+ ] : [ ] ;
99
+
100
+ const isSymmetricGetterSetterPair = valueVariable
101
+ && setterVariable
102
+ && expectedSetterVariableNames . indexOf ( setterVariableName ) !== - 1
103
+ && variableNodes . length === 2 ;
104
+
105
+ if ( ! isSymmetricGetterSetterPair ) {
106
+ const suggestions = [
107
+ {
108
+ desc : 'Destructure useState call into value + setter pair' ,
109
+ fix : ( fixer ) => {
110
+ if ( expectedSetterVariableNames . length === 0 ) {
111
+ return ;
112
+ }
113
+
114
+ const fix = fixer . replaceTextRange (
115
+ node . parent . id . range ,
116
+ `[${ valueVariableName } , ${ expectedSetterVariableNames [ 0 ] } ]`
117
+ ) ;
118
+
119
+ return fix ;
120
+ } ,
121
+ } ,
122
+ ] ;
123
+
124
+ const defaultReactImports = components . getDefaultReactImports ( ) ;
125
+ const defaultReactImportSpecifier = defaultReactImports
126
+ ? defaultReactImports [ 0 ]
127
+ : undefined ;
128
+
129
+ const defaultReactImportName = defaultReactImportSpecifier
130
+ ? defaultReactImportSpecifier . local . name
131
+ : undefined ;
132
+
133
+ const namedReactImports = components . getNamedReactImports ( ) ;
134
+ const useStateReactImportSpecifier = namedReactImports
135
+ ? namedReactImports . find ( ( specifier ) => specifier . imported . name === 'useState' )
136
+ : undefined ;
137
+
138
+ const isSingleGetter = valueVariable && variableNodes . length === 1 ;
139
+ const isUseStateCalledWithSingleArgument = node . arguments . length === 1 ;
140
+ if ( isSingleGetter && isUseStateCalledWithSingleArgument ) {
141
+ const useMemoReactImportSpecifier = namedReactImports
142
+ && namedReactImports . find ( ( specifier ) => specifier . imported . name === 'useMemo' ) ;
143
+
144
+ let useMemoCode ;
145
+ if ( useMemoReactImportSpecifier ) {
146
+ useMemoCode = useMemoReactImportSpecifier . local . name ;
147
+ } else if ( defaultReactImportName ) {
148
+ useMemoCode = `${ defaultReactImportName } .useMemo` ;
149
+ } else {
150
+ useMemoCode = 'useMemo' ;
151
+ }
152
+
153
+ suggestions . unshift ( {
154
+ desc : 'Replace useState call with useMemo' ,
155
+ fix : ( fixer ) => [
156
+ // Add useMemo import, if necessary
157
+ useStateReactImportSpecifier
158
+ && ( ! useMemoReactImportSpecifier || defaultReactImportName )
159
+ && fixer . insertTextAfter ( useStateReactImportSpecifier , ', useMemo' ) ,
160
+ // Convert single-value destructure to simple assignment
161
+ fixer . replaceTextRange ( node . parent . id . range , valueVariableName ) ,
162
+ // Convert useState call to useMemo + arrow function + dependency array
163
+ fixer . replaceTextRange (
164
+ node . range ,
165
+ `${ useMemoCode } (() => ${ context . getSourceCode ( ) . getText ( node . arguments [ 0 ] ) } , [])`
166
+ ) ,
167
+ ] . filter ( Boolean ) ,
168
+ } ) ;
128
169
}
129
170
130
- suggestions . unshift ( {
131
- desc : 'Replace useState call with useMemo' ,
132
- fix : ( fixer ) => [
133
- // Add useMemo import, if necessary
134
- useStateReactImportSpecifier
135
- && ( ! useMemoReactImportSpecifier || defaultReactImportName )
136
- && fixer . insertTextAfter ( useStateReactImportSpecifier , ', useMemo' ) ,
137
- // Convert single-value destructure to simple assignment
138
- fixer . replaceTextRange ( node . parent . id . range , valueVariableName ) ,
139
- // Convert useState call to useMemo + arrow function + dependency array
140
- fixer . replaceTextRange (
141
- node . range ,
142
- `${ useMemoCode } (() => ${ context . getSourceCode ( ) . getText ( node . arguments [ 0 ] ) } , [])`
143
- ) ,
144
- ] . filter ( Boolean ) ,
145
- } ) ;
146
- }
147
-
148
- report (
149
- context ,
150
- messages . useStateErrorMessage ,
151
- 'useStateErrorMessage' ,
152
- {
153
- node : node . parent . id ,
154
- suggest : suggestions ,
171
+ if ( isOnlyValueDestructuring ) {
172
+ report (
173
+ context ,
174
+ messages . useStateErrorMessageOrAddOption ,
175
+ 'useStateErrorMessageOrAddOption' ,
176
+ {
177
+ node : node . parent . id ,
178
+ }
179
+ ) ;
180
+ return ;
155
181
}
156
- ) ;
157
- }
158
- } ,
159
- } ) ) ,
182
+
183
+ report (
184
+ context ,
185
+ messages . useStateErrorMessage ,
186
+ 'useStateErrorMessage' ,
187
+ {
188
+ node : node . parent . id ,
189
+ suggest : suggestions ,
190
+ }
191
+ ) ;
192
+ }
193
+ } ,
194
+ } ;
195
+ } ) ,
160
196
} ;
0 commit comments