Skip to content

Commit f47deef

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

File tree

4 files changed

+438
-20
lines changed

4 files changed

+438
-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: 93 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
'use strict';
77

88
const propName = require('jsx-ast-utils/propName');
9+
const includes = require('array-includes');
910
const docsUrl = require('../util/docsUrl');
1011
const jsxUtil = require('../util/jsx');
1112
const report = require('../util/report');
@@ -18,13 +19,19 @@ function isCallbackPropName(name) {
1819
return /^on[A-Z]/.test(name);
1920
}
2021

22+
function isMultilineProp(node) {
23+
return node.loc.start.line !== node.loc.end.line;
24+
}
25+
2126
const messages = {
2227
noUnreservedProps: 'A customized reserved first list must only contain a subset of React reserved props. Remove: {{unreservedWords}}',
2328
listIsEmpty: 'A customized reserved first list must not be empty',
2429
listReservedPropsFirst: 'Reserved props must be listed before all other props',
2530
listCallbacksLast: 'Callbacks must be listed after all other props',
2631
listShorthandFirst: 'Shorthand props must be listed before all other props',
2732
listShorthandLast: 'Shorthand props must be listed after all other props',
33+
listMultilineFirst: 'Multiline props must be listed before all other props',
34+
listMultilineLast: 'Multiline props must be listed after all other props',
2835
sortPropsByAlpha: 'Props should be sorted alphabetically',
2936
};
3037

@@ -75,6 +82,18 @@ function contextCompare(a, b, options) {
7582
}
7683
}
7784

