Skip to content

Commit 83eb307

Browse files
committed
jsx-one-expression-per-line: support shorthand fragments
1 parent d537c06 commit 83eb307

File tree

2 files changed

+194
-133
lines changed

2 files changed

+194
-133
lines changed

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

Lines changed: 136 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -50,171 +50,174 @@ module.exports = {
5050
return n.openingElement ? n.openingElement.name.name : sourceCode.getText(n).replace(/\n/g, '');
5151
}
5252

53-
return {
54-
JSXElement: function (node) {
55-
const children = node.children;
56-
57-
if (!children || !children.length) {
58-
return;
59-
}
53+
function handleJSX(node) {
54+
const children = node.children;
6055

61-
const openingElement = node.openingElement;
62-
const closingElement = node.closingElement;
63-
const openingElementStartLine = openingElement.loc.start.line;
64-
const openingElementEndLine = openingElement.loc.end.line;
65-
const closingElementStartLine = closingElement.loc.start.line;
66-
const closingElementEndLine = closingElement.loc.end.line;
56+
if (!children || !children.length) {
57+
return;
58+
}
6759

68-
if (children.length === 1) {
69-
const child = children[0];
60+
const openingElement = node.openingElement || node.openingFragment;
61+
const closingElement = node.closingElement || node.closingFragment;
62+
const openingElementStartLine = openingElement.loc.start.line;
63+
const openingElementEndLine = openingElement.loc.end.line;
64+
const closingElementStartLine = closingElement.loc.start.line;
65+
const closingElementEndLine = closingElement.loc.end.line;
66+
67+
if (children.length === 1) {
68+
const child = children[0];
69+
if (
70+
openingElementStartLine === openingElementEndLine &&
71+
openingElementEndLine === closingElementStartLine &&
72+
closingElementStartLine === closingElementEndLine &&
73+
closingElementEndLine === child.loc.start.line &&
74+
child.loc.start.line === child.loc.end.line
75+
) {
7076
if (
71-
openingElementStartLine === openingElementEndLine &&
72-
openingElementEndLine === closingElementStartLine &&
73-
closingElementStartLine === closingElementEndLine &&
74-
closingElementEndLine === child.loc.start.line &&
75-
child.loc.start.line === child.loc.end.line
77+
options.allow === 'single-child' ||
78+
options.allow === 'literal' && (child.type === 'Literal' || child.type === 'JSXText')
7679
) {
77-
if (
78-
options.allow === 'single-child' ||
79-
options.allow === 'literal' && (child.type === 'Literal' || child.type === 'JSXText')
80-
) {
81-
return;
82-
}
80+
return;
8381
}
8482
}
83+
}
8584

86-
const childrenGroupedByLine = {};
87-
const fixDetailsByNode = {};
85+
const childrenGroupedByLine = {};
86+
const fixDetailsByNode = {};
8887

89-
children.forEach(child => {
90-
let countNewLinesBeforeContent = 0;
91-
let countNewLinesAfterContent = 0;
88+
children.forEach(child => {
89+
let countNewLinesBeforeContent = 0;
90+
let countNewLinesAfterContent = 0;
9291

93-
if (child.type === 'Literal' || child.type === 'JSXText') {
94-
if (/^\s*$/.test(child.raw)) {
95-
return;
96-
}
92+
if (child.type === 'Literal' || child.type === 'JSXText') {
93+
if (/^\s*$/.test(child.raw)) {
94+
return;
95+
}
96+
97+
countNewLinesBeforeContent = (child.raw.match(/^ *\n/g) || []).length;
98+
countNewLinesAfterContent = (child.raw.match(/\n *$/g) || []).length;
99+
}
100+
101+
const startLine = child.loc.start.line + countNewLinesBeforeContent;
102+
const endLine = child.loc.end.line - countNewLinesAfterContent;
97103

98-
countNewLinesBeforeContent = (child.raw.match(/^ *\n/g) || []).length;
99-
countNewLinesAfterContent = (child.raw.match(/\n *$/g) || []).length;
104+
if (startLine === endLine) {
105+
if (!childrenGroupedByLine[startLine]) {
106+
childrenGroupedByLine[startLine] = [];
100107
}
108+
childrenGroupedByLine[startLine].push(child);
109+
} else {
110+
if (!childrenGroupedByLine[startLine]) {
111+
childrenGroupedByLine[startLine] = [];
112+
}
113+
childrenGroupedByLine[startLine].push(child);
114+
if (!childrenGroupedByLine[endLine]) {
115+
childrenGroupedByLine[endLine] = [];
116+
}
117+
childrenGroupedByLine[endLine].push(child);
118+
}
119+
});
101120

102-
const startLine = child.loc.start.line + countNewLinesBeforeContent;
103-
const endLine = child.loc.end.line - countNewLinesAfterContent;
121+
Object.keys(childrenGroupedByLine).forEach(_line => {
122+
const line = parseInt(_line, 10);
123+
const firstIndex = 0;
124+
const lastIndex = childrenGroupedByLine[line].length - 1;
104125

105-
if (startLine === endLine) {
106-
if (!childrenGroupedByLine[startLine]) {
107-
childrenGroupedByLine[startLine] = [];
126+
childrenGroupedByLine[line].forEach((child, i) => {
127+
let prevChild;
128+
let nextChild;
129+
130+
if (i === firstIndex) {
131+
if (line === openingElementEndLine) {
132+
prevChild = openingElement;
108133
}
109-
childrenGroupedByLine[startLine].push(child);
110134
} else {
111-
if (!childrenGroupedByLine[startLine]) {
112-
childrenGroupedByLine[startLine] = [];
113-
}
114-
childrenGroupedByLine[startLine].push(child);
115-
if (!childrenGroupedByLine[endLine]) {
116-
childrenGroupedByLine[endLine] = [];
117-
}
118-
childrenGroupedByLine[endLine].push(child);
135+
prevChild = childrenGroupedByLine[line][i - 1];
119136
}
120-
});
121-
122-
Object.keys(childrenGroupedByLine).forEach(_line => {
123-
const line = parseInt(_line, 10);
124-
const firstIndex = 0;
125-
const lastIndex = childrenGroupedByLine[line].length - 1;
126-
127-
childrenGroupedByLine[line].forEach((child, i) => {
128-
let prevChild;
129-
let nextChild;
130-
131-
if (i === firstIndex) {
132-
if (line === openingElementEndLine) {
133-
prevChild = openingElement;
134-
}
135-
} else {
136-
prevChild = childrenGroupedByLine[line][i - 1];
137-
}
138137

139-
if (i === lastIndex) {
140-
if (line === closingElementStartLine) {
141-
nextChild = closingElement;
142-
}
143-
} else {
144-
// We don't need to append a trailing because the next child will prepend a leading.
145-
// nextChild = childrenGroupedByLine[line][i + 1];
138+
if (i === lastIndex) {
139+
if (line === closingElementStartLine) {
140+
nextChild = closingElement;
146141
}
142+
} else {
143+
// We don't need to append a trailing because the next child will prepend a leading.
144+
// nextChild = childrenGroupedByLine[line][i + 1];
145+
}
147146

148-
function spaceBetweenPrev () {
149-
return ((prevChild.type === 'Literal' || prevChild.type === 'JSXText') && / $/.test(prevChild.raw)) ||
150-
((child.type === 'Literal' || child.type === 'JSXText') && /^ /.test(child.raw)) ||
151-
sourceCode.isSpaceBetweenTokens(prevChild, child);
152-
}
147+
function spaceBetweenPrev () {
148+
return ((prevChild.type === 'Literal' || prevChild.type === 'JSXText') && / $/.test(prevChild.raw)) ||
149+
((child.type === 'Literal' || child.type === 'JSXText') && /^ /.test(child.raw)) ||
150+
sourceCode.isSpaceBetweenTokens(prevChild, child);
151+
}
153152

154-
function spaceBetweenNext () {
155-
return ((nextChild.type === 'Literal' || nextChild.type === 'JSXText') && /^ /.test(nextChild.raw)) ||
156-
((child.type === 'Literal' || child.type === 'JSXText') && / $/.test(child.raw)) ||
157-
sourceCode.isSpaceBetweenTokens(child, nextChild);
158-
}
153+
function spaceBetweenNext () {
154+
return ((nextChild.type === 'Literal' || nextChild.type === 'JSXText') && /^ /.test(nextChild.raw)) ||
155+
((child.type === 'Literal' || child.type === 'JSXText') && / $/.test(child.raw)) ||
156+
sourceCode.isSpaceBetweenTokens(child, nextChild);
157+
}
159158

160-
if (!prevChild && !nextChild) {
161-
return;
162-
}
159+
if (!prevChild && !nextChild) {
160+
return;
161+
}
163162

164-
const source = sourceCode.getText(child);
165-
const leadingSpace = !!(prevChild && spaceBetweenPrev());
166-
const trailingSpace = !!(nextChild && spaceBetweenNext());
167-
const leadingNewLine = !!prevChild;
168-
const trailingNewLine = !!nextChild;
163+
const source = sourceCode.getText(child);
164+
const leadingSpace = !!(prevChild && spaceBetweenPrev());
165+
const trailingSpace = !!(nextChild && spaceBetweenNext());
166+
const leadingNewLine = !!prevChild;
167+
const trailingNewLine = !!nextChild;
169168

170-
const key = nodeKey(child);
169+
const key = nodeKey(child);
171170

172-
if (!fixDetailsByNode[key]) {
173-
fixDetailsByNode[key] = {
174-
node: child,
175-
source: source,
176-
descriptor: nodeDescriptor(child)
177-
};
178-
}
171+
if (!fixDetailsByNode[key]) {
172+
fixDetailsByNode[key] = {
173+
node: child,
174+
source: source,
175+
descriptor: nodeDescriptor(child)
176+
};
177+
}
179178

180-
if (leadingSpace) {
181-
fixDetailsByNode[key].leadingSpace = true;
182-
}
183-
if (leadingNewLine) {
184-
fixDetailsByNode[key].leadingNewLine = true;
185-
}
186-
if (trailingNewLine) {
187-
fixDetailsByNode[key].trailingNewLine = true;
188-
}
189-
if (trailingSpace) {
190-
fixDetailsByNode[key].trailingSpace = true;
191-
}
192-
});
179+
if (leadingSpace) {
180+
fixDetailsByNode[key].leadingSpace = true;
181+
}
182+
if (leadingNewLine) {
183+
fixDetailsByNode[key].leadingNewLine = true;
184+
}
185+
if (trailingNewLine) {
186+
fixDetailsByNode[key].trailingNewLine = true;
187+
}
188+
if (trailingSpace) {
189+
fixDetailsByNode[key].trailingSpace = true;
190+
}
193191
});
192+
});
194193

195-
Object.keys(fixDetailsByNode).forEach(key => {
196-
const details = fixDetailsByNode[key];
194+
Object.keys(fixDetailsByNode).forEach(key => {
195+
const details = fixDetailsByNode[key];
197196

198-
const nodeToReport = details.node;
199-
const descriptor = details.descriptor;
200-
const source = details.source.replace(/(^ +| +(?=\n)*$)/g, '');
197+
const nodeToReport = details.node;
198+
const descriptor = details.descriptor;
199+
const source = details.source.replace(/(^ +| +(?=\n)*$)/g, '');
201200

202-
const leadingSpaceString = details.leadingSpace ? '\n{\' \'}' : '';
203-
const trailingSpaceString = details.trailingSpace ? '{\' \'}\n' : '';
204-
const leadingNewLineString = details.leadingNewLine ? '\n' : '';
205-
const trailingNewLineString = details.trailingNewLine ? '\n' : '';
201+
const leadingSpaceString = details.leadingSpace ? '\n{\' \'}' : '';
202+
const trailingSpaceString = details.trailingSpace ? '{\' \'}\n' : '';
203+
const leadingNewLineString = details.leadingNewLine ? '\n' : '';
204+
const trailingNewLineString = details.trailingNewLine ? '\n' : '';
206205

207-
const replaceText = `${leadingSpaceString}${leadingNewLineString}${source}${trailingNewLineString}${trailingSpaceString}`;
206+
const replaceText = `${leadingSpaceString}${leadingNewLineString}${source}${trailingNewLineString}${trailingSpaceString}`;
208207

209-
context.report({
210-
node: nodeToReport,
211-
message: `\`${descriptor}\` must be placed on a new line`,
212-
fix: function (fixer) {
213-
return fixer.replaceText(nodeToReport, replaceText);
214-
}
215-
});
208+
context.report({
209+
node: nodeToReport,
210+
message: `\`${descriptor}\` must be placed on a new line`,
211+
fix: function (fixer) {
212+
return fixer.replaceText(nodeToReport, replaceText);
213+
}
216214
});
217-
}
215+
});
216+
}
217+
218+
return {
219+
JSXElement: handleJSX,
220+
JSXFragment: handleJSX
218221
};
219222
}
220223
};

tests/lib/rules/jsx-one-expression-per-line.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,24 @@ ruleTester.run('jsx-one-expression-per-line', rule, {
102102
}, {
103103
code: '<App><Foo /></App>',
104104
options: [{allow: 'single-child'}]
105+
}, {
106+
code: '<></>',
107+
parser: 'babel-eslint'
108+
}, {
109+
code: [
110+
'<>',
111+
' <Foo />',
112+
'</>'
113+
].join('\n'),
114+
parser: 'babel-eslint'
115+
}, {
116+
code: [
117+
'<>',
118+
' <Foo />',
119+
' <Bar />',
120+
'</>'
121+
].join('\n'),
122+
parser: 'babel-eslint'
105123
}],
106124

107125
invalid: [{
@@ -949,5 +967,45 @@ ruleTester.run('jsx-one-expression-per-line', rule, {
949967
'</App>'
950968
].join('\n'),
951969
errors: [{message: '`foobar` must be placed on a new line'}]
970+
}, {
971+
code: '<>{"foo"}</>',
972+
output: [
973+
'<>',
974+
'{"foo"}',
975+
'</>'
976+
].join('\n'),
977+
errors: [{message: '`{"foo"}` must be placed on a new line'}],
978+
parser: 'babel-eslint',
979+
parserOptions: parserOptions
980+
}, {
981+
code: [
982+
'<App>',
983+
' <Foo /><></>',
984+
'</App>'
985+
].join('\n'),
986+
output: [
987+
'<App>',
988+
' <Foo />',
989+
'<></>',
990+
'</App>'
991+
].join('\n'),
992+
errors: [{message: '`<></>` must be placed on a new line'}],
993+
parser: 'babel-eslint',
994+
parserOptions: parserOptions
995+
}, {
996+
code: [
997+
'<',
998+
'><Foo />',
999+
'</>'
1000+
].join('\n'),
1001+
output: [
1002+
'<',
1003+
'>',
1004+
'<Foo />',
1005+
'</>'
1006+
].join('\n'),
1007+
errors: [{message: '`Foo` must be placed on a new line'}],
1008+
parser: 'babel-eslint',
1009+
parserOptions: parserOptions
9521010
}]
9531011
});

0 commit comments

Comments
 (0)