Skip to content

Commit bf3ccb3

Browse files
authored
Merge pull request #1497 from Vydia/new-rule-one-element-per-line
New rule: jsx-one-expression-per-line
2 parents fcf580b + cd3648c commit bf3ccb3

File tree

3 files changed

+1039
-0
lines changed

3 files changed

+1039
-0
lines changed

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const allRules = {
2323
'jsx-indent-props': require('./lib/rules/jsx-indent-props'),
2424
'jsx-key': require('./lib/rules/jsx-key'),
2525
'jsx-max-props-per-line': require('./lib/rules/jsx-max-props-per-line'),
26+
'jsx-one-expression-per-line': require('./lib/rules/jsx-one-expression-per-line'),
2627
'jsx-no-bind': require('./lib/rules/jsx-no-bind'),
2728
'jsx-no-comment-textnodes': require('./lib/rules/jsx-no-comment-textnodes'),
2829
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/**
2+
* @fileoverview Limit to one element tag per line in JSX
3+
* @author Mark Ivan Allen <Vydia.com>
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 nodeKey (node) {
27+
return `${node.loc.start.line},${node.loc.start.column}`;
28+
}
29+
30+
function nodeDescriptor (n) {
31+
return n.openingElement ? n.openingElement.name.name : sourceCode.getText(n).replace(/\n/g, '');
32+
}
33+
34+
return {
35+
JSXElement: function (node) {
36+
const children = node.children;
37+
38+
if (!children || !children.length) {
39+
return;
40+
}
41+
42+
const openingElement = node.openingElement;
43+
const closingElement = node.closingElement;
44+
const openingElementEndLine = openingElement.loc.end.line;
45+
const closingElementStartLine = closingElement.loc.start.line;
46+
47+
const childrenGroupedByLine = {};
48+
const fixDetailsByNode = {};
49+
50+
children.forEach(child => {
51+
let countNewLinesBeforeContent = 0;
52+
let countNewLinesAfterContent = 0;
53+
54+
if (child.type === 'Literal') {
55+
if (/^\s*$/.test(child.raw)) {
56+
return;
57+
}
58+
59+
countNewLinesBeforeContent = (child.raw.match(/^ *\n/g) || []).length;
60+
countNewLinesAfterContent = (child.raw.match(/\n *$/g) || []).length;
61+
}
62+
63+
const startLine = child.loc.start.line + countNewLinesBeforeContent;
64+
const endLine = child.loc.end.line - countNewLinesAfterContent;
65+
66+
if (startLine === endLine) {
67+
if (!childrenGroupedByLine[startLine]) {
68+
childrenGroupedByLine[startLine] = [];
69+
}
70+
childrenGroupedByLine[startLine].push(child);
71+
} else {
72+
if (!childrenGroupedByLine[startLine]) {
73+
childrenGroupedByLine[startLine] = [];
74+
}
75+
childrenGroupedByLine[startLine].push(child);
76+
if (!childrenGroupedByLine[endLine]) {
77+
childrenGroupedByLine[endLine] = [];
78+
}
79+
childrenGroupedByLine[endLine].push(child);
80+
}
81+
});
82+
83+
Object.keys(childrenGroupedByLine).forEach(_line => {
84+
const line = parseInt(_line, 10);
85+
const firstIndex = 0;
86+
const lastIndex = childrenGroupedByLine[line].length - 1;
87+
88+
childrenGroupedByLine[line].forEach((child, i) => {
89+
let prevChild;
90+
let nextChild;
91+
92+
if (i === firstIndex) {
93+
if (line === openingElementEndLine) {
94+
prevChild = openingElement;
95+
}
96+
} else {
97+
prevChild = childrenGroupedByLine[line][i - 1];
98+
}
99+
100+
if (i === lastIndex) {
101+
if (line === closingElementStartLine) {
102+
nextChild = closingElement;
103+
}
104+
} else {
105+
// We don't need to append a trailing because the next child will prepend a leading.
106+
// nextChild = childrenGroupedByLine[line][i + 1];
107+
}
108+
109+
function spaceBetweenPrev () {
110+
return (prevChild.type === 'Literal' && / $/.test(prevChild.raw)) ||
111+
(child.type === 'Literal' && /^ /.test(child.raw)) ||
112+
sourceCode.isSpaceBetweenTokens(prevChild, child);
113+
}
114+
115+
function spaceBetweenNext () {
116+
return (nextChild.type === 'Literal' && /^ /.test(nextChild.raw)) ||
117+
(child.type === 'Literal' && / $/.test(child.raw)) ||
118+
sourceCode.isSpaceBetweenTokens(child, nextChild);
119+
}
120+
121+
if (!prevChild && !nextChild) {
122+
return;
123+
}
124+
125+
const source = sourceCode.getText(child);
126+
const leadingSpace = !!(prevChild && spaceBetweenPrev());
127+
const trailingSpace = !!(nextChild && spaceBetweenNext());
128+
const leadingNewLine = !!prevChild;
129+
const trailingNewLine = !!nextChild;
130+
131+
const key = nodeKey(child);
132+
133+
if (!fixDetailsByNode[key]) {
134+
fixDetailsByNode[key] = {
135+
node: child,
136+
source: source,
137+
descriptor: nodeDescriptor(child)
138+
};
139+
}
140+
141+
if (leadingSpace) {
142+
fixDetailsByNode[key].leadingSpace = true;
143+
}
144+
if (leadingNewLine) {
145+
fixDetailsByNode[key].leadingNewLine = true;
146+
}
147+
if (trailingNewLine) {
148+
fixDetailsByNode[key].trailingNewLine = true;
149+
}
150+
if (trailingSpace) {
151+
fixDetailsByNode[key].trailingSpace = true;
152+
}
153+
});
154+
});
155+
156+
Object.keys(fixDetailsByNode).forEach(key => {
157+
const details = fixDetailsByNode[key];
158+
159+
const nodeToReport = details.node;
160+
const descriptor = details.descriptor;
161+
const source = details.source.replace(/(^ +| +(?=\n)*$)/g, '');
162+
163+
const leadingSpaceString = details.leadingSpace ? '\n{\' \'}' : '';
164+
const trailingSpaceString = details.trailingSpace ? '{\' \'}\n' : '';
165+
const leadingNewLineString = details.leadingNewLine ? '\n' : '';
166+
const trailingNewLineString = details.trailingNewLine ? '\n' : '';
167+
168+
const replaceText = `${leadingSpaceString}${leadingNewLineString}${source}${trailingNewLineString}${trailingSpaceString}`;
169+
170+
context.report({
171+
node: nodeToReport,
172+
message: `\`${descriptor}\` must be placed on a new line`,
173+
fix: function (fixer) {
174+
return fixer.replaceText(nodeToReport, replaceText);
175+
}
176+
});
177+
});
178+
}
179+
};
180+
}
181+
};

0 commit comments

Comments
 (0)