@@ -13,8 +13,13 @@ const report = require('../util/report');
1313// Rule Definition
1414// ------------------------------------------------------------------------------
1515
16+ function isNodeDestructuring ( node ) {
17+ return node && ( node . type === 'ArrayPattern' || node . type === 'ObjectPattern' ) ;
18+ }
19+
1620const messages = {
1721 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)' ,
1823} ;
1924
2025module . exports = {
@@ -26,135 +31,166 @@ module.exports = {
2631 url : docsUrl ( 'hook-use-state' ) ,
2732 } ,
2833 messages,
29- schema : [ ] ,
34+ schema : [ {
35+ type : 'object' ,
36+ properties : {
37+ allowDestructuredState : {
38+ default : false ,
39+ type : 'boolean' ,
40+ } ,
41+ } ,
42+ additionalProperties : false ,
43+ } ] ,
3044 type : 'suggestion' ,
3145 hasSuggestions : true ,
3246 } ,
3347
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 ;
9051
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' ;
9556
96- return fix ;
97- } ,
98- } ,
99- ] ;
57+ if ( isImmediateReturn || ! util . isReactHookCall ( node , [ 'useState' ] ) ) {
58+ return ;
59+ }
10060
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+ }
10583
106- const defaultReactImportName = defaultReactImportSpecifier
107- ? defaultReactImportSpecifier . local . name
84+ const valueVariableName = valueVariable
85+ ? valueVariable . name
10886 : undefined ;
10987
110- const namedReactImports = components . getNamedReactImports ( ) ;
111- const useStateReactImportSpecifier = namedReactImports
112- ? namedReactImports . find ( ( specifier ) => specifier . imported . name === 'useState' )
88+ const setterVariableName = setterVariable
89+ ? setterVariable . name
11390 : undefined ;
11491
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+ } ) ;
128169 }
129170
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 ;
155181 }
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+ } ) ,
160196} ;
0 commit comments