source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/no-array-index-key.js@ 0c6b92a

main
Last change on this file since 0c6b92a was 0c6b92a, checked in by stefan toskovski <stefantoska84@…>, 5 weeks ago

Pred finalna verzija

  • Property mode set to 100644
File size: 7.3 KB
RevLine 
[d565449]1/**
2 * @fileoverview Prevent usage of Array index in keys
3 * @author Joe Lencioni
4 */
5
6'use strict';
7
8const has = require('hasown');
9const astUtil = require('../util/ast');
10const docsUrl = require('../util/docsUrl');
11const pragma = require('../util/pragma');
12const report = require('../util/report');
13const variableUtil = require('../util/variable');
14
15// ------------------------------------------------------------------------------
16// Rule Definition
17// ------------------------------------------------------------------------------
18
19function isCreateCloneElement(node, context) {
20 if (!node) {
21 return false;
22 }
23
24 if (node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') {
25 return node.object
26 && node.object.name === pragma.getFromContext(context)
27 && ['createElement', 'cloneElement'].indexOf(node.property.name) !== -1;
28 }
29
30 if (node.type === 'Identifier') {
31 const variable = variableUtil.findVariableByName(context, node, node.name);
32 if (variable && variable.type === 'ImportSpecifier') {
33 return variable.parent.source.value === 'react';
34 }
35 }
36
37 return false;
38}
39
40const messages = {
41 noArrayIndex: 'Do not use Array index in keys',
42};
43
44/** @type {import('eslint').Rule.RuleModule} */
45module.exports = {
46 meta: {
47 docs: {
48 description: 'Disallow usage of Array index in keys',
49 category: 'Best Practices',
50 recommended: false,
51 url: docsUrl('no-array-index-key'),
52 },
53
54 messages,
55
56 schema: [],
57 },
58
59 create(context) {
60 // --------------------------------------------------------------------------
61 // Public
62 // --------------------------------------------------------------------------
63 const indexParamNames = [];
64 const iteratorFunctionsToIndexParamPosition = {
65 every: 1,
66 filter: 1,
67 find: 1,
68 findIndex: 1,
69 flatMap: 1,
70 forEach: 1,
71 map: 1,
72 reduce: 2,
73 reduceRight: 2,
74 some: 1,
75 };
76
77 function isArrayIndex(node) {
78 return node.type === 'Identifier'
79 && indexParamNames.indexOf(node.name) !== -1;
80 }
81
82 function isUsingReactChildren(node) {
83 const callee = node.callee;
84 if (
85 !callee
86 || !callee.property
87 || !callee.object
88 ) {
89 return null;
90 }
91
92 const isReactChildMethod = ['map', 'forEach'].indexOf(callee.property.name) > -1;
93 if (!isReactChildMethod) {
94 return null;
95 }
96
97 const obj = callee.object;
98 if (obj && obj.name === 'Children') {
99 return true;
100 }
101 if (obj && obj.object && obj.object.name === pragma.getFromContext(context)) {
102 return true;
103 }
104
105 return false;
106 }
107
108 function getMapIndexParamName(node) {
109 const callee = node.callee;
110 if (callee.type !== 'MemberExpression' && callee.type !== 'OptionalMemberExpression') {
111 return null;
112 }
113 if (callee.property.type !== 'Identifier') {
114 return null;
115 }
116 if (!has(iteratorFunctionsToIndexParamPosition, callee.property.name)) {
117 return null;
118 }
119
120 const name = /** @type {keyof iteratorFunctionsToIndexParamPosition} */ (callee.property.name);
121
122 const callbackArg = isUsingReactChildren(node)
123 ? node.arguments[1]
124 : node.arguments[0];
125
126 if (!callbackArg) {
127 return null;
128 }
129
130 if (!astUtil.isFunctionLikeExpression(callbackArg)) {
131 return null;
132 }
133
134 const params = callbackArg.params;
135
136 const indexParamPosition = iteratorFunctionsToIndexParamPosition[name];
137 if (params.length < indexParamPosition + 1) {
138 return null;
139 }
140
141 return params[indexParamPosition].name;
142 }
143
144 function getIdentifiersFromBinaryExpression(side) {
145 if (side.type === 'Identifier') {
146 return side;
147 }
148
149 if (side.type === 'BinaryExpression') {
150 // recurse
151 const left = getIdentifiersFromBinaryExpression(side.left);
152 const right = getIdentifiersFromBinaryExpression(side.right);
153 return [].concat(left, right).filter(Boolean);
154 }
155
156 return null;
157 }
158
159 function checkPropValue(node) {
160 if (isArrayIndex(node)) {
161 // key={bar}
162 report(context, messages.noArrayIndex, 'noArrayIndex', {
163 node,
164 });
165 return;
166 }
167
168 if (node.type === 'TemplateLiteral') {
169 // key={`foo-${bar}`}
170 node.expressions.filter(isArrayIndex).forEach(() => {
171 report(context, messages.noArrayIndex, 'noArrayIndex', {
172 node,
173 });
174 });
175
176 return;
177 }
178
179 if (node.type === 'BinaryExpression') {
180 // key={'foo' + bar}
181 const identifiers = getIdentifiersFromBinaryExpression(node);
182
183 identifiers.filter(isArrayIndex).forEach(() => {
184 report(context, messages.noArrayIndex, 'noArrayIndex', {
185 node,
186 });
187 });
188
189 return;
190 }
191
[0c6b92a]192 if (
193 astUtil.isCallExpression(node)
194 && node.callee
195 && node.callee.type === 'MemberExpression'
196 && node.callee.object
197 && isArrayIndex(node.callee.object)
198 && node.callee.property
199 && node.callee.property.type === 'Identifier'
200 && node.callee.property.name === 'toString'
[d565449]201 ) {
202 // key={bar.toString()}
203 report(context, messages.noArrayIndex, 'noArrayIndex', {
204 node,
205 });
206 return;
207 }
208
[0c6b92a]209 if (
210 astUtil.isCallExpression(node)
211 && node.callee
212 && node.callee.type === 'Identifier'
213 && node.callee.name === 'String'
214 && Array.isArray(node.arguments)
215 && node.arguments.length > 0
216 && isArrayIndex(node.arguments[0])
[d565449]217 ) {
218 // key={String(bar)}
219 report(context, messages.noArrayIndex, 'noArrayIndex', {
220 node: node.arguments[0],
221 });
222 }
223 }
224
225 function popIndex(node) {
226 const mapIndexParamName = getMapIndexParamName(node);
227 if (!mapIndexParamName) {
228 return;
229 }
230
231 indexParamNames.pop();
232 }
233
234 return {
235 'CallExpression, OptionalCallExpression'(node) {
236 if (isCreateCloneElement(node.callee, context) && node.arguments.length > 1) {
237 // React.createElement
238 if (!indexParamNames.length) {
239 return;
240 }
241
242 const props = node.arguments[1];
243
244 if (props.type !== 'ObjectExpression') {
245 return;
246 }
247
248 props.properties.forEach((prop) => {
249 if (!prop.key || prop.key.name !== 'key') {
250 // { ...foo }
251 // { foo: bar }
252 return;
253 }
254
255 checkPropValue(prop.value);
256 });
257
258 return;
259 }
260
261 const mapIndexParamName = getMapIndexParamName(node);
262 if (!mapIndexParamName) {
263 return;
264 }
265
266 indexParamNames.push(mapIndexParamName);
267 },
268
269 JSXAttribute(node) {
270 if (node.name.name !== 'key') {
271 // foo={bar}
272 return;
273 }
274
275 if (!indexParamNames.length) {
276 // Not inside a call expression that we think has an index param.
277 return;
278 }
279
280 const value = node.value;
281 if (!value || value.type !== 'JSXExpressionContainer') {
282 // key='foo' or just simply 'key'
283 return;
284 }
285
286 checkPropValue(value.expression);
287 },
288
289 'CallExpression:exit': popIndex,
290 'OptionalCallExpression:exit': popIndex,
291 };
292 },
293};
Note: See TracBrowser for help on using the repository browser.