77
88const hasProp = require ( 'jsx-ast-utils/hasProp' ) ;
99const propName = require ( 'jsx-ast-utils/propName' ) ;
10+ const values = require ( 'object.values' ) ;
1011const docsUrl = require ( '../util/docsUrl' ) ;
1112const pragmaUtil = require ( '../util/pragma' ) ;
1213const report = require ( '../util/report' ) ;
@@ -18,6 +19,7 @@ const report = require('../util/report');
1819const defaultOptions = {
1920 checkFragmentShorthand : false ,
2021 checkKeyMustBeforeSpread : false ,
22+ warnOnDuplicates : false ,
2123} ;
2224
2325const messages = {
@@ -26,6 +28,7 @@ const messages = {
2628 missingArrayKey : 'Missing "key" prop for element in array' ,
2729 missingArrayKeyUsePrag : 'Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead' ,
2830 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' ,
2932} ;
3033
3134module . exports = {
@@ -50,6 +53,10 @@ module.exports = {
5053 type : 'boolean' ,
5154 default : defaultOptions . checkKeyMustBeforeSpread ,
5255 } ,
56+ warnOnDuplicates : {
57+ type : 'boolean' ,
58+ default : defaultOptions . warnOnDuplicates ,
59+ } ,
5360 } ,
5461 additionalProperties : false ,
5562 } ] ,
@@ -59,6 +66,7 @@ module.exports = {
5966 const options = Object . assign ( { } , defaultOptions , context . options [ 0 ] ) ;
6067 const checkFragmentShorthand = options . checkFragmentShorthand ;
6168 const checkKeyMustBeforeSpread = options . checkKeyMustBeforeSpread ;
69+ const warnOnDuplicates = options . warnOnDuplicates ;
6270 const reactPragma = pragmaUtil . getFromContext ( context ) ;
6371 const fragmentPragma = pragmaUtil . getFragmentFromContext ( context ) ;
6472
@@ -97,19 +105,43 @@ module.exports = {
97105 }
98106
99107 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 ) {
107111 return ;
108112 }
109113
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+ } ) ;
113145 } ) ;
114146 }
115147 } ,
0 commit comments