Skip to content

Commit 618bdb7

Browse files
committed
new rule: one-element-per-line
1 parent e9e33cf commit 618bdb7

File tree

2 files changed

+380
-0
lines changed

2 files changed

+380
-0
lines changed

lib/rules/jsx-one-element-per-line.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* @fileoverview Limit to one element tag per line in JSX
3+
* @author Mark Allen
4+
*/
5+
6+
'use strict';
7+
8+
// ------------------------------------------------------------------------------
9+
// Rule Definition
10+
// ------------------------------------------------------------------------------
11+
12+
module.exports = {
13+
meta: {
14+
docs: {
15+
description: 'Limit to one element tag per line in JSX',
16+
category: 'Stylistic Issues',
17+
recommended: false
18+
},
19+
fixable: 'whitespace',
20+
schema: []
21+
},
22+
23+
create: function (context) {
24+
const sourceCode = context.getSourceCode();
25+
26+
function generateFixFunction(elementInLine) {
27+
const nodeText = sourceCode.getText(elementInLine);
28+
29+
return function(fixer) {
30+
return fixer.replaceText(elementInLine, `\n${nodeText}`);
31+
};
32+
}
33+
34+
return {
35+
JSXOpeningElement: function (node) {
36+
if (!node.parent) {
37+
return;
38+
}
39+
40+
const openingElementEndLine = node.loc.end.line;
41+
const openingElementEndColumn = node.loc.end.column;
42+
43+
// Children
44+
if (node.parent.children.length) {
45+
const childrenOnLine = [];
46+
47+
node.parent.children.forEach(childNode => {
48+
if (!childNode.openingElement || childNode.openingElement.loc.start.line !== openingElementEndLine) {
49+
return;
50+
}
51+
52+
childrenOnLine.push(childNode);
53+
});
54+
55+
if (!childrenOnLine.length) {
56+
return;
57+
}
58+
59+
childrenOnLine.forEach(elementInLine => {
60+
context.report({
61+
node: elementInLine,
62+
message: `Opening tag for Element \`${elementInLine.openingElement.name.name}\` must be placed on a new line`,
63+
fix: generateFixFunction(elementInLine)
64+
});
65+
});
66+
}
67+
68+
// Siblings
69+
if (node.parent.parent && node.parent.parent.children && node.parent.parent.children.length) {
70+
const siblingsOnLine = [];
71+
node.parent.parent.children.forEach(childNode => {
72+
if (
73+
node.parent === childNode ||
74+
!childNode.openingElement ||
75+
childNode.openingElement.loc.start.line !== openingElementEndLine ||
76+
childNode.openingElement.loc.start.column < openingElementEndColumn
77+
) {
78+
return;
79+
}
80+
81+
siblingsOnLine.push(childNode);
82+
});
83+
84+
if (!siblingsOnLine.length) {
85+
return;
86+
}
87+
88+
siblingsOnLine.forEach(elementInLine => {
89+
context.report({
90+
node: elementInLine,
91+
message: `Opening tag for Element \`${elementInLine.openingElement.name.name}\` must be placed on a new line`,
92+
fix: generateFixFunction(elementInLine)
93+
});
94+
});
95+
}
96+
},
97+
98+
JSXClosingElement: function (node) {
99+
if (!node.parent || !node.parent.children.length) {
100+
return;
101+
}
102+
103+
const childrenOnLine = [];
104+
const closingElementStartLine = node.loc.end.line;
105+
106+
node.parent.children.forEach(childNode => {
107+
const reportableLines = [childNode.openingElement, childNode.closingElement].reduce((lines, el) => {
108+
if (!el) {
109+
return lines;
110+
}
111+
112+
return lines.concat([el.loc.start.line, el.loc.end.line]);
113+
}, []);
114+
115+
if (reportableLines.indexOf(closingElementStartLine) === -1) {
116+
return;
117+
}
118+
119+
childrenOnLine.push(childNode);
120+
});
121+
122+
if (!childrenOnLine.length) {
123+
return;
124+
}
125+
126+
context.report({
127+
node: node,
128+
message: `Closing tag for Element \`${node.name.name}\` must be placed on a new line`,
129+
fix: generateFixFunction(node)
130+
});
131+
}
132+
};
133+
}
134+
};
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/**
2+
* @fileoverview Limit to one element tag per line in JSX
3+
* @author Mark Allen
4+
*/
5+
6+
'use strict';
7+
8+
// ------------------------------------------------------------------------------
9+
// Requirements
10+
// ------------------------------------------------------------------------------
11+
12+
const rule = require('../../../lib/rules/jsx-one-element-per-line');
13+
const RuleTester = require('eslint').RuleTester;
14+
15+
const parserOptions = {
16+
ecmaVersion: 8,
17+
sourceType: 'module',
18+
ecmaFeatures: {
19+
experimentalObjectRestSpread: true,
20+
jsx: true
21+
}
22+
};
23+
24+
// ------------------------------------------------------------------------------
25+
// Tests
26+
// ------------------------------------------------------------------------------
27+
28+
const ruleTester = new RuleTester({parserOptions});
29+
ruleTester.run('jsx-max-elements-per-line', rule, {
30+
valid: [{
31+
code: '<App />'
32+
}, {
33+
code: '<App></App>'
34+
}, {
35+
code: '<App>{"foo"}</App>'
36+
}, {
37+
code: '<App>foo</App>'
38+
}, {
39+
code: '<App foo="bar" />'
40+
}, {
41+
code: [
42+
'<App>',
43+
' <Foo />',
44+
'</App>'
45+
].join('\n')
46+
}, {
47+
code: [
48+
'<App>',
49+
' <Foo></Foo>',
50+
'</App>'
51+
].join('\n')
52+
}, {
53+
code: [
54+
'<App>',
55+
' <Foo>',
56+
' </Foo>',
57+
'</App>'
58+
].join('\n')
59+
}, {
60+
code: [
61+
'<App',
62+
' foo="bar"',
63+
'>',
64+
'<Foo />',
65+
'</App>'
66+
].join('\n')
67+
}, {
68+
code: [
69+
'<',
70+
'App',
71+
'>',
72+
' <',
73+
' Foo',
74+
' />',
75+
'</',
76+
'App',
77+
'>'
78+
].join('\n')
79+
}],
80+
81+
invalid: [{
82+
code: [
83+
'<App>',
84+
' <Foo /><Bar />',
85+
'</App>'
86+
].join('\n'),
87+
output: [
88+
'<App>',
89+
' <Foo />',
90+
'<Bar />',
91+
'</App>'
92+
].join('\n'),
93+
errors: [{message: 'Opening tag for Element `Bar` must be placed on a new line'}],
94+
parserOptions: parserOptions
95+
}, {
96+
code: [
97+
'<App>',
98+
' <Foo></Foo><Bar></Bar>',
99+
'</App>'
100+
].join('\n'),
101+
output: [
102+
'<App>',
103+
' <Foo></Foo>',
104+
'<Bar></Bar>',
105+
'</App>'
106+
].join('\n'),
107+
errors: [{message: 'Opening tag for Element `Bar` must be placed on a new line'}],
108+
parserOptions: parserOptions
109+
}, {
110+
code: [
111+
'<App>',
112+
'<Foo></Foo></App>'
113+
].join('\n'),
114+
output: [
115+
'<App>',
116+
'<Foo></Foo>',
117+
'</App>'
118+
].join('\n'),
119+
errors: [{message: 'Closing tag for Element `App` must be placed on a new line'}],
120+
parserOptions: parserOptions
121+
}, {
122+
code: [
123+
'<App><Foo />',
124+
'</App>'
125+
].join('\n'),
126+
output: [
127+
'<App>',
128+
'<Foo />',
129+
'</App>'
130+
].join('\n'),
131+
errors: [{message: 'Opening tag for Element `Foo` must be placed on a new line'}],
132+
parserOptions: parserOptions
133+
}, {
134+
code: [
135+
'<App>',
136+
'<Foo/></App>'
137+
].join('\n'),
138+
output: [
139+
'<App>',
140+
'<Foo/>',
141+
'</App>'
142+
].join('\n'),
143+
errors: [{message: 'Closing tag for Element `App` must be placed on a new line'}],
144+
parserOptions: parserOptions
145+
}, {
146+
code: [
147+
'<App><Foo',
148+
'/>',
149+
'</App>'
150+
].join('\n'),
151+
output: [
152+
'<App>',
153+
'<Foo',
154+
'/>',
155+
'</App>'
156+
].join('\n'),
157+
errors: [{message: 'Opening tag for Element `Foo` must be placed on a new line'}],
158+
parserOptions: parserOptions
159+
}, {
160+
code: [
161+
'<App',
162+
'>',
163+
'<Foo /></App>'
164+
].join('\n'),
165+
output: [
166+
'<App',
167+
'>',
168+
'<Foo />',
169+
'</App>'
170+
].join('\n'),
171+
errors: [{message: 'Closing tag for Element `App` must be placed on a new line'}],
172+
parserOptions: parserOptions
173+
}, {
174+
code: [
175+
'<App',
176+
'>',
177+
'<Foo',
178+
'/></App>'
179+
].join('\n'),
180+
output: [
181+
'<App',
182+
'>',
183+
'<Foo',
184+
'/>',
185+
'</App>'
186+
].join('\n'),
187+
errors: [{message: 'Closing tag for Element `App` must be placed on a new line'}],
188+
parserOptions: parserOptions
189+
}, {
190+
code: [
191+
'<App',
192+
'><Foo />',
193+
'</App>'
194+
].join('\n'),
195+
output: [
196+
'<App',
197+
'>',
198+
'<Foo />',
199+
'</App>'
200+
].join('\n'),
201+
errors: [{message: 'Opening tag for Element `Foo` must be placed on a new line'}],
202+
parserOptions: parserOptions
203+
}, {
204+
code: [
205+
'<App>',
206+
' <Foo></Foo',
207+
'></App>'
208+
].join('\n'),
209+
output: [
210+
'<App>',
211+
' <Foo></Foo',
212+
'>',
213+
'</App>'
214+
].join('\n'),
215+
errors: [{message: 'Closing tag for Element `App` must be placed on a new line'}],
216+
parserOptions: parserOptions
217+
}, {
218+
code: [
219+
'<App>',
220+
' <Foo></',
221+
'Foo></App>'
222+
].join('\n'),
223+
output: [
224+
'<App>',
225+
' <Foo></',
226+
'Foo>',
227+
'</App>'
228+
].join('\n'),
229+
errors: [{message: 'Closing tag for Element `App` must be placed on a new line'}],
230+
parserOptions: parserOptions
231+
}, {
232+
code: [
233+
'<App>',
234+
' <Foo></',
235+
'Foo></App>'
236+
].join('\n'),
237+
output: [
238+
'<App>',
239+
' <Foo></',
240+
'Foo>',
241+
'</App>'
242+
].join('\n'),
243+
errors: [{message: 'Closing tag for Element `App` must be placed on a new line'}],
244+
parserOptions: parserOptions
245+
}]
246+
});

0 commit comments

Comments
 (0)