@@ -25,6 +25,7 @@ const messages = {
25
25
fragmentsNotSupported : 'Fragments are only supported starting from React v16.2. '
26
26
+ 'Please disable the `react/jsx-fragments` rule in `eslint` settings or upgrade your version of React.' ,
27
27
preferPragma : 'Prefer {{react}}.{{fragment}} over fragment shorthand' ,
28
+ preferPragmaShort : 'Prefer {{fragment}} over fragment shorthand' ,
28
29
preferFragment : 'Prefer fragment shorthand over {{react}}.{{fragment}}' ,
29
30
} ;
30
31
@@ -41,7 +42,7 @@ module.exports = {
41
42
messages,
42
43
43
44
schema : [ {
44
- enum : [ 'syntax' , 'element' ] ,
45
+ enum : [ 'syntax' , 'element' , 'elementShort' ] ,
45
46
} ] ,
46
47
} ,
47
48
@@ -53,6 +54,11 @@ module.exports = {
53
54
const closeFragShort = '</>' ;
54
55
const openFragLong = `<${ reactPragma } .${ fragmentPragma } >` ;
55
56
const closeFragLong = `</${ reactPragma } .${ fragmentPragma } >` ;
57
+ const openFragMedium = `<${ fragmentPragma } >` ;
58
+ const closeFragMedium = `</${ fragmentPragma } >` ;
59
+
60
+ let reactImportFound = false ;
61
+ const reactImports = [ ] ;
56
62
57
63
function reportOnReactVersion ( node ) {
58
64
if ( ! testReactVersion ( context , '>= 16.2.0' ) ) {
@@ -65,20 +71,50 @@ module.exports = {
65
71
return false ;
66
72
}
67
73
68
- function getFixerToLong ( jsxFragment ) {
74
+ function getFixerToLong ( jsxFragment , withoutReactPragma ) {
69
75
if ( ! jsxFragment . closingFragment || ! jsxFragment . openingFragment ) {
70
76
// the old TS parser crashes here
71
77
// TODO: FIXME: can we fake these two descriptors?
72
78
return null ;
73
79
}
74
80
return function fix ( fixer ) {
81
+ const closeFrag = withoutReactPragma ? closeFragMedium : closeFragLong ;
82
+ const openFrag = withoutReactPragma ? openFragMedium : openFragLong ;
75
83
let source = getText ( context ) ;
76
- source = replaceNode ( source , jsxFragment . closingFragment , closeFragLong ) ;
77
- source = replaceNode ( source , jsxFragment . openingFragment , openFragLong ) ;
78
- const lengthDiff = openFragLong . length - getText ( context , jsxFragment . openingFragment ) . length
79
- + closeFragLong . length - getText ( context , jsxFragment . closingFragment ) . length ;
84
+ source = replaceNode ( source , jsxFragment . closingFragment , closeFrag ) ;
85
+ source = replaceNode ( source , jsxFragment . openingFragment , openFrag ) ;
86
+ const lengthDiff = openFrag . length - getText ( context , jsxFragment . openingFragment ) . length
87
+ + closeFrag . length - getText ( context , jsxFragment . closingFragment ) . length ;
80
88
const range = jsxFragment . range ;
81
- return fixer . replaceTextRange ( range , source . slice ( range [ 0 ] , range [ 1 ] + lengthDiff ) ) ;
89
+
90
+ const fixes = [ ] ;
91
+
92
+ // Insert the import statement at the top of the file if `withoutReactPragma` is true
93
+ if ( withoutReactPragma ) {
94
+ const ancestors = context . getAncestors ( ) ;
95
+ const rootNode = ancestors . length > 0 ? ancestors [ 0 ] : jsxFragment ;
96
+ const reactImport = reactImports . find ( importNode =>
97
+ importNode . specifiers . some ( spec => spec . imported && spec . imported . name === fragmentPragma )
98
+ ) ;
99
+
100
+ if ( ! reactImport ) {
101
+ // No `Fragment` import found, so add it
102
+ const existingReactImport = reactImports . find ( importNode => importNode . source . value === 'react' ) ;
103
+
104
+ if ( existingReactImport ) {
105
+ // If there's already an import from 'react', add `Fragment` to the existing specifiers
106
+ const lastSpecifier = existingReactImport . specifiers [ existingReactImport . specifiers . length - 1 ] ;
107
+ fixes . push ( fixer . insertTextAfter ( lastSpecifier , `, ${ fragmentPragma } ` ) ) ;
108
+ } else {
109
+ // Otherwise, add a new import statement at the top
110
+ fixes . push ( fixer . insertTextBefore ( rootNode . body [ 0 ] , `import { Fragment } from 'react';\n\n` ) ) ;
111
+ }
112
+ }
113
+ }
114
+
115
+ fixes . push ( fixer . replaceTextRange ( range , source . slice ( range [ 0 ] , range [ 1 ] + lengthDiff ) ) )
116
+
117
+ return fixes ;
82
118
} ;
83
119
}
84
120
@@ -165,12 +201,27 @@ module.exports = {
165
201
fix : getFixerToLong ( node ) ,
166
202
} ) ;
167
203
}
204
+
205
+ if ( configuration === 'elementShort' ) {
206
+ report ( context , messages . preferPragmaShort , 'preferPragmaShort' , {
207
+ node,
208
+ data : {
209
+ react : reactPragma ,
210
+ fragment : fragmentPragma ,
211
+ } ,
212
+ fix : getFixerToLong ( node , true ) ,
213
+ } ) ;
214
+ }
168
215
} ,
169
216
170
217
ImportDeclaration ( node ) {
171
218
if ( node . source && node . source . value === 'react' ) {
219
+ reactImports . push ( node ) ;
220
+ let hasFragment = false ;
221
+
172
222
node . specifiers . forEach ( ( spec ) => {
173
223
if ( spec . imported && spec . imported . name === fragmentPragma ) {
224
+ hasFragment = true ;
174
225
if ( spec . local ) {
175
226
fragmentNames . add ( spec . local . name ) ;
176
227
}
0 commit comments