Skip to content

Commit 95d3c3f

Browse files
authored
Merge pull request #1956 from alexzherdev/1694-fragments
[New] Support shorthand fragment syntax
2 parents b161d7a + 8215be3 commit 95d3c3f

27 files changed

+1614
-307
lines changed

lib/rules/jsx-child-element-spacing.js

+34-31
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ module.exports = {
5656
]
5757
},
5858
create: function (context) {
59+
const TEXT_FOLLOWING_ELEMENT_PATTERN = /^\s*\n\s*\S/;
60+
const TEXT_PRECEDING_ELEMENT_PATTERN = /\S\s*\n\s*$/;
61+
5962
const elementName = node => (
6063
node.openingElement &&
6164
node.openingElement.name &&
@@ -68,39 +71,39 @@ module.exports = {
6871
INLINE_ELEMENTS.has(elementName(node))
6972
);
7073

71-
const TEXT_FOLLOWING_ELEMENT_PATTERN = /^\s*\n\s*\S/;
72-
const TEXT_PRECEDING_ELEMENT_PATTERN = /\S\s*\n\s*$/;
74+
const handleJSX = node => {
75+
let lastChild = null;
76+
let child = null;
77+
(node.children.concat([null])).forEach(nextChild => {
78+
if (
79+
(lastChild || nextChild) &&
80+
(!lastChild || isInlineElement(lastChild)) &&
81+
(child && (child.type === 'Literal' || child.type === 'JSXText')) &&
82+
(!nextChild || isInlineElement(nextChild)) &&
83+
true
84+
) {
85+
if (lastChild && child.value.match(TEXT_FOLLOWING_ELEMENT_PATTERN)) {
86+
context.report({
87+
node: lastChild,
88+
loc: lastChild.loc.end,
89+
message: `Ambiguous spacing after previous element ${elementName(lastChild)}`
90+
});
91+
} else if (nextChild && child.value.match(TEXT_PRECEDING_ELEMENT_PATTERN)) {
92+
context.report({
93+
node: nextChild,
94+
loc: nextChild.loc.start,
95+
message: `Ambiguous spacing before next element ${elementName(nextChild)}`
96+
});
97+
}
98+
}
99+
lastChild = child;
100+
child = nextChild;
101+
});
102+
};
73103

74104
return {
75-
JSXElement: function(node) {
76-
let lastChild = null;
77-
let child = null;
78-
(node.children.concat([null])).forEach(nextChild => {
79-
if (
80-
(lastChild || nextChild) &&
81-
(!lastChild || isInlineElement(lastChild)) &&
82-
(child && (child.type === 'Literal' || child.type === 'JSXText')) &&
83-
(!nextChild || isInlineElement(nextChild)) &&
84-
true
85-
) {
86-
if (lastChild && child.value.match(TEXT_FOLLOWING_ELEMENT_PATTERN)) {
87-
context.report({
88-
node: lastChild,
89-
loc: lastChild.loc.end,
90-
message: `Ambiguous spacing after previous element ${elementName(lastChild)}`
91-
});
92-
} else if (nextChild && child.value.match(TEXT_PRECEDING_ELEMENT_PATTERN)) {
93-
context.report({
94-
node: nextChild,
95-
loc: nextChild.loc.start,
96-
message: `Ambiguous spacing before next element ${elementName(nextChild)}`
97-
});
98-
}
99-
}
100-
lastChild = child;
101-
child = nextChild;
102-
});
103-
}
105+
JSXElement: handleJSX,
106+
JSXFragment: handleJSX
104107
};
105108
}
106109
};

lib/rules/jsx-closing-tag-location.js

+37-34
Original file line numberDiff line numberDiff line change
@@ -22,45 +22,48 @@ module.exports = {
2222
},
2323

