1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 |
|
8 | const propName = require('jsx-ast-utils/propName');
|
9 | const docsUrl = require('../util/docsUrl');
|
10 | const jsxUtil = require('../util/jsx');
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | function isCallbackPropName(name) {
|
17 | return /^on[A-Z]/.test(name);
|
18 | }
|
19 |
|
20 | const RESERVED_PROPS_LIST = [
|
21 | 'children',
|
22 | 'dangerouslySetInnerHTML',
|
23 | 'key',
|
24 | 'ref'
|
25 | ];
|
26 |
|
27 | function isReservedPropName(name, list) {
|
28 | return list.indexOf(name) >= 0;
|
29 | }
|
30 |
|
31 | function contextCompare(a, b, options) {
|
32 | let aProp = propName(a);
|
33 | let bProp = propName(b);
|
34 |
|
35 | if (options.reservedFirst) {
|
36 | const aIsReserved = isReservedPropName(aProp, options.reservedList);
|
37 | const bIsReserved = isReservedPropName(bProp, options.reservedList);
|
38 | if (aIsReserved && !bIsReserved) {
|
39 | return -1;
|
40 | }
|
41 | if (!aIsReserved && bIsReserved) {
|
42 | return 1;
|
43 | }
|
44 | }
|
45 |
|
46 | if (options.callbacksLast) {
|
47 | const aIsCallback = isCallbackPropName(aProp);
|
48 | const bIsCallback = isCallbackPropName(bProp);
|
49 | if (aIsCallback && !bIsCallback) {
|
50 | return 1;
|
51 | }
|
52 | if (!aIsCallback && bIsCallback) {
|
53 | return -1;
|
54 | }
|
55 | }
|
56 |
|
57 | if (options.shorthandFirst || options.shorthandLast) {
|
58 | const shorthandSign = options.shorthandFirst ? -1 : 1;
|
59 | if (!a.value && b.value) {
|
60 | return shorthandSign;
|
61 | }
|
62 | if (a.value && !b.value) {
|
63 | return -shorthandSign;
|
64 | }
|
65 | }
|
66 |
|
67 | if (options.noSortAlphabetically) {
|
68 | return 0;
|
69 | }
|
70 |
|
71 | if (options.ignoreCase) {
|
72 | aProp = aProp.toLowerCase();
|
73 | bProp = bProp.toLowerCase();
|
74 | return aProp.localeCompare(bProp);
|
75 | }
|
76 | if (aProp === bProp) {
|
77 | return 0;
|
78 | }
|
79 | return aProp < bProp ? -1 : 1;
|
80 | }
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 | function getGroupsOfSortableAttributes(attributes) {
|
89 | const sortableAttributeGroups = [];
|
90 | let groupCount = 0;
|
91 | for (let i = 0; i < attributes.length; i++) {
|
92 | const lastAttr = attributes[i - 1];
|
93 |
|
94 |
|
95 |
|
96 | if (
|
97 | !lastAttr
|
98 | || (lastAttr.type === 'JSXSpreadAttribute'
|
99 | && attributes[i].type !== 'JSXSpreadAttribute')
|
100 | ) {
|
101 | groupCount++;
|
102 | sortableAttributeGroups[groupCount - 1] = [];
|
103 | }
|
104 | if (attributes[i].type !== 'JSXSpreadAttribute') {
|
105 | sortableAttributeGroups[groupCount - 1].push(attributes[i]);
|
106 | }
|
107 | }
|
108 | return sortableAttributeGroups;
|
109 | }
|
110 |
|
111 | const generateFixerFunction = (node, context, reservedList) => {
|
112 | const sourceCode = context.getSourceCode();
|
113 | const attributes = node.attributes.slice(0);
|
114 | const configuration = context.options[0] || {};
|
115 | const ignoreCase = configuration.ignoreCase || false;
|
116 | const callbacksLast = configuration.callbacksLast || false;
|
117 | const shorthandFirst = configuration.shorthandFirst || false;
|
118 | const shorthandLast = configuration.shorthandLast || false;
|
119 | const noSortAlphabetically = configuration.noSortAlphabetically || false;
|
120 | const reservedFirst = configuration.reservedFirst || false;
|
121 |
|
122 |
|
123 |
|
124 |
|
125 | const options = {
|
126 | ignoreCase,
|
127 | callbacksLast,
|
128 | shorthandFirst,
|
129 | shorthandLast,
|
130 | noSortAlphabetically,
|
131 | reservedFirst,
|
132 | reservedList
|
133 | };
|
134 | const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes);
|
135 | const sortedAttributeGroups = sortableAttributeGroups
|
136 | .slice(0)
|
137 | .map((group) => group.slice(0).sort((a, b) => contextCompare(a, b, options)));
|
138 |
|
139 | return function fixFunction(fixer) {
|
140 | const fixers = [];
|
141 | let source = sourceCode.getText();
|
142 |
|
143 |
|
144 | sortableAttributeGroups.forEach((sortableGroup, ii) => {
|
145 | sortableGroup.forEach((attr, jj) => {
|
146 | const sortedAttr = sortedAttributeGroups[ii][jj];
|
147 | const sortedAttrText = sourceCode.getText(sortedAttr);
|
148 | fixers.push({
|
149 | range: [attr.range[0], attr.range[1]],
|
150 | text: sortedAttrText
|
151 | });
|
152 | });
|
153 | });
|
154 |
|
155 | fixers.sort((a, b) => b.range[0] - a.range[0]);
|
156 |
|
157 | const rangeStart = fixers[fixers.length - 1].range[0];
|
158 | const rangeEnd = fixers[0].range[1];
|
159 |
|
160 | fixers.forEach((fix) => {
|
161 | source = `${source.substr(0, fix.range[0])}${fix.text}${source.substr(fix.range[1])}`;
|
162 | });
|
163 |
|
164 | return fixer.replaceTextRange([rangeStart, rangeEnd], source.substr(rangeStart, rangeEnd - rangeStart));
|
165 | };
|
166 | };
|
167 |
|
168 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 | function validateReservedFirstConfig(context, reservedFirst) {
|
176 | if (reservedFirst) {
|
177 | if (Array.isArray(reservedFirst)) {
|
178 |
|
179 | const nonReservedWords = reservedFirst.filter((word) => !isReservedPropName(
|
180 | word,
|
181 | RESERVED_PROPS_LIST
|
182 | ));
|
183 |
|
184 | if (reservedFirst.length === 0) {
|
185 | return function report(decl) {
|
186 | context.report({
|
187 | node: decl,
|
188 | message: 'A customized reserved first list must not be empty'
|
189 | });
|
190 | };
|
191 | }
|
192 | if (nonReservedWords.length > 0) {
|
193 | return function report(decl) {
|
194 | context.report({
|
195 | node: decl,
|
196 | message: 'A customized reserved first list must only contain a subset of React reserved props.'
|
197 | + ' Remove: {{ nonReservedWords }}',
|
198 | data: {
|
199 | nonReservedWords: nonReservedWords.toString()
|
200 | }
|
201 | });
|
202 | };
|
203 | }
|
204 | }
|
205 | }
|
206 | }
|
207 |
|
208 | module.exports = {
|
209 | meta: {
|
210 | docs: {
|
211 | description: 'Enforce props alphabetical sorting',
|
212 | category: 'Stylistic Issues',
|
213 | recommended: false,
|
214 | url: docsUrl('jsx-sort-props')
|
215 | },
|
216 | fixable: 'code',
|
217 | schema: [{
|
218 | type: 'object',
|
219 | properties: {
|
220 |
|
221 |
|
222 | callbacksLast: {
|
223 | type: 'boolean'
|
224 | },
|
225 |
|
226 | shorthandFirst: {
|
227 | type: 'boolean'
|
228 | },
|
229 |
|
230 | shorthandLast: {
|
231 | type: 'boolean'
|
232 | },
|
233 | ignoreCase: {
|
234 | type: 'boolean'
|
235 | },
|
236 |
|
237 | noSortAlphabetically: {
|
238 | type: 'boolean'
|
239 | },
|
240 | reservedFirst: {
|
241 | type: ['array', 'boolean']
|
242 | }
|
243 | },
|
244 | additionalProperties: false
|
245 | }]
|
246 | },
|
247 |
|
248 | create(context) {
|
249 | const configuration = context.options[0] || {};
|
250 | const ignoreCase = configuration.ignoreCase || false;
|
251 | const callbacksLast = configuration.callbacksLast || false;
|
252 | const shorthandFirst = configuration.shorthandFirst || false;
|
253 | const shorthandLast = configuration.shorthandLast || false;
|
254 | const noSortAlphabetically = configuration.noSortAlphabetically || false;
|
255 | const reservedFirst = configuration.reservedFirst || false;
|
256 | const reservedFirstError = validateReservedFirstConfig(context, reservedFirst);
|
257 | let reservedList = Array.isArray(reservedFirst) ? reservedFirst : RESERVED_PROPS_LIST;
|
258 |
|
259 | return {
|
260 | JSXOpeningElement(node) {
|
261 |
|
262 | if (reservedFirst && !jsxUtil.isDOMComponent(node)) {
|
263 | reservedList = reservedList.filter((prop) => prop !== 'dangerouslySetInnerHTML');
|
264 | }
|
265 |
|
266 | node.attributes.reduce((memo, decl, idx, attrs) => {
|
267 | if (decl.type === 'JSXSpreadAttribute') {
|
268 | return attrs[idx + 1];
|
269 | }
|
270 |
|
271 | let previousPropName = propName(memo);
|
272 | let currentPropName = propName(decl);
|
273 | const previousValue = memo.value;
|
274 | const currentValue = decl.value;
|
275 | const previousIsCallback = isCallbackPropName(previousPropName);
|
276 | const currentIsCallback = isCallbackPropName(currentPropName);
|
277 |
|
278 | if (ignoreCase) {
|
279 | previousPropName = previousPropName.toLowerCase();
|
280 | currentPropName = currentPropName.toLowerCase();
|
281 | }
|
282 |
|
283 | if (reservedFirst) {
|
284 | if (reservedFirstError) {
|
285 | reservedFirstError(decl);
|
286 | return memo;
|
287 | }
|
288 |
|
289 | const previousIsReserved = isReservedPropName(previousPropName, reservedList);
|
290 | const currentIsReserved = isReservedPropName(currentPropName, reservedList);
|
291 |
|
292 | if (previousIsReserved && !currentIsReserved) {
|
293 | return decl;
|
294 | }
|
295 | if (!previousIsReserved && currentIsReserved) {
|
296 | context.report({
|
297 | node: decl.name,
|
298 | message: 'Reserved props must be listed before all other props',
|
299 | fix: generateFixerFunction(node, context, reservedList)
|
300 | });
|
301 | return memo;
|
302 | }
|
303 | }
|
304 |
|
305 | if (callbacksLast) {
|
306 | if (!previousIsCallback && currentIsCallback) {
|
307 |
|
308 | return decl;
|
309 | }
|
310 | if (previousIsCallback && !currentIsCallback) {
|
311 |
|
312 | context.report({
|
313 | node: memo.name,
|
314 | message: 'Callbacks must be listed after all other props',
|
315 | fix: generateFixerFunction(node, context, reservedList)
|
316 | });
|
317 | return memo;
|
318 | }
|
319 | }
|
320 |
|
321 | if (shorthandFirst) {
|
322 | if (currentValue && !previousValue) {
|
323 | return decl;
|
324 | }
|
325 | if (!currentValue && previousValue) {
|
326 | context.report({
|
327 | node: memo.name,
|
328 | message: 'Shorthand props must be listed before all other props',
|
329 | fix: generateFixerFunction(node, context, reservedList)
|
330 | });
|
331 | return memo;
|
332 | }
|
333 | }
|
334 |
|
335 | if (shorthandLast) {
|
336 | if (!currentValue && previousValue) {
|
337 | return decl;
|
338 | }
|
339 | if (currentValue && !previousValue) {
|
340 | context.report({
|
341 | node: memo.name,
|
342 | message: 'Shorthand props must be listed after all other props',
|
343 | fix: generateFixerFunction(node, context, reservedList)
|
344 | });
|
345 | return memo;
|
346 | }
|
347 | }
|
348 |
|
349 | if (
|
350 | !noSortAlphabetically
|
351 | && (
|
352 | ignoreCase
|
353 | ? previousPropName.localeCompare(currentPropName) > 0
|
354 | : previousPropName > currentPropName
|
355 | )
|
356 | ) {
|
357 | context.report({
|
358 | node: decl.name,
|
359 | message: 'Props should be sorted alphabetically',
|
360 | fix: generateFixerFunction(node, context, reservedList)
|
361 | });
|
362 | return memo;
|
363 | }
|
364 |
|
365 | return decl;
|
366 | }, node.attributes[0]);
|
367 | }
|
368 | };
|
369 | }
|
370 | };
|