Skip to content

Commit

Permalink
[New] jsx-max-props-per-line: add single and multi options
Browse files Browse the repository at this point in the history
  • Loading branch information
Carlux authored and ljharb committed Sep 20, 2021
1 parent 95a8a4e commit 83eb226
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 22 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -7,13 +7,15 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel

### Added
* add [`no-namespace`] rule ([#2640] @yacinehmito @ljharb)
* [`jsx-max-props-per-line`]: add `single` and `multi` options ([#3078] @SIL0RAK)

### Fixed
* [`display-name`]: Get rid of false position on component detection ([#2759] @iiison)

### Changed
* [`no-access-state-in-setstate`]: passing test for “don't error if it's not a React Component” ([#1873] @kentcdodds)

[#3078]: https://github.com/yannickcr/eslint-plugin-react/pull/3078
[#2640]: https://github.com/yannickcr/eslint-plugin-react/pull/2640
[#2759]: https://github.com/yannickcr/eslint-plugin-react/pull/2759
[#1873]: https://github.com/yannickcr/eslint-plugin-react/pull/1873
Expand Down
10 changes: 10 additions & 0 deletions docs/rules/jsx-max-props-per-line.md
Expand Up @@ -39,6 +39,12 @@ Examples of **correct** code for this rule:
...
"react/jsx-max-props-per-line": [<enabled>, { "maximum": <number>, "when": <string> }]
...

// OR

...
"react/jsx-max-props-per-line": [<enabled>, { "maximum": { single <number> multi: <number> } }]
...
```

### `maximum`
Expand All @@ -62,8 +68,12 @@ Examples of **correct** code for this rule:
/>;
```

Maximum can be specified as object `{ single: 1, multi: 1 }` to specify maximum allowed number of props for single line and multiple line tags.

### `when`

_when only applied if `maximum` is specified as number._

Possible values:
- `always` (default) - Always check for max props per line.
- `multiline` - Only check for max props per line when jsx tag spans multiple lines.
Expand Down
81 changes: 60 additions & 21 deletions lib/rules/jsx-max-props-per-line.js
Expand Up @@ -8,6 +8,13 @@
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');

function getPropName(context, propNode) {
if (propNode.type === 'JSXSpreadAttribute') {
return context.getSourceCode().getText(propNode.argument);
}
return propNode.name.name;
}

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
Expand All @@ -29,37 +36,61 @@ module.exports = {
messages,

schema: [{
type: 'object',
properties: {
maximum: {
type: 'integer',
minimum: 1
anyOf: [{
type: 'object',
properties: {
maximum: {
type: 'object',
properties: {
single: {
type: 'integer',
minimum: 1
},
multi: {
type: 'integer',
minimum: 1
}
}
}
},
when: {
type: 'string',
enum: ['always', 'multiline']
}
}
additionalProperties: false
}, {
type: 'object',
properties: {
maximum: {
type: 'number',
minimum: 1
},
when: {
type: 'string',
enum: ['always', 'multiline']
}
},
additionalProperties: false
}]
}]
},

create(context) {
const configuration = context.options[0] || {};
const maximum = configuration.maximum || 1;
const when = configuration.when || 'always';

function getPropName(propNode) {
if (propNode.type === 'JSXSpreadAttribute') {
return context.getSourceCode().getText(propNode.argument);
const maxConfig = typeof maximum === 'number'
? {
single: configuration.when === 'multiline' ? Infinity : maximum,
multi: maximum
}
return propNode.name.name;
}
: {
single: maximum.single || Infinity,
multi: maximum.multi || Infinity
};

function generateFixFunction(line, max) {
const sourceCode = context.getSourceCode();
const output = [];
const front = line[0].range[0];
const back = line[line.length - 1].range[1];

for (let i = 0; i < line.length; i += max) {
const nodes = line.slice(i, i + max);
output.push(nodes.reduce((prev, curr) => {
Expand All @@ -69,7 +100,9 @@ module.exports = {
return `${prev} ${sourceCode.getText(curr)}`;
}, ''));
}

const code = output.join('\n');

return function fix(fixer) {
return fixer.replaceTextRange([front, back], code);
};
Expand All @@ -81,7 +114,9 @@ module.exports = {
return;
}

if (when === 'multiline' && node.loc.start.line === node.loc.end.line) {
const isSingleLineTag = node.loc.start.line === node.loc.end.line;

if ((isSingleLineTag ? maxConfig.single : maxConfig.multi) === Infinity) {
return;
}

Expand All @@ -98,14 +133,18 @@ module.exports = {
});

linePartitionedProps.forEach((propsInLine) => {
if (propsInLine.length > maximum) {
const name = getPropName(propsInLine[maximum]);
const maxPropsCountPerLine = isSingleLineTag && propsInLine[0].loc.start.line === node.loc.start.line
? maxConfig.single
: maxConfig.multi;

if (propsInLine.length > maxPropsCountPerLine) {
const name = getPropName(context, propsInLine[maxPropsCountPerLine]);
report(context, messages.newLine, 'newLine', {
node: propsInLine[maximum],
node: propsInLine[maxPropsCountPerLine],
data: {
prop: name
},
fix: generateFixFunction(propsInLine, maximum)
fix: generateFixFunction(propsInLine, maxPropsCountPerLine)
});
}
});
Expand Down
181 changes: 180 additions & 1 deletion tests/lib/rules/jsx-max-props-per-line.js
Expand Up @@ -60,7 +60,70 @@ ruleTester.run('jsx-max-props-per-line', rule, {
'/>'
].join('\n'),
options: [{maximum: 2}]
}],
}, {
code: [
'<App',
' foo bar',
' baz',
'/>'
].join('\n'),
options: [{maximum: {multi: 2}}]
}, {
code: [
'<App',
' bar',
' baz',
'/>'
].join('\n'),
options: [{maximum: {multi: 2, single: 1}}]
}, {
code: '<App foo baz bar />',
options: [{maximum: {multi: 2, single: 3}}]
}, {
code: '<App {...this.props} bar />',
options: [{maximum: {single: 2}}]
}, {
code: [
'<App',
' foo bar',
' baz bor',
'/>'
].join('\n'),
options: [{maximum: {multi: 2, single: 1}}]
}, {
code: '<App foo baz bar />',
options: [{maximum: {multi: 2}}]
}, {
code: [
'<App',
' foo bar',
' baz bor',
'/>'
].join('\n'),
options: [{maximum: {single: 1}}]
}, {
code: [
'<App foo bar',
' baz bor',
'/>'
].join('\n'),
options: [{maximum: {single: 2, multi: 2}}]
}, {
code: [
'<App foo bar',
' baz bor',
'/>'
].join('\n'),
options: [{maximum: 2}]
}, {
code: [
'<App foo',
' bar',
'/>'
].join('\n'),
options: [{maximum: 1, when: 'multiline'}]
}
],

invalid: [{
code: '<App foo bar baz />;',
Expand Down Expand Up @@ -266,5 +329,121 @@ ruleTester.run('jsx-max-props-per-line', rule, {
messageId: 'newLine',
data: {prop: 'baz'}
}]
},
{
code: '<App foo bar baz />',
output: [
'<App foo',
'bar',
'baz />'
].join('\n'),
options: [{maximum: {single: 1, multi: 1}}],
errors: [{
messageId: 'newLine',
data: {prop: 'bar'}
}]
}, {
code: [
'<App',
' foo bar baz',
'/>'
].join('\n'),
output: [
'<App',
' foo',
'bar',
'baz',
'/>'
].join('\n'),
options: [{maximum: {single: 1, multi: 1}}],
errors: [{
messageId: 'newLine',
data: {prop: 'bar'}
}]
}, {
code: [
'<App foo',
' bar baz',
'/>'
].join('\n'),
output: [
'<App foo',
' bar',
'baz',
'/>'
].join('\n'),
options: [{maximum: {single: 1, multi: 1}}],
errors: [{
messageId: 'newLine',
data: {prop: 'baz'}
}]
}, {
code: [
'<App foo bar',
' bar baz bor',
'/>'
].join('\n'),
output: [
'<App foo bar',
' bar baz',
'bor',
'/>'
].join('\n'),
options: [{maximum: {single: 1, multi: 2}}],
errors: [
{
messageId: 'newLine',
data: {prop: 'bor'}
}]
}, {
code: '<App foo bar baz bor />',
output: [
'<App foo bar baz',
'bor />'
].join('\n'),
options: [{maximum: {single: 3, multi: 2}}],
errors: [
{
messageId: 'newLine',
data: {prop: 'bor'}
}]
}, {
code: [
'<App',
' foo={{',
' }} bar baz bor',
'/>'
].join('\n'),
output: [
'<App',
' foo={{',
' }} bar',
'baz bor',
'/>'
].join('\n'),
options: [{maximum: {multi: 2}}],
errors: [{
messageId: 'newLine',
data: {prop: 'baz'}
}]
}, {
code: [
'<App boz fuz',
' foo={{',
' }} bar baz bor',
'/>'
].join('\n'),
output: [
'<App boz fuz',
' foo={{',
' }} bar',
'baz bor',
'/>'
].join('\n'),
options: [{maximum: {multi: 2, single: 1}}],
errors: [{
messageId: 'newLine',
data: {prop: 'baz'}
}]
}]
});

0 comments on commit 83eb226

Please sign in to comment.