7
7
8
8
const hasProp = require ( 'jsx-ast-utils/hasProp' ) ;
9
9
const propName = require ( 'jsx-ast-utils/propName' ) ;
10
+ const values = require ( 'object.values' ) ;
10
11
const docsUrl = require ( '../util/docsUrl' ) ;
11
12
const pragmaUtil = require ( '../util/pragma' ) ;
12
13
const report = require ( '../util/report' ) ;
@@ -18,6 +19,7 @@ const report = require('../util/report');
18
19
const defaultOptions = {
19
20
checkFragmentShorthand : false ,
20
21
checkKeyMustBeforeSpread : false ,
22
+ warnOnDuplicates : false ,
21
23
} ;
22
24
23
25
const messages = {
@@ -26,6 +28,7 @@ const messages = {
26
28
missingArrayKey : 'Missing "key" prop for element in array' ,
27
29
missingArrayKeyUsePrag : 'Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead' ,
28
30
keyBeforeSpread : '`key` prop must be placed before any `{...spread}, to avoid conflicting with React’s new JSX transform: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html`' ,
31
+ nonUniqueKeys : '`key` prop must be unique' ,
29
32
} ;
30
33
31
34
module . exports = {
@@ -50,6 +53,10 @@ module.exports = {
50
53
type : 'boolean' ,
51
54
default : defaultOptions . checkKeyMustBeforeSpread ,
52
55
} ,
56
+ warnOnDuplicates : {
57
+ type : 'boolean' ,
58
+ default : defaultOptions . warnOnDuplicates ,
59
+ } ,
53
60
} ,
54
61
additionalProperties : false ,
55
62
} ] ,
@@ -59,6 +66,7 @@ module.exports = {
59
66
const options = Object . assign ( { } , defaultOptions , context . options [ 0 ] ) ;
60
67
const checkFragmentShorthand = options . checkFragmentShorthand ;
61
68
const checkKeyMustBeforeSpread = options . checkKeyMustBeforeSpread ;
69
+ const warnOnDuplicates = options . warnOnDuplicates ;
62
70
const reactPragma = pragmaUtil . getFromContext ( context ) ;
63
71
const fragmentPragma = pragmaUtil . getFragmentFromContext ( context ) ;
64
72
@@ -97,19 +105,43 @@ module.exports = {
97
105
}
98
106
99
107
return {
100
- JSXElement ( node ) {
101
- if ( hasProp ( node . openingElement . attributes , 'key' ) ) {
102
- if ( checkKeyMustBeforeSpread && isKeyAfterSpread ( node . openingElement . attributes ) ) {
103
- report ( context , messages . keyBeforeSpread , 'keyBeforeSpread' , {
104
- node,
105
- } ) ;
106
- }
108
+ ArrayExpression ( node ) {
109
+ const jsx = node . elements . filter ( ( x ) => x . type === 'JSXElement' ) ;
110
+ if ( jsx . length === 0 ) {
107
111
return ;
108
112
}
109
113
110
- if ( node . parent . type === 'ArrayExpression' ) {
111
- report ( context , messages . missingArrayKey , 'missingArrayKey' , {
112
- node,
114
+ const map = { } ;
115
+ jsx . forEach ( ( element ) => {
116
+ const attrs = element . openingElement . attributes ;
117
+ const keys = attrs . filter ( ( x ) => x . name && x . name . name === 'key' ) ;
118
+
119
+ if ( keys . length === 0 ) {
120
+ report ( context , messages . missingArrayKey , 'missingArrayKey' , {
121
+ node : element ,
122
+ } ) ;
123
+ } else {
124
+ keys . forEach ( ( attr ) => {
125
+ const value = context . getSourceCode ( ) . getText ( attr . value ) ;
126
+ if ( ! map [ value ] ) { map [ value ] = [ ] ; }
127
+ map [ value ] . push ( attr ) ;
128
+
129
+ if ( checkKeyMustBeforeSpread && isKeyAfterSpread ( attrs ) ) {
130
+ report ( context , messages . keyBeforeSpread , 'keyBeforeSpread' , {
131
+ node,
132
+ } ) ;
133
+ }
134
+ } ) ;
135
+ }
136
+ } ) ;
137
+
138
+ if ( warnOnDuplicates ) {
139
+ values ( map ) . filter ( ( v ) => v . length > 1 ) . forEach ( ( v ) => {
140
+ v . forEach ( ( n ) => {
141
+ report ( context , messages . nonUniqueKeys , 'nonUniqueKeys' , {
142
+ node : n ,
143
+ } ) ;
144
+ } ) ;
113
145
} ) ;
114
146
}
115
147
} ,
0 commit comments