Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New] jsx-sort-props: support multiline prop groups #3198

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -11,6 +11,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
* [`jsx-curly-brace-presence`]: add "propElementValues" config option ([#3191][] @ljharb)
* add [`iframe-missing-sandbox`] rule ([#2753][] @tosmolka @ljharb)
* [`no-did-mount-set-state`], [`no-did-update-set-state`]: no-op with react >= 16.3 ([#1754][] @ljharb)
* [`jsx-sort-props`]: support multiline prop groups ([#3198][] @duhamelgm)

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

[#3198]: https://github.com/yannickcr/eslint-plugin-react/pull/3198
[#3195]: https://github.com/yannickcr/eslint-plugin-react/pull/3195
[#3191]: https://github.com/yannickcr/eslint-plugin-react/pull/3191
[#3190]: https://github.com/yannickcr/eslint-plugin-react/pull/3190
Expand Down
37 changes: 37 additions & 0 deletions docs/rules/jsx-sort-props.md
Expand Up @@ -29,6 +29,7 @@ Examples of **correct** code for this rule:
"callbacksLast": <boolean>,
"shorthandFirst": <boolean>,
"shorthandLast": <boolean>,
"multiline": "ignore" | "first" | "last",
"ignoreCase": <boolean>,
"noSortAlphabetically": <boolean>,
"reservedFirst": <boolean>|<array<string>>,
Expand Down Expand Up @@ -70,6 +71,42 @@ When `true`, short hand props must be listed after all other props (unless `call
<Hello name="John" tel={5555555} active validate />
```

### `multiline`

Enforced sorting for multiline props

* `ignore`: Multiline props will not be taken in consideration for sorting.

* `first`: Multiline props must be listed before all other props (unless `shorthandFirst` is set), but still respecting the alphabetical order.

* `last`: Multiline props must be listed after all other props (unless either `callbacksLast` or `shorthandLast` are set), but still respecting the alphabetical order.

Defaults to `ignore`.

```jsx
// 'jsx-sort-props': [1, { multiline: 'first' }]
<Hello
classes={{
greetings: classes.greetings,
}}
active
validate
name="John"
tel={5555555}
/>

// 'jsx-sort-props': [1, { multiline: 'last' }]
<Hello
active
validate
name="John"
tel={5555555}
classes={{
greetings: classes.greetings,
}}
/>
```

### `noSortAlphabetically`

When `true`, alphabetical order is **not** enforced:
Expand Down
113 changes: 93 additions & 20 deletions lib/rules/jsx-sort-props.js
Expand Up @@ -6,6 +6,7 @@
'use strict';

const propName = require('jsx-ast-utils/propName');
const includes = require('array-includes');
const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');
const report = require('../util/report');
Expand All @@ -18,13 +19,19 @@ function isCallbackPropName(name) {
return /^on[A-Z]/.test(name);
}

function isMultilineProp(node) {
return node.loc.start.line !== node.loc.end.line;
}

const messages = {
noUnreservedProps: 'A customized reserved first list must only contain a subset of React reserved props. Remove: {{unreservedWords}}',
listIsEmpty: 'A customized reserved first list must not be empty',
listReservedPropsFirst: 'Reserved props must be listed before all other props',
listCallbacksLast: 'Callbacks must be listed after all other props',
listShorthandFirst: 'Shorthand props must be listed before all other props',
listShorthandLast: 'Shorthand props must be listed after all other props',
listMultilineFirst: 'Multiline props must be listed before all other props',
listMultilineLast: 'Multiline props must be listed after all other props',
sortPropsByAlpha: 'Props should be sorted alphabetically',
};

Expand Down Expand Up @@ -75,6 +82,18 @@ function contextCompare(a, b, options) {
}
}

if (options.multiline !== 'ignore') {
const multilineSign = options.multiline === 'first' ? -1 : 1;
const aIsMultiline = isMultilineProp(a);
const bIsMultiline = isMultilineProp(b);
if (aIsMultiline && !bIsMultiline) {
return multilineSign;
}
if (!aIsMultiline && bIsMultiline) {
return -multilineSign;
}
}

if (options.noSortAlphabetically) {
return 0;
}
Expand Down Expand Up @@ -127,6 +146,7 @@ const generateFixerFunction = (node, context, reservedList) => {
const callbacksLast = configuration.callbacksLast || false;
const shorthandFirst = configuration.shorthandFirst || false;
const shorthandLast = configuration.shorthandLast || false;
const multiline = configuration.multiline || 'ignore';
const noSortAlphabetically = configuration.noSortAlphabetically || false;
const reservedFirst = configuration.reservedFirst || false;

Expand All @@ -138,6 +158,7 @@ const generateFixerFunction = (node, context, reservedList) => {
callbacksLast,
shorthandFirst,
shorthandLast,
multiline,
noSortAlphabetically,
reservedFirst,
reservedList,
Expand Down Expand Up @@ -213,6 +234,34 @@ function validateReservedFirstConfig(context, reservedFirst) {
}
}

const reportedNodeAttributes = new WeakMap();
/**
* Check if the current node attribute has already been reported with the same error type
* if that's the case then we don't report a new error
* otherwise we report the error
* @param {Object} nodeAttribute The node attribute to be reported
* @param {string} errorType The error type to be reported
* @param {Object} node The parent node for the node attribute
* @param {Object} context The context of the rule
* @param {Array<String>} reservedList The list of reserved props
*/
function reportNodeAttribute(nodeAttribute, errorType, node, context, reservedList) {
const errors = reportedNodeAttributes.get(nodeAttribute) || [];

if (includes(errors, errorType)) {
return;
}

errors.push(errorType);

reportedNodeAttributes.set(nodeAttribute, errors);

report(context, messages[errorType], errorType, {
node: nodeAttribute.name,
fix: generateFixerFunction(node, context, reservedList),
});
}

module.exports = {
meta: {
docs: {
Expand Down Expand Up @@ -241,6 +290,11 @@ module.exports = {
shorthandLast: {
type: 'boolean',
},
// Whether multiline properties should be listed first or last
multiline: {
enum: ['ignore', 'first', 'last'],
duhamelgm marked this conversation as resolved.
Show resolved Hide resolved
default: 'ignore',
},
ignoreCase: {
type: 'boolean',
},
Expand All @@ -262,6 +316,7 @@ module.exports = {
const callbacksLast = configuration.callbacksLast || false;
const shorthandFirst = configuration.shorthandFirst || false;
const shorthandLast = configuration.shorthandLast || false;
const multiline = configuration.multiline || 'ignore';
const noSortAlphabetically = configuration.noSortAlphabetically || false;
const reservedFirst = configuration.reservedFirst || false;
const reservedFirstError = validateReservedFirstConfig(context, reservedFirst);
Expand All @@ -285,6 +340,8 @@ module.exports = {
const currentValue = decl.value;
const previousIsCallback = isCallbackPropName(previousPropName);
const currentIsCallback = isCallbackPropName(currentPropName);
const previousIsMultiline = isMultilineProp(memo);
const currentIsMultiline = isMultilineProp(decl);

if (ignoreCase) {
previousPropName = previousPropName.toLowerCase();
Expand All @@ -304,10 +361,8 @@ module.exports = {
return decl;
}
if (!previousIsReserved && currentIsReserved) {
report(context, messages.listReservedPropsFirst, 'listReservedPropsFirst', {
node: decl.name,
fix: generateFixerFunction(node, context, reservedList),
});
reportNodeAttribute(decl, 'listReservedPropsFirst', node, context, reservedList);

return memo;
}
}
Expand All @@ -319,10 +374,8 @@ module.exports = {
}
if (previousIsCallback && !currentIsCallback) {
// Encountered a non-callback prop after a callback prop
report(context, messages.listCallbacksLast, 'listCallbacksLast', {
node: memo.name,
fix: generateFixerFunction(node, context, reservedList),
});
reportNodeAttribute(memo, 'listCallbacksLast', node, context, reservedList);

return memo;
}
}
Expand All @@ -332,10 +385,8 @@ module.exports = {
return decl;
}
if (!currentValue && previousValue) {
report(context, messages.listShorthandFirst, 'listShorthandFirst', {
node: memo.name,
fix: generateFixerFunction(node, context, reservedList),
});
reportNodeAttribute(decl, 'listShorthandFirst', node, context, reservedList);

return memo;
}
}
Expand All @@ -345,10 +396,34 @@ module.exports = {
return decl;
}
if (currentValue && !previousValue) {
report(context, messages.listShorthandLast, 'listShorthandLast', {
node: memo.name,
fix: generateFixerFunction(node, context, reservedList),
});
reportNodeAttribute(memo, 'listShorthandLast', node, context, reservedList);

return memo;
}
}

if (multiline === 'first') {
if (previousIsMultiline && !currentIsMultiline) {
// Exiting the multiline prop section
return decl;
}
if (!previousIsMultiline && currentIsMultiline) {
// Encountered a non-multiline prop before a multiline prop
reportNodeAttribute(decl, 'listMultilineFirst', node, context, reservedList);

return memo;
}
}

if (multiline === 'last') {
if (!previousIsMultiline && currentIsMultiline) {
// Entering the multiline prop section
return decl;
}
if (previousIsMultiline && !currentIsMultiline) {
// Encountered a non-multiline prop after a multiline prop
reportNodeAttribute(memo, 'listMultilineLast', node, context, reservedList);

return memo;
}
}
Expand All @@ -361,10 +436,8 @@ module.exports = {
: previousPropName > currentPropName
)
) {
report(context, messages.sortPropsByAlpha, 'sortPropsByAlpha', {
node: decl.name,
fix: generateFixerFunction(node, context, reservedList),
});
reportNodeAttribute(decl, 'sortPropsByAlpha', node, context, reservedList);

return memo;
}

Expand Down