Skip to content

Commit 83eb226

Browse files
Carluxljharb
authored andcommitted
[New] jsx-max-props-per-line: add single and multi options
1 parent 95a8a4e commit 83eb226

File tree

4 files changed

+252
-22
lines changed

4 files changed

+252
-22
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
77

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

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

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

18+
[#3078]: https://github.com/yannickcr/eslint-plugin-react/pull/3078
1719
[#2640]: https://github.com/yannickcr/eslint-plugin-react/pull/2640
1820
[#2759]: https://github.com/yannickcr/eslint-plugin-react/pull/2759
1921
[#1873]: https://github.com/yannickcr/eslint-plugin-react/pull/1873

docs/rules/jsx-max-props-per-line.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ Examples of **correct** code for this rule:
3939
...
4040
"react/jsx-max-props-per-line": [<enabled>, { "maximum": <number>, "when": <string> }]
4141
...
42+
43+
// OR
44+
45+
...
46+
"react/jsx-max-props-per-line": [<enabled>, { "maximum": { single <number> multi: <number> } }]
47+
...
4248
```
4349

4450
### `maximum`
@@ -62,8 +68,12 @@ Examples of **correct** code for this rule:
6268
/>;
6369
```
6470

71+
Maximum can be specified as object `{ single: 1, multi: 1 }` to specify maximum allowed number of props for single line and multiple line tags.
72+
6573
### `when`
6674

75+
_when only applied if `maximum` is specified as number._
76+
6777
Possible values:
6878
- `always` (default) - Always check for max props per line.
6979
- `multiline` - Only check for max props per line when jsx tag spans multiple lines.

lib/rules/jsx-max-props-per-line.js

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
const docsUrl = require('../util/docsUrl');
99
const report = require('../util/report');
1010

11+
function getPropName(context, propNode) {
12+
if (propNode.type === 'JSXSpreadAttribute') {
13+
return context.getSourceCode().getText(propNode.argument);
14+
}
15+
return propNode.name.name;
16+
}
17+
1118
// ------------------------------------------------------------------------------
1219
// Rule Definition
1320
// ------------------------------------------------------------------------------
@@ -29,37 +36,61 @@ module.exports = {
2936
messages,
3037

3138
schema: [{
32-
type: 'object',
33-
properties: {
34-
maximum: {
35-
type: 'integer',
36-
minimum: 1
39+
anyOf: [{
40+
type: 'object',
41+
properties: {
42+
maximum: {
43+
type: 'object',
44+
properties: {
45+
single: {
46+
type: 'integer',
47+
minimum: 1
48+
},
49+
multi: {
50+
type: 'integer',
51+
minimum: 1
52+
}
53+
}
54+
}
3755
},
38-
when: {
39-
type: 'string',
40-
enum: ['always', 'multiline']
41-
}
42-
}
56+
additionalProperties: false
57+
}, {
58+
type: 'object',
59+
properties: {
60+
maximum: {
61+
type: 'number',
62+
minimum: 1
63+
},
64+
when: {
65+
type: 'string',
66+
enum: ['always', 'multiline']
67+
}
68+
},
69+
additionalProperties: false
70+
}]
4371
}]
4472
},
4573

4674
create(context) {
4775
const configuration = context.options[0] || {};
4876
const maximum = configuration.maximum || 1;
49-
const when = configuration.when || 'always';
5077

51-
function getPropName(propNode) {
52-
if (propNode.type === 'JSXSpreadAttribute') {
53-
return context.getSourceCode().getText(propNode.argument);
78+
const maxConfig = typeof maximum === 'number'
79+
? {
80+
single: configuration.when === 'multiline' ? Infinity : maximum,
81+
multi: maximum
5482
}
55-
return propNode.name.name;
56-
}
83+
: {
84+
single: maximum.single || Infinity,
85+
multi: maximum.multi || Infinity
86+
};
5787

5888
function generateFixFunction(line, max) {
5989
const sourceCode = context.getSourceCode();
6090
const output = [];
6191
const front = line[0].range[0];
6292
const back = line[line.length - 1].range[1];
93+
6394
for (let i = 0; i < line.length; i += max) {
6495
const nodes = line.slice(i, i + max);
6596
output.push(nodes.reduce((prev, curr) => {
@@ -69,7 +100,9 @@ module.exports = {
69100
return `${prev} ${sourceCode.getText(curr)}`;
70101
}, ''));
71102
}
103+
72104
const code = output.join('\n');
105+
73106
return function fix(fixer) {
74107
return fixer.replaceTextRange([front, back], code);
75108
};
@@ -81,7 +114,9 @@ module.exports = {
81114
return;
82115
}
83116

84-
if (when === 'multiline' && node.loc.start.line === node.loc.end.line) {
117+
const isSingleLineTag = node.loc.start.line === node.loc.end.line;
118+
119+
if ((isSingleLineTag ? maxConfig.single : maxConfig.multi) === Infinity) {
85120
return;
86121
}
87122

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

100135
linePartitionedProps.forEach((propsInLine) => {
101-
if (propsInLine.length > maximum) {
102-
const name = getPropName(propsInLine[maximum]);
136+
const maxPropsCountPerLine = isSingleLineTag && propsInLine[0].loc.start.line === node.loc.start.line
137+
? maxConfig.single
138+
: maxConfig.multi;
139+
140+
if (propsInLine.length > maxPropsCountPerLine) {
141+
const name = getPropName(context, propsInLine[maxPropsCountPerLine]);
103142
report(context, messages.newLine, 'newLine', {
104-
node: propsInLine[maximum],
143+
node: propsInLine[maxPropsCountPerLine],
105144
data: {
106145
prop: name
107146
},
108-
fix: generateFixFunction(propsInLine, maximum)
147+
fix: generateFixFunction(propsInLine, maxPropsCountPerLine)
109148
});
110149
}
111150
});

tests/lib/rules/jsx-max-props-per-line.js

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,70 @@ ruleTester.run('jsx-max-props-per-line', rule, {
6060
'/>'
6161
].join('\n'),
6262
options: [{maximum: 2}]
63-
}],
63+
}, {
64+
code: [
65+
'<App',
66+
' foo bar',
67+
' baz',
68+
'/>'
69+
].join('\n'),
70+
options: [{maximum: {multi: 2}}]
71+
}, {
72+
code: [
73+
'<App',
74+
' bar',
75+
' baz',
76+
'/>'
77+
].join('\n'),
78+
options: [{maximum: {multi: 2, single: 1}}]
79+
}, {
80+
code: '<App foo baz bar />',
81+
options: [{maximum: {multi: 2, single: 3}}]
82+
}, {
83+
code: '<App {...this.props} bar />',
84+
options: [{maximum: {single: 2}}]
85+
}, {
86+
code: [
87+
'<App',
88+
' foo bar',
89+
' baz bor',
90+
'/>'
91+
].join('\n'),
92+
options: [{maximum: {multi: 2, single: 1}}]
93+
}, {
94+
code: '<App foo baz bar />',
95+
options: [{maximum: {multi: 2}}]
96+
}, {
97+
code: [
98+
'<App',
99+
' foo bar',
100+
' baz bor',
101+
'/>'
102+
].join('\n'),
103+
options: [{maximum: {single: 1}}]
104+
}, {
105+
code: [
106+
'<App foo bar',
107+
' baz bor',
108+
'/>'
109+
].join('\n'),
110+
options: [{maximum: {single: 2, multi: 2}}]
111+
}, {
112+
code: [
113+
'<App foo bar',
114+
' baz bor',
115+
'/>'
116+
].join('\n'),
117+
options: [{maximum: 2}]
118+
}, {
119+
code: [
120+
'<App foo',
121+
' bar',
122+
'/>'
123+
].join('\n'),
124+
options: [{maximum: 1, when: 'multiline'}]
125+
}
126+
],
64127

65128
invalid: [{
66129
code: '<App foo bar baz />;',
@@ -266,5 +329,121 @@ ruleTester.run('jsx-max-props-per-line', rule, {
266329
messageId: 'newLine',
267330
data: {prop: 'baz'}
268331
}]
332+
},
333+
{
334+
code: '<App foo bar baz />',
335+
output: [
336+
'<App foo',
337+
'bar',
338+
'baz />'
339+
].join('\n'),
340+
options: [{maximum: {single: 1, multi: 1}}],
341+
errors: [{
342+
messageId: 'newLine',
343+
data: {prop: 'bar'}
344+
}]
345+
}, {
346+
code: [
347+
'<App',
348+
' foo bar baz',
349+
'/>'
350+
].join('\n'),
351+
output: [
352+
'<App',
353+
' foo',
354+
'bar',
355+
'baz',
356+
'/>'
357+
].join('\n'),
358+
options: [{maximum: {single: 1, multi: 1}}],
359+
errors: [{
360+
messageId: 'newLine',
361+
data: {prop: 'bar'}
362+
}]
363+
}, {
364+
code: [
365+
'<App foo',
366+
' bar baz',
367+
'/>'
368+
].join('\n'),
369+
output: [
370+
'<App foo',
371+
' bar',
372+
'baz',
373+
'/>'
374+
].join('\n'),
375+
options: [{maximum: {single: 1, multi: 1}}],
376+
errors: [{
377+
messageId: 'newLine',
378+
data: {prop: 'baz'}
379+
}]
380+
}, {
381+
code: [
382+
'<App foo bar',
383+
' bar baz bor',
384+
'/>'
385+
].join('\n'),
386+
output: [
387+
'<App foo bar',
388+
' bar baz',
389+
'bor',
390+
'/>'
391+
].join('\n'),
392+
options: [{maximum: {single: 1, multi: 2}}],
393+
errors: [
394+
{
395+
messageId: 'newLine',
396+
data: {prop: 'bor'}
397+
}]
398+
}, {
399+
code: '<App foo bar baz bor />',
400+
output: [
401+
'<App foo bar baz',
402+
'bor />'
403+
].join('\n'),
404+
options: [{maximum: {single: 3, multi: 2}}],
405+
errors: [
406+
{
407+
messageId: 'newLine',
408+
data: {prop: 'bor'}
409+
}]
410+
}, {
411+
code: [
412+
'<App',
413+
' foo={{',
414+
' }} bar baz bor',
415+
'/>'
416+
].join('\n'),
417+
output: [
418+
'<App',
419+
' foo={{',
420+
' }} bar',
421+
'baz bor',
422+
'/>'
423+
].join('\n'),
424+
options: [{maximum: {multi: 2}}],
425+
errors: [{
426+
messageId: 'newLine',
427+
data: {prop: 'baz'}
428+
}]
429+
}, {
430+
code: [
431+
'<App boz fuz',
432+
' foo={{',
433+
' }} bar baz bor',
434+
'/>'
435+
].join('\n'),
436+
output: [
437+
'<App boz fuz',
438+
' foo={{',
439+
' }} bar',
440+
'baz bor',
441+
'/>'
442+
].join('\n'),
443+
options: [{maximum: {multi: 2, single: 1}}],
444+
errors: [{
445+
messageId: 'newLine',
446+
data: {prop: 'baz'}
447+
}]
269448
}]
270449
});

0 commit comments

Comments
 (0)