85+
if (options.multiline !== 'ignore') {
86+
const multilineSign = options.multiline === 'first' ? -1 : 1;
87+
const aIsMultiline = isMultilineProp(a);
88+
const bIsMultiline = isMultilineProp(b);
89+
if (aIsMultiline && !bIsMultiline) {
90+
return multilineSign;
91+
}
92+
if (!aIsMultiline && bIsMultiline) {
93+
return -multilineSign;
94+
}
95+
}
96+
7897
if (options.noSortAlphabetically) {
7998
return 0;
8099
}
@@ -127,6 +146,7 @@ const generateFixerFunction = (node, context, reservedList) => {
127146
const callbacksLast = configuration.callbacksLast || false;
128147
const shorthandFirst = configuration.shorthandFirst || false;
129148
const shorthandLast = configuration.shorthandLast || false;
149+
const multiline = configuration.multiline || 'ignore';
130150
const noSortAlphabetically = configuration.noSortAlphabetically || false;
131151
const reservedFirst = configuration.reservedFirst || false;
132152

@@ -138,6 +158,7 @@ const generateFixerFunction = (node, context, reservedList) => {
138158
callbacksLast,
139159
shorthandFirst,
140160
shorthandLast,
161+
multiline,
141162
noSortAlphabetically,
142163
reservedFirst,
143164
reservedList,
@@ -213,6 +234,34 @@ function validateReservedFirstConfig(context, reservedFirst) {
213234
}
214235
}
215236

237+
const reportedNodeAttributes = new WeakMap();
238+
/**
239+
* Check if the current node attribute has already been reported with the same error type
240+
* if that's the case then we don't report a new error
241+
* otherwise we report the error
242+
* @param {Object} nodeAttribute The node attribute to be reported
243+
* @param {string} errorType The error type to be reported
244+
* @param {Object} node The parent node for the node attribute
245+
* @param {Object} context The context of the rule
246+
* @param {Array<String>} reservedList The list of reserved props
247+
*/
248+
function reportNodeAttribute(nodeAttribute, errorType, node, context, reservedList) {
249+
const errors = reportedNodeAttributes.get(nodeAttribute) || [];
250+
251+
if (includes(errors, errorType)) {
252+
return;
253+
}
254+
255+
errors.push(errorType);
256+
257+
reportedNodeAttributes.set(nodeAttribute, errors);
258+
259+
report(context, messages[errorType], errorType, {
260+
node: nodeAttribute.name,
261+
fix: generateFixerFunction(node, context, reservedList),
262+
});
263+
}
264+
216265
module.exports = {
217266
meta: {
218267
docs: {
@@ -241,6 +290,11 @@ module.exports = {
241290
shorthandLast: {
242291
type: 'boolean',
243292
},
293+
// Whether multiline properties should be listed first or last
294+
multiline: {
295+
enum: ['ignore', 'first', 'last'],
296+
default: 'ignore',
297+
},
244298
ignoreCase: {
245299
type: 'boolean',
246300
},
@@ -262,6 +316,7 @@ module.exports = {
262316
const callbacksLast = configuration.callbacksLast || false;
263317
const shorthandFirst = configuration.shorthandFirst || false;
264318
const shorthandLast = configuration.shorthandLast || false;
319+
const multiline = configuration.multiline || 'ignore';
265320
const noSortAlphabetically = configuration.noSortAlphabetically || false;
266321
const reservedFirst = configuration.reservedFirst || false;
267322
const reservedFirstError = validateReservedFirstConfig(context, reservedFirst);
@@ -285,6 +340,8 @@ module.exports = {
285340
const currentValue = decl.value;
286341
const previousIsCallback = isCallbackPropName(previousPropName);
287342
const currentIsCallback = isCallbackPropName(currentPropName);
343+
const previousIsMultiline = isMultilineProp(memo);
344+
const currentIsMultiline = isMultilineProp(decl);
288345

289346
if (ignoreCase) {
290347
previousPropName = previousPropName.toLowerCase();
@@ -304,10 +361,8 @@ module.exports = {
304361
return decl;
305362
}
306363
if (!previousIsReserved && currentIsReserved) {
307-
report(context, messages.listReservedPropsFirst, 'listReservedPropsFirst', {
308-
node: decl.name,
309-
fix: generateFixerFunction(node, context, reservedList),
310-
});
364+
reportNodeAttribute(decl, 'listReservedPropsFirst', node, context, reservedList);
365+
311366
return memo;
312367
}
313368
}
@@ -319,10 +374,8 @@ module.exports = {
319374
}
320375
if (previousIsCallback && !currentIsCallback) {
321376
// 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-
});
377+
reportNodeAttribute(memo, 'listCallbacksLast', node, context, reservedList);
378+
326379
return memo;
327380
}
328381
}
@@ -332,10 +385,8 @@ module.exports = {
332385
return decl;
333386
}
334387
if (!currentValue && previousValue) {
335-
report(context, messages.listShorthandFirst, 'listShorthandFirst', {
336-
node: memo.name,
337-
fix: generateFixerFunction(node, context, reservedList),
338-
});
388+
reportNodeAttribute(decl, 'listShorthandFirst', node, context, reservedList);
389+
339390
return memo;
340391
}
341392
}
@@ -345,10 +396,34 @@ module.exports = {
345396
return decl;
346397
}
347398
if (currentValue && !previousValue) {
348-
report(context, messages.listShorthandLast, 'listShorthandLast', {
349-
node: memo.name,
350-
fix: generateFixerFunction(node, context, reservedList),
351-
});
399+
reportNodeAttribute(memo, 'listShorthandLast', node, context, reservedList);
400+
401+
return memo;
402+
}
403+
}
404+
405+
if (multiline === 'first') {
406+
if (previousIsMultiline && !currentIsMultiline) {
407+
// Exiting the multiline prop section
408+
return decl;
409+
}
410+
if (!previousIsMultiline && currentIsMultiline) {
411+
// Encountered a non-multiline prop before a multiline prop
412+
reportNodeAttribute(decl, 'listMultilineFirst', node, context, reservedList);
413+
414+
return memo;
415+
}
416+
}
417+
418+
if (multiline === 'last') {
419+
if (!previousIsMultiline && currentIsMultiline) {
420+
// Entering the multiline prop section
421+
return decl;
422+
}
423+
if (previousIsMultiline && !currentIsMultiline) {
424+
// Encountered a non-multiline prop after a multiline prop
425+
reportNodeAttribute(memo, 'listMultilineLast', node, context, reservedList);
426+
352427
return memo;
353428
}
354429
}
@@ -361,10 +436,8 @@ module.exports = {
361436
: previousPropName > currentPropName
362437
)
363438
) {
364-
report(context, messages.sortPropsByAlpha, 'sortPropsByAlpha', {
365-
node: decl.name,
366-
fix: generateFixerFunction(node, context, reservedList),
367-
});
439+
reportNodeAttribute(decl, 'sortPropsByAlpha', node, context, reservedList);
440+
368441
return memo;
369442
}
370443

0 commit comments

Comments
 (0)