2424
create: function(context) {
25-
return {
26-
JSXClosingElement: function(node) {
27-
if (!node.parent) {
28-
return;
29-
}
30-
31-
const opening = node.parent.openingElement;
32-
if (opening.loc.start.line === node.loc.start.line) {
33-
return;
34-
}
25+
function handleClosingElement(node) {
26+
if (!node.parent) {
27+
return;
28+
}
3529

36-
if (opening.loc.start.column === node.loc.start.column) {
37-
return;
38-
}
30+
const opening = node.parent.openingElement || node.parent.openingFragment;
31+
if (opening.loc.start.line === node.loc.start.line) {
32+
return;
33+
}
3934

40-
let message;
41-
if (!astUtil.isNodeFirstInLine(context, node)) {
42-
message = 'Closing tag of a multiline JSX expression must be on its own line.';
43-
} else {
44-
message = 'Expected closing tag to match indentation of opening.';
45-
}
35+
if (opening.loc.start.column === node.loc.start.column) {
36+
return;
37+
}
4638

47-
context.report({
48-
node: node,
49-
loc: node.loc,
50-
message,
51-
fix: function(fixer) {
52-
const indent = Array(opening.loc.start.column + 1).join(' ');
53-
if (astUtil.isNodeFirstInLine(context, node)) {
54-
return fixer.replaceTextRange(
55-
[node.range[0] - node.loc.start.column, node.range[0]],
56-
indent
57-
);
58-
}
39+
let message;
40+
if (!astUtil.isNodeFirstInLine(context, node)) {
41+
message = 'Closing tag of a multiline JSX expression must be on its own line.';
42+
} else {
43+
message = 'Expected closing tag to match indentation of opening.';
44+
}
5945

60-
return fixer.insertTextBefore(node, `\n${indent}`);
46+
context.report({
47+
node: node,
48+
loc: node.loc,
49+
message,
50+
fix: function(fixer) {
51+
const indent = Array(opening.loc.start.column + 1).join(' ');
52+
if (astUtil.isNodeFirstInLine(context, node)) {
53+
return fixer.replaceTextRange(
54+
[node.range[0] - node.loc.start.column, node.range[0]],
55+
indent
56+
);
6157
}
62-
});
63-
}
58+
59+
return fixer.insertTextBefore(node, `\n${indent}`);
60+
}
61+
});
62+
}
63+
64+
return {
65+
JSXClosingElement: handleClosingElement,
66+
JSXClosingFragment: handleClosingElement
6467
};
6568
}
6669
};

lib/rules/jsx-curly-brace-presence.js

+9-11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
'use strict';
77

88
const docsUrl = require('../util/docsUrl');
9+
const jsxUtil = require('../util/jsx');
910

1011
// ------------------------------------------------------------------------------
1112
// Constants
@@ -168,13 +169,12 @@ module.exports = {
168169
function lintUnnecessaryCurly(JSXExpressionNode) {
169170
const expression = JSXExpressionNode.expression;
170171
const expressionType = expression.type;
171-
const parentType = JSXExpressionNode.parent.type;
172172

173173
if (
174174
(expressionType === 'Literal' || expressionType === 'JSXText') &&
175175
typeof expression.value === 'string' &&
176176
!needToEscapeCharacterForJSX(expression.raw) && (
177-
parentType === 'JSXElement' ||
177+
jsxUtil.isJSX(JSXExpressionNode.parent) ||
178178
!containsQuoteCharacters(expression.value)
179179
)
180180
) {
@@ -183,32 +183,30 @@ module.exports = {
183183
expressionType === 'TemplateLiteral' &&
184184
expression.expressions.length === 0 &&
185185
!needToEscapeCharacterForJSX(expression.quasis[0].value.raw) && (
186-
parentType === 'JSXElement' ||
186+
jsxUtil.isJSX(JSXExpressionNode.parent) ||
187187
!containsQuoteCharacters(expression.quasis[0].value.cooked)
188188
)
189189
) {
190190
reportUnnecessaryCurly(JSXExpressionNode);
191191
}
192192
}
193193

194-
function areRuleConditionsSatisfied(parentType, config, ruleCondition) {
194+
function areRuleConditionsSatisfied(parent, config, ruleCondition) {
195195
return (
196-
parentType === 'JSXAttribute' &&
196+
parent.type === 'JSXAttribute' &&
197197
typeof config.props === 'string' &&
198198
config.props === ruleCondition
199199
) || (
200-
parentType === 'JSXElement' &&
200+
jsxUtil.isJSX(parent) &&
201201
typeof config.children === 'string' &&
202202
config.children === ruleCondition
203203
);
204204
}
205205

206206
function shouldCheckForUnnecessaryCurly(parent, config) {
207-
const parentType = parent.type;
208-
209207
// If there are more than one JSX child, there is no need to check for
210208
// unnecessary curly braces.
211-
if (parentType === 'JSXElement' && parent.children.length !== 1) {
209+
if (jsxUtil.isJSX(parent) && parent.children.length !== 1) {
212210
return false;
213211
}
214212

@@ -220,7 +218,7 @@ module.exports = {
220218
return false;
221219
}
222220

223-
return areRuleConditionsSatisfied(parentType, config, OPTION_NEVER);
221+
return areRuleConditionsSatisfied(parent, config, OPTION_NEVER);
224222
}
225223

226224
function shouldCheckForMissingCurly(parent, config) {
@@ -232,7 +230,7 @@ module.exports = {
232230
return false;
233231
}
234232

235-
return areRuleConditionsSatisfied(parent.type, config, OPTION_ALWAYS);
233+
return areRuleConditionsSatisfied(parent, config, OPTION_ALWAYS);
236234
}
237235

238236
// --------------------------------------------------------------------------

lib/rules/jsx-curly-spacing.js

+1
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ module.exports = {
331331
break;
332332

333333
case 'JSXElement':
334+
case 'JSXFragment':
334335
config = childrenConfig;
335336
break;
336337

lib/rules/jsx-filename-extension.js

+25-22
Original file line numberDiff line numberDiff line change
@@ -43,38 +43,41 @@ module.exports = {
4343
},
4444

4545
create: function(context) {
46+
let invalidExtension;
47+
let invalidNode;
48+
4649
function getExtensionsConfig() {
4750
return context.options[0] && context.options[0].extensions || DEFAULTS.extensions;
4851
}
4952

50-
let invalidExtension;
51-
let invalidNode;
53+
function handleJSX(node) {
54+
const filename = context.getFilename();
55+
if (filename === '<text>') {
56+
return;
57+
}
5258

53-
// --------------------------------------------------------------------------
54-
// Public
55-
// --------------------------------------------------------------------------
59+
if (invalidNode) {
60+
return;
61+
}
5662

57-
return {
58-
JSXElement: function(node) {
59-
const filename = context.getFilename();
60-
if (filename === '<text>') {
61-
return;
62-
}
63+
const allowedExtensions = getExtensionsConfig();
64+
const isAllowedExtension = allowedExtensions.some(extension => filename.slice(-extension.length) === extension);
6365

64-
if (invalidNode) {
65-
return;
66-
}
66+
if (isAllowedExtension) {
67+
return;
68+
}
6769

68-
const allowedExtensions = getExtensionsConfig();
69-
const isAllowedExtension = allowedExtensions.some(extension => filename.slice(-extension.length) === extension);
70+
invalidNode = node;
71+
invalidExtension = path.extname(filename);
72+
}
7073

71-
if (isAllowedExtension) {
72-
return;
73-
}
74+
// --------------------------------------------------------------------------
75+
// Public
76+
// --------------------------------------------------------------------------
7477

75-
invalidNode = node;
76-
invalidExtension = path.extname(filename);
77-
},
78+
return {
79+
JSXElement: handleJSX,
80+
JSXFragment: handleJSX,
7881

7982
'Program:exit': function() {
8083
if (!invalidNode) {

lib/rules/jsx-indent.js

+41-36
Original file line numberDiff line numberDiff line change
@@ -205,43 +205,48 @@ module.exports = {
205205
}
206206
}
207207

208-
return {
209-
JSXOpeningElement: function(node) {
210-
let prevToken = sourceCode.getTokenBefore(node);
211-
if (!prevToken) {
212-
return;
213-
}
214-
// Use the parent in a list or an array
215-
if (prevToken.type === 'JSXText' || prevToken.type === 'Punctuator' && prevToken.value === ',') {
216-
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
217-
prevToken = prevToken.type === 'Literal' || prevToken.type === 'JSXText' ? prevToken.parent : prevToken;
218-
// Use the first non-punctuator token in a conditional expression
219-
} else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
220-
do {
221-
prevToken = sourceCode.getTokenBefore(prevToken);
222-
} while (prevToken.type === 'Punctuator');
223-
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
224-
while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
225-
prevToken = prevToken.parent;
226-
}
227-
}
228-
prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
229-
230-
const parentElementIndent = getNodeIndent(prevToken);
231-
const indent = (
232-
prevToken.loc.start.line === node.loc.start.line ||
233-
isRightInLogicalExp(node) ||
234-
isAlternateInConditionalExp(node)
235-
) ? 0 : indentSize;
236-
checkNodesIndent(node, parentElementIndent + indent);
237-
},
238-
JSXClosingElement: function(node) {
239-
if (!node.parent) {
240-
return;
208+
function handleOpeningElement(node) {
209+
let prevToken = sourceCode.getTokenBefore(node);
210+
if (!prevToken) {
211+
return;
212+
}
213+
// Use the parent in a list or an array
214+
if (prevToken.type === 'JSXText' || prevToken.type === 'Punctuator' && prevToken.value === ',') {
215+
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
216+
prevToken = prevToken.type === 'Literal' || prevToken.type === 'JSXText' ? prevToken.parent : prevToken;
217+
// Use the first non-punctuator token in a conditional expression
218+
} else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
219+
do {
220+
prevToken = sourceCode.getTokenBefore(prevToken);
221+
} while (prevToken.type === 'Punctuator' && prevToken.value !== '/');
222+
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
223+
while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
224+
prevToken = prevToken.parent;
241225
}
242-
const peerElementIndent = getNodeIndent(node.parent.openingElement);
243-
checkNodesIndent(node, peerElementIndent);
244-
},
226+
}
227+
prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
228+
const parentElementIndent = getNodeIndent(prevToken);
229+
const indent = (
230+
prevToken.loc.start.line === node.loc.start.line ||
231+
isRightInLogicalExp(node) ||
232+
isAlternateInConditionalExp(node)
233+
) ? 0 : indentSize;
234+
checkNodesIndent(node, parentElementIndent + indent);
235+
}
236+
237+
function handleClosingElement(node) {
238+
if (!node.parent) {
239+
return;
240+
}
241+
const peerElementIndent = getNodeIndent(node.parent.openingElement || node.parent.openingFragment);
242+
checkNodesIndent(node, peerElementIndent);
243+
}
244+
245+
return {
246+
JSXOpeningElement: handleOpeningElement,
247+
JSXOpeningFragment: handleOpeningElement,
248+
JSXClosingElement: handleClosingElement,
249+
JSXClosingFragment: handleClosingElement,
245250
JSXExpressionContainer: function(node) {
246251
if (!node.parent) {
247252
return;

0 commit comments

Comments
 (0)