UNPKG

5.59 kBJavaScriptView Raw
1/**
2 * @fileoverview Prevent usage of Array index in keys
3 * @author Joe Lencioni
4 */
5
6'use strict';
7
8const has = require('has');
9const astUtil = require('../util/ast');
10const docsUrl = require('../util/docsUrl');
11const pragma = require('../util/pragma');
12
13// ------------------------------------------------------------------------------
14// Rule Definition
15// ------------------------------------------------------------------------------
16
17module.exports = {
18 meta: {
19 docs: {
20 description: 'Prevent usage of Array index in keys',
21 category: 'Best Practices',
22 recommended: false,
23 url: docsUrl('no-array-index-key')
24 },
25
26 schema: []
27 },
28
29 create(context) {
30 // --------------------------------------------------------------------------
31 // Public
32 // --------------------------------------------------------------------------
33 const indexParamNames = [];
34 const iteratorFunctionsToIndexParamPosition = {
35 every: 1,
36 filter: 1,
37 find: 1,
38 findIndex: 1,
39 forEach: 1,
40 map: 1,
41 reduce: 2,
42 reduceRight: 2,
43 some: 1
44 };
45 const ERROR_MESSAGE = 'Do not use Array index in keys';
46
47 function isArrayIndex(node) {
48 return node.type === 'Identifier'
49 && indexParamNames.indexOf(node.name) !== -1;
50 }
51
52 function isUsingReactChildren(node) {
53 const callee = node.callee;
54 if (
55 !callee
56 || !callee.property
57 || !callee.object
58 ) {
59 return null;
60 }
61
62 const isReactChildMethod = ['map', 'forEach'].indexOf(callee.property.name) > -1;
63 if (!isReactChildMethod) {
64 return null;
65 }
66
67 const obj = callee.object;
68 if (obj && obj.name === 'Children') {
69 return true;
70 }
71 if (obj && obj.object && obj.object.name === pragma.getFromContext(context)) {
72 return true;
73 }
74
75 return false;
76 }
77
78 function getMapIndexParamName(node) {
79 const callee = node.callee;
80 if (callee.type !== 'MemberExpression') {
81 return null;
82 }
83 if (callee.property.type !== 'Identifier') {
84 return null;
85 }
86 if (!has(iteratorFunctionsToIndexParamPosition, callee.property.name)) {
87 return null;
88 }
89
90 const callbackArg = isUsingReactChildren(node)
91 ? node.arguments[1]
92 : node.arguments[0];
93
94 if (!callbackArg) {
95 return null;
96 }
97
98 if (!astUtil.isFunctionLikeExpression(callbackArg)) {
99 return null;
100 }
101
102 const params = callbackArg.params;
103
104 const indexParamPosition = iteratorFunctionsToIndexParamPosition[callee.property.name];
105 if (params.length < indexParamPosition + 1) {
106 return null;
107 }
108
109 return params[indexParamPosition].name;
110 }
111
112 function getIdentifiersFromBinaryExpression(side) {
113 if (side.type === 'Identifier') {
114 return side;
115 }
116
117 if (side.type === 'BinaryExpression') {
118 // recurse
119 const left = getIdentifiersFromBinaryExpression(side.left);
120 const right = getIdentifiersFromBinaryExpression(side.right);
121 return [].concat(left, right).filter(Boolean);
122 }
123
124 return null;
125 }
126
127 function checkPropValue(node) {
128 if (isArrayIndex(node)) {
129 // key={bar}
130 context.report({
131 node,
132 message: ERROR_MESSAGE
133 });
134 return;
135 }
136
137 if (node.type === 'TemplateLiteral') {
138 // key={`foo-${bar}`}
139 node.expressions.filter(isArrayIndex).forEach(() => {
140 context.report({node, message: ERROR_MESSAGE});
141 });
142
143 return;
144 }
145
146 if (node.type === 'BinaryExpression') {
147 // key={'foo' + bar}
148 const identifiers = getIdentifiersFromBinaryExpression(node);
149
150 identifiers.filter(isArrayIndex).forEach(() => {
151 context.report({node, message: ERROR_MESSAGE});
152 });
153 }
154 }
155
156 return {
157 CallExpression(node) {
158 if (
159 node.callee
160 && node.callee.type === 'MemberExpression'
161 && ['createElement', 'cloneElement'].indexOf(node.callee.property.name) !== -1
162 && node.arguments.length > 1
163 ) {
164 // React.createElement
165 if (!indexParamNames.length) {
166 return;
167 }
168
169 const props = node.arguments[1];
170
171 if (props.type !== 'ObjectExpression') {
172 return;
173 }
174
175 props.properties.forEach((prop) => {
176 if (!prop.key || prop.key.name !== 'key') {
177 // { ...foo }
178 // { foo: bar }
179 return;
180 }
181
182 checkPropValue(prop.value);
183 });
184
185 return;
186 }
187
188 const mapIndexParamName = getMapIndexParamName(node);
189 if (!mapIndexParamName) {
190 return;
191 }
192
193 indexParamNames.push(mapIndexParamName);
194 },
195
196 JSXAttribute(node) {
197 if (node.name.name !== 'key') {
198 // foo={bar}
199 return;
200 }
201
202 if (!indexParamNames.length) {
203 // Not inside a call expression that we think has an index param.
204 return;
205 }
206
207 const value = node.value;
208 if (!value || value.type !== 'JSXExpressionContainer') {
209 // key='foo' or just simply 'key'
210 return;
211 }
212
213 checkPropValue(value.expression);
214 },
215
216 'CallExpression:exit'(node) {
217 const mapIndexParamName = getMapIndexParamName(node);
218 if (!mapIndexParamName) {
219 return;
220 }
221
222 indexParamNames.pop();
223 }
224 };
225 }
226};