source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/jsx-key.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 9.9 KB
RevLine 
[d565449]1/**
2 * @fileoverview Report missing `key` props in iterators/collection literals.
3 * @author Ben Mosher
4 */
5
6'use strict';
7
8const hasProp = require('jsx-ast-utils/hasProp');
9const propName = require('jsx-ast-utils/propName');
10const values = require('object.values');
11const docsUrl = require('../util/docsUrl');
12const pragmaUtil = require('../util/pragma');
13const report = require('../util/report');
14const astUtil = require('../util/ast');
15const getText = require('../util/eslint').getText;
16
17// ------------------------------------------------------------------------------
18// Rule Definition
19// ------------------------------------------------------------------------------
20
21const defaultOptions = {
22 checkFragmentShorthand: false,
23 checkKeyMustBeforeSpread: false,
24 warnOnDuplicates: false,
25};
26
27const messages = {
28 missingIterKey: 'Missing "key" prop for element in iterator',
29 missingIterKeyUsePrag: 'Missing "key" prop for element in iterator. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead',
30 missingArrayKey: 'Missing "key" prop for element in array',
31 missingArrayKeyUsePrag: 'Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead',
32 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`',
33 nonUniqueKeys: '`key` prop must be unique',
34};
35
36/** @type {import('eslint').Rule.RuleModule} */
37module.exports = {
38 meta: {
39 docs: {
40 description: 'Disallow missing `key` props in iterators/collection literals',
41 category: 'Possible Errors',
42 recommended: true,
43 url: docsUrl('jsx-key'),
44 },
45
46 messages,
47
48 schema: [{
49 type: 'object',
50 properties: {
51 checkFragmentShorthand: {
52 type: 'boolean',
53 default: defaultOptions.checkFragmentShorthand,
54 },
55 checkKeyMustBeforeSpread: {
56 type: 'boolean',
57 default: defaultOptions.checkKeyMustBeforeSpread,
58 },
59 warnOnDuplicates: {
60 type: 'boolean',
61 default: defaultOptions.warnOnDuplicates,
62 },
63 },
64 additionalProperties: false,
65 }],
66 },
67
68 create(context) {
69 const options = Object.assign({}, defaultOptions, context.options[0]);
70 const checkFragmentShorthand = options.checkFragmentShorthand;
71 const checkKeyMustBeforeSpread = options.checkKeyMustBeforeSpread;
72 const warnOnDuplicates = options.warnOnDuplicates;
73 const reactPragma = pragmaUtil.getFromContext(context);
74 const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
75
76 function isKeyAfterSpread(attributes) {
77 let hasFoundSpread = false;
78 return attributes.some((attribute) => {
79 if (attribute.type === 'JSXSpreadAttribute') {
80 hasFoundSpread = true;
81 return false;
82 }
83 if (attribute.type !== 'JSXAttribute') {
84 return false;
85 }
86 return hasFoundSpread && propName(attribute) === 'key';
87 });
88 }
89
90 function checkIteratorElement(node) {
91 if (node.type === 'JSXElement') {
92 if (!hasProp(node.openingElement.attributes, 'key')) {
93 report(context, messages.missingIterKey, 'missingIterKey', { node });
94 } else {
95 const attrs = node.openingElement.attributes;
96
97 if (checkKeyMustBeforeSpread && isKeyAfterSpread(attrs)) {
98 report(context, messages.keyBeforeSpread, 'keyBeforeSpread', { node });
99 }
100 }
101 } else if (checkFragmentShorthand && node.type === 'JSXFragment') {
102 report(context, messages.missingIterKeyUsePrag, 'missingIterKeyUsePrag', {
103 node,
104 data: {
105 reactPrag: reactPragma,
106 fragPrag: fragmentPragma,
107 },
108 });
109 }
110 }
111
112 function getReturnStatements(node) {
113 const returnStatements = arguments[1] || [];
114 if (node.type === 'IfStatement') {
115 if (node.consequent) {
116 getReturnStatements(node.consequent, returnStatements);
117 }
118 if (node.alternate) {
119 getReturnStatements(node.alternate, returnStatements);
120 }
121 } else if (node.type === 'ReturnStatement') {
122 returnStatements.push(node);
123 } else if (Array.isArray(node.body)) {
124 node.body.forEach((item) => {
125 if (item.type === 'IfStatement') {
126 getReturnStatements(item, returnStatements);
127 }
128
129 if (item.type === 'ReturnStatement') {
130 returnStatements.push(item);
131 }
132 });
133 }
134
135 return returnStatements;
136 }
137
138 /**
139 * Checks if the given node is a function expression or arrow function,
140 * and checks if there is a missing key prop in return statement's arguments
141 * @param {ASTNode} node
142 */
143 function checkFunctionsBlockStatement(node) {
144 if (astUtil.isFunctionLikeExpression(node)) {
145 if (node.body.type === 'BlockStatement') {
146 getReturnStatements(node.body)
147 .filter((returnStatement) => returnStatement && returnStatement.argument)
148 .forEach((returnStatement) => {
149 checkIteratorElement(returnStatement.argument);
150 });
151 }
152 }
153 }
154
155 /**
156 * Checks if the given node is an arrow function that has an JSX Element or JSX Fragment in its body,
157 * and the JSX is missing a key prop
158 * @param {ASTNode} node
159 */
160 function checkArrowFunctionWithJSX(node) {
161 const isArrFn = node && node.type === 'ArrowFunctionExpression';
162 const shouldCheckNode = (n) => n && (n.type === 'JSXElement' || n.type === 'JSXFragment');
163 if (isArrFn && shouldCheckNode(node.body)) {
164 checkIteratorElement(node.body);
165 }
166 if (node.body.type === 'ConditionalExpression') {
167 if (shouldCheckNode(node.body.consequent)) {
168 checkIteratorElement(node.body.consequent);
169 }
170 if (shouldCheckNode(node.body.alternate)) {
171 checkIteratorElement(node.body.alternate);
172 }
173 } else if (node.body.type === 'LogicalExpression' && shouldCheckNode(node.body.right)) {
174 checkIteratorElement(node.body.right);
175 }
176 }
177
178 const childrenToArraySelector = `:matches(
179 CallExpression
180 [callee.object.object.name=${reactPragma}]
181 [callee.object.property.name=Children]
182 [callee.property.name=toArray],
183 CallExpression
184 [callee.object.name=Children]
185 [callee.property.name=toArray]
186 )`.replace(/\s/g, '');
187 let isWithinChildrenToArray = false;
188
189 const seen = new WeakSet();
190
191 return {
192 [childrenToArraySelector]() {
193 isWithinChildrenToArray = true;
194 },
195
196 [`${childrenToArraySelector}:exit`]() {
197 isWithinChildrenToArray = false;
198 },
199
200 'ArrayExpression, JSXElement > JSXElement'(node) {
201 if (isWithinChildrenToArray) {
202 return;
203 }
204
205 const jsx = (node.type === 'ArrayExpression' ? node.elements : node.parent.children).filter((x) => x && x.type === 'JSXElement');
206 if (jsx.length === 0) {
207 return;
208 }
209
210 const map = {};
211 jsx.forEach((element) => {
212 const attrs = element.openingElement.attributes;
213 const keys = attrs.filter((x) => x.name && x.name.name === 'key');
214
215 if (keys.length === 0) {
216 if (node.type === 'ArrayExpression') {
217 report(context, messages.missingArrayKey, 'missingArrayKey', {
218 node: element,
219 });
220 }
221 } else {
222 keys.forEach((attr) => {
223 const value = getText(context, attr.value);
224 if (!map[value]) { map[value] = []; }
225 map[value].push(attr);
226
227 if (checkKeyMustBeforeSpread && isKeyAfterSpread(attrs)) {
228 report(context, messages.keyBeforeSpread, 'keyBeforeSpread', {
229 node: node.type === 'ArrayExpression' ? node : node.parent,
230 });
231 }
232 });
233 }
234 });
235
236 if (warnOnDuplicates) {
237 values(map).filter((v) => v.length > 1).forEach((v) => {
238 v.forEach((n) => {
239 if (!seen.has(n)) {
240 seen.add(n);
241 report(context, messages.nonUniqueKeys, 'nonUniqueKeys', {
242 node: n,
243 });
244 }
245 });
246 });
247 }
248 },
249
250 JSXFragment(node) {
251 if (!checkFragmentShorthand || isWithinChildrenToArray) {
252 return;
253 }
254
255 if (node.parent.type === 'ArrayExpression') {
256 report(context, messages.missingArrayKeyUsePrag, 'missingArrayKeyUsePrag', {
257 node,
258 data: {
259 reactPrag: reactPragma,
260 fragPrag: fragmentPragma,
261 },
262 });
263 }
264 },
265
266 // Array.prototype.map
267 // eslint-disable-next-line no-multi-str
268 'CallExpression[callee.type="MemberExpression"][callee.property.name="map"],\
269 CallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"],\
270 OptionalCallExpression[callee.type="MemberExpression"][callee.property.name="map"],\
271 OptionalCallExpression[callee.type="OptionalMemberExpression"][callee.property.name="map"]'(node) {
272 if (isWithinChildrenToArray) {
273 return;
274 }
275
276 const fn = node.arguments.length > 0 && node.arguments[0];
277 if (!fn || !astUtil.isFunctionLikeExpression(fn)) {
278 return;
279 }
280
281 checkArrowFunctionWithJSX(fn);
282
283 checkFunctionsBlockStatement(fn);
284 },
285
286 // Array.from
287 'CallExpression[callee.type="MemberExpression"][callee.property.name="from"]'(node) {
288 if (isWithinChildrenToArray) {
289 return;
290 }
291
292 const fn = node.arguments.length > 1 && node.arguments[1];
293 if (!astUtil.isFunctionLikeExpression(fn)) {
294 return;
295 }
296
297 checkArrowFunctionWithJSX(fn);
298
299 checkFunctionsBlockStatement(fn);
300 },
301 };
302 },
303};
Note: See TracBrowser for help on using the repository browser.