Skip to content

Commit 446c5a3

Browse files
duhamelgmljharb
authored andcommitted
[New] jsx-sort-props: support multiline prop groups
Fixes #3170.
1 parent cfb4d6b commit 446c5a3

File tree

4 files changed

+437
-20
lines changed

4 files changed

+437
-20
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1111
* [`jsx-curly-brace-presence`]: add "propElementValues" config option ([#3191][] @ljharb)
1212
* add [`iframe-missing-sandbox`] rule ([#2753][] @tosmolka @ljharb)
1313
* [`no-did-mount-set-state`], [`no-did-update-set-state`]: no-op with react >= 16.3 ([#1754][] @ljharb)
14+
* [`jsx-sort-props`]: support multiline prop groups ([#3198][] @duhamelgm)
1415

1516
### Fixed
1617
* [`prop-types`], `propTypes`: add support for exported type inference ([#3163][] @vedadeepta)
@@ -31,6 +32,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
3132
* [Docs] [`forbid-foreign-prop-types`]: document `allowInPropTypes` option ([#1815][] @ljharb)
3233
* [Refactor] [`jsx-sort-default-props`]: remove unnecessary code ([#1817][] @ljharb)
3334

35+
[#3198]: https://github.com/yannickcr/eslint-plugin-react/pull/3198
3436
[#3195]: https://github.com/yannickcr/eslint-plugin-react/pull/3195
3537
[#3191]: https://github.com/yannickcr/eslint-plugin-react/pull/3191
3638
[#3190]: https://github.com/yannickcr/eslint-plugin-react/pull/3190

docs/rules/jsx-sort-props.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Examples of **correct** code for this rule:
2929
"callbacksLast": <boolean>,
3030
"shorthandFirst": <boolean>,
3131
"shorthandLast": <boolean>,
32+
"multiline": "ignore" | "first" | "last",
3233
"ignoreCase": <boolean>,
3334
"noSortAlphabetically": <boolean>,
3435
"reservedFirst": <boolean>|<array<string>>,
@@ -70,6 +71,42 @@ When `true`, short hand props must be listed after all other props (unless `call
7071
<Hello name="John" tel={5555555} active validate />
7172
```
7273

74+
### `multiline`
75+
76+
Enforced sorting for multiline props
77+
78+
* `ignore`: Multiline props will not be taken in consideration for sorting.
79+
80+
* `first`: Multiline props must be listed before all other props (unless `shorthandFirst` is set), but still respecting the alphabetical order.
81+
82+
* `last`: Multiline props must be listed after all other props (unless either `callbacksLast` or `shorthandLast` are set), but still respecting the alphabetical order.
83+
84+
Defaults to `ignore`.
85+
86+
```jsx
87+
// 'jsx-sort-props': [1, { multiline: 'first' }]
88+
<Hello
89+
classes={{
90+
greetings: classes.greetings,
91+
}}
92+
active
93+
validate
94+
name="John"
95+
tel={5555555}
96+
/>
97+
98+
// 'jsx-sort-props': [1, { multiline: 'last' }]
99+
<Hello
100+
active
101+
validate
102+
name="John"
103+
tel={5555555}
104+
classes={{
105+
greetings: classes.greetings,
106+
}}
107+
/>
108+
```
109+
73110
### `noSortAlphabetically`
74111

75112
When `true`, alphabetical order is **not** enforced:

lib/rules/jsx-sort-props.js

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ function isCallbackPropName(name) {
1818
return /^on[A-Z]/.test(name);
1919
}
2020

21+
function isMultilineProp(node) {
22+
return node.loc.start.line !== node.loc.end.line;
23+
}
24+
2125
const messages = {
2226
noUnreservedProps: 'A customized reserved first list must only contain a subset of React reserved props. Remove: {{unreservedWords}}',
2327
listIsEmpty: 'A customized reserved first list must not be empty',
2428
listReservedPropsFirst: 'Reserved props must be listed before all other props',
2529
listCallbacksLast: 'Callbacks must be listed after all other props',
2630
listShorthandFirst: 'Shorthand props must be listed before all other props',
2731
listShorthandLast: 'Shorthand props must be listed after all other props',
32+
listMultilineFirst: 'Multiline props must be listed before all other props',
33+
listMultilineLast: 'Multiline props must be listed after all other props',
2834
sortPropsByAlpha: 'Props should be sorted alphabetically',
2935
};
3036

@@ -75,6 +81,18 @@ function contextCompare(a, b, options) {
7581
}
7682
}
7783

84+
if (options.multiline !== 'ignore') {
85+
const multilineSign = options.multiline === 'first' ? -1 : 1;
86+
const aIsMultiline = isMultilineProp(a);
87+
const bIsMultiline = isMultilineProp(b);
88+
if (aIsMultiline && !bIsMultiline) {
89+
return multilineSign;
90+
}
91+
if (!aIsMultiline && bIsMultiline) {
92+
return -multilineSign;
93+
}
94+
}
95+
7896
if (options.noSortAlphabetically) {
7997
return 0;
8098
}
@@ -127,6 +145,7 @@ const generateFixerFunction = (node, context, reservedList) => {
127145
const callbacksLast = configuration.callbacksLast || false;
128146
const shorthandFirst = configuration.shorthandFirst || false;
129147
const shorthandLast = configuration.shorthandLast || false;
148+
const multiline = configuration.multiline || 'ignore';
130149
const noSortAlphabetically = configuration.noSortAlphabetically || false;
131150
const reservedFirst = configuration.reservedFirst || false;
132151

@@ -138,6 +157,7 @@ const generateFixerFunction = (node, context, reservedList) => {
138157
callbacksLast,
139158
shorthandFirst,
140159
shorthandLast,
160+
multiline,
141161
noSortAlphabetically,
142162
reservedFirst,
143163
reservedList,
@@ -213,6 +233,34 @@ function validateReservedFirstConfig(context, reservedFirst) {
213233
}
214234
}
215235

236+
const reportedNodeAttributes = new WeakMap();
237+
/**
238+
* Check if the current node attribute has already been reported with the same error type
239+
* if that's the case then we don't report a new error
240+
* otherwise we report the error
241+
* @param {Object} nodeAttribute The node attribute to be reported
242+
* @param {string} errorType The error type to be reported
243+
* @param {Object} node The parent node for the node attribute
244+
* @param {Object} context The context of the rule
245+
* @param {Array<String>} reservedList The list of reserved props
246+
*/
247+
function reportNodeAttribute(nodeAttribute, errorType, node, context, reservedList) {
248+
const errors = reportedNodeAttributes.get(nodeAttribute) || [];
249+
250+
if (errors.includes(errorType)) {
251+
return;
252+
}
253+
254+
errors.push(errorType);
255+
256+
reportedNodeAttributes.set(nodeAttribute, errors);
257+
258+
report(context, messages[errorType], errorType, {
259+
node: nodeAttribute.name,
260+
fix: generateFixerFunction(node, context, reservedList),
261+
});
262+
}
263+
216264
module.exports = {
217265
meta: {
218266
docs: {
@@ -241,6 +289,11 @@ module.exports = {
241289
shorthandLast: {
242290
type: 'boolean',
243291
},
292+
// Whether multiline properties should be listed first or last
293+
multiline: {
294+
enum: ['ignore', 'first', 'last'],
295+
default: 'ignore',
296+
},
244297
ignoreCase: {
245298
type: 'boolean',
246299
},
@@ -262,6 +315,7 @@ module.exports = {
262315
const callbacksLast = configuration.callbacksLast || false;
263316
const shorthandFirst = configuration.shorthandFirst || false;
264317
const shorthandLast = configuration.shorthandLast || false;
318+
const multiline = configuration.multiline || 'ignore';
265319
const noSortAlphabetically = configuration.noSortAlphabetically || false;
266320
const reservedFirst = configuration.reservedFirst || false;
267321
const reservedFirstError = validateReservedFirstConfig(context, reservedFirst);
@@ -285,6 +339,8 @@ module.exports = {
285339
const currentValue = decl.value;
286340
const previousIsCallback = isCallbackPropName(previousPropName);
287341
const currentIsCallback = isCallbackPropName(currentPropName);
342+
const previousIsMultiline = isMultilineProp(memo);
343+
const currentIsMultiline = isMultilineProp(decl);
288344

289345
if (ignoreCase) {
290346
previousPropName = previousPropName.toLowerCase();
@@ -304,10 +360,8 @@ module.exports = {
304360
return decl;
305361
}
306362
if (!previousIsReserved && currentIsReserved) {
307-
report(context, messages.listReservedPropsFirst, 'listReservedPropsFirst', {
308-
node: decl.name,
309-
fix: generateFixerFunction(node, context, reservedList),
310-
});
363+
reportNodeAttribute(decl, 'listReservedPropsFirst', node, context, reservedList);
364+
311365
return memo;
312366
}
313367
}
@@ -319,10 +373,8 @@ module.exports = {
319373
}
320374
if (previousIsCallback && !currentIsCallback) {
321375
// Encountered a non-callback prop after a callback prop
322-
report(context, messages.listCallbacksLast, 'listCallbacksLast', {
323-
node: memo.name,
324-
fix: generateFixerFunction(node, context, reservedList),
325-
});
376+
reportNodeAttribute(memo, 'listCallbacksLast', node, context, reservedList);
377+
326378
return memo;
327379
}
328380
}
@@ -332,10 +384,8 @@ module.exports = {
332384
return decl;
333385
}
334386
if (!currentValue && previousValue) {
335-
report(context, messages.listShorthandFirst, 'listShorthandFirst', {
336-
node: memo.name,
337-
fix: generateFixerFunction(node, context, reservedList),
338-
});
387+
reportNodeAttribute(decl, 'listShorthandFirst', node, context, reservedList);
388+
339389
return memo;
340390
}
341391
}
@@ -345,10 +395,34 @@ module.exports = {
345395
return decl;
346396
}
347397
if (currentValue && !previousValue) {
348-
report(context, messages.listShorthandLast, 'listShorthandLast', {
349-
node: memo.name,
350-
fix: generateFixerFunction(node, context, reservedList),
351-
});
398+
reportNodeAttribute(memo, 'listShorthandLast', node, context, reservedList);
399+
400+
return memo;
401+
}
402+
}
403+
404+
if (multiline === 'first') {
405+
if (previousIsMultiline && !currentIsMultiline) {
406+
// Exiting the multiline prop section
407+
return decl;
408+
}
409+
if (!previousIsMultiline && currentIsMultiline) {
410+
// Encountered a non-multiline prop before a multiline prop
411+
reportNodeAttribute(decl, 'listMultilineFirst', node, context, reservedList);
412+
413+
return memo;
414+
}
415+
}
416+
417+
if (multiline === 'last') {
418+
if (!previousIsMultiline && currentIsMultiline) {
419+
// Entering the multiline prop section
420+
return decl;
421+
}
422+
if (previousIsMultiline && !currentIsMultiline) {
423+
// Encountered a non-multiline prop after a multiline prop
424+
reportNodeAttribute(memo, 'listMultilineLast', node, context, reservedList);
425+
352426
return memo;
353427
}
354428
}
@@ -361,10 +435,8 @@ module.exports = {
361435
: previousPropName > currentPropName
362436
)
363437
) {
364-
report(context, messages.sortPropsByAlpha, 'sortPropsByAlpha', {
365-
node: decl.name,
366-
fix: generateFixerFunction(node, context, reservedList),
367-
});
438+
reportNodeAttribute(decl, 'sortPropsByAlpha', node, context, reservedList);
439+
368440
return memo;
369441
}
370442

0 commit comments

Comments
 (0)