Skip to content

Commit

Permalink
Merge pull request jsx-eslint#1956 from alexzherdev/1694-fragments
Browse files Browse the repository at this point in the history
[New] Support shorthand fragment syntax
  • Loading branch information
ljharb committed Sep 8, 2018
2 parents b161d7a + 8215be3 commit 95d3c3f
Show file tree
Hide file tree
Showing 27 changed files with 1,614 additions and 307 deletions.
65 changes: 34 additions & 31 deletions lib/rules/jsx-child-element-spacing.js
Expand Up @@ -56,6 +56,9 @@ module.exports = {
]
},
create: function (context) {
const TEXT_FOLLOWING_ELEMENT_PATTERN = /^\s*\n\s*\S/;
const TEXT_PRECEDING_ELEMENT_PATTERN = /\S\s*\n\s*$/;

const elementName = node => (
node.openingElement &&
node.openingElement.name &&
Expand All @@ -68,39 +71,39 @@ module.exports = {
INLINE_ELEMENTS.has(elementName(node))
);

const TEXT_FOLLOWING_ELEMENT_PATTERN = /^\s*\n\s*\S/;
const TEXT_PRECEDING_ELEMENT_PATTERN = /\S\s*\n\s*$/;
const handleJSX = node => {
let lastChild = null;
let child = null;
(node.children.concat([null])).forEach(nextChild => {
if (
(lastChild || nextChild) &&
(!lastChild || isInlineElement(lastChild)) &&
(child && (child.type === 'Literal' || child.type === 'JSXText')) &&
(!nextChild || isInlineElement(nextChild)) &&
true
) {
if (lastChild && child.value.match(TEXT_FOLLOWING_ELEMENT_PATTERN)) {
context.report({
node: lastChild,
loc: lastChild.loc.end,
message: `Ambiguous spacing after previous element ${elementName(lastChild)}`
});
} else if (nextChild && child.value.match(TEXT_PRECEDING_ELEMENT_PATTERN)) {
context.report({
node: nextChild,
loc: nextChild.loc.start,
message: `Ambiguous spacing before next element ${elementName(nextChild)}`
});
}
}
lastChild = child;
child = nextChild;
});
};

return {
JSXElement: function(node) {
let lastChild = null;
let child = null;
(node.children.concat([null])).forEach(nextChild => {
if (
(lastChild || nextChild) &&
(!lastChild || isInlineElement(lastChild)) &&
(child && (child.type === 'Literal' || child.type === 'JSXText')) &&
(!nextChild || isInlineElement(nextChild)) &&
true
) {
if (lastChild && child.value.match(TEXT_FOLLOWING_ELEMENT_PATTERN)) {
context.report({
node: lastChild,
loc: lastChild.loc.end,
message: `Ambiguous spacing after previous element ${elementName(lastChild)}`
});
} else if (nextChild && child.value.match(TEXT_PRECEDING_ELEMENT_PATTERN)) {
context.report({
node: nextChild,
loc: nextChild.loc.start,
message: `Ambiguous spacing before next element ${elementName(nextChild)}`
});
}
}
lastChild = child;
child = nextChild;
});
}
JSXElement: handleJSX,
JSXFragment: handleJSX
};
}
};
71 changes: 37 additions & 34 deletions lib/rules/jsx-closing-tag-location.js
Expand Up @@ -22,45 +22,48 @@ module.exports = {
},

create: function(context) {
return {
JSXClosingElement: function(node) {
if (!node.parent) {
return;
}

const opening = node.parent.openingElement;
if (opening.loc.start.line === node.loc.start.line) {
return;
}
function handleClosingElement(node) {
if (!node.parent) {
return;
}

if (opening.loc.start.column === node.loc.start.column) {
return;
}
const opening = node.parent.openingElement || node.parent.openingFragment;
if (opening.loc.start.line === node.loc.start.line) {
return;
}

let message;
if (!astUtil.isNodeFirstInLine(context, node)) {
message = 'Closing tag of a multiline JSX expression must be on its own line.';
} else {
message = 'Expected closing tag to match indentation of opening.';
}
if (opening.loc.start.column === node.loc.start.column) {
return;
}

context.report({
node: node,
loc: node.loc,
message,
fix: function(fixer) {
const indent = Array(opening.loc.start.column + 1).join(' ');
if (astUtil.isNodeFirstInLine(context, node)) {
return fixer.replaceTextRange(
[node.range[0] - node.loc.start.column, node.range[0]],
indent
);
}
let message;
if (!astUtil.isNodeFirstInLine(context, node)) {
message = 'Closing tag of a multiline JSX expression must be on its own line.';
} else {
message = 'Expected closing tag to match indentation of opening.';
}

return fixer.insertTextBefore(node, `\n${indent}`);
context.report({
node: node,
loc: node.loc,
message,
fix: function(fixer) {
const indent = Array(opening.loc.start.column + 1).join(' ');
if (astUtil.isNodeFirstInLine(context, node)) {
return fixer.replaceTextRange(
[node.range[0] - node.loc.start.column, node.range[0]],
indent
);
}
});
}

return fixer.insertTextBefore(node, `\n${indent}`);
}
});
}

return {
JSXClosingElement: handleClosingElement,
JSXClosingFragment: handleClosingElement
};
}
};
20 changes: 9 additions & 11 deletions lib/rules/jsx-curly-brace-presence.js
Expand Up @@ -6,6 +6,7 @@
'use strict';

const docsUrl = require('../util/docsUrl');
const jsxUtil = require('../util/jsx');

// ------------------------------------------------------------------------------
// Constants
Expand Down Expand Up @@ -168,13 +169,12 @@ module.exports = {
function lintUnnecessaryCurly(JSXExpressionNode) {
const expression = JSXExpressionNode.expression;
const expressionType = expression.type;
const parentType = JSXExpressionNode.parent.type;

if (
(expressionType === 'Literal' || expressionType === 'JSXText') &&
typeof expression.value === 'string' &&
!needToEscapeCharacterForJSX(expression.raw) && (
parentType === 'JSXElement' ||
jsxUtil.isJSX(JSXExpressionNode.parent) ||
!containsQuoteCharacters(expression.value)
)
) {
Expand All @@ -183,32 +183,30 @@ module.exports = {
expressionType === 'TemplateLiteral' &&
expression.expressions.length === 0 &&
!needToEscapeCharacterForJSX(expression.quasis[0].value.raw) && (
parentType === 'JSXElement' ||
jsxUtil.isJSX(JSXExpressionNode.parent) ||
!containsQuoteCharacters(expression.quasis[0].value.cooked)
)
) {
reportUnnecessaryCurly(JSXExpressionNode);
}
}

function areRuleConditionsSatisfied(parentType, config, ruleCondition) {
function areRuleConditionsSatisfied(parent, config, ruleCondition) {
return (
parentType === 'JSXAttribute' &&
parent.type === 'JSXAttribute' &&
typeof config.props === 'string' &&
config.props === ruleCondition
) || (
parentType === 'JSXElement' &&
jsxUtil.isJSX(parent) &&
typeof config.children === 'string' &&
config.children === ruleCondition
);
}

function shouldCheckForUnnecessaryCurly(parent, config) {
const parentType = parent.type;

// If there are more than one JSX child, there is no need to check for
// unnecessary curly braces.
if (parentType === 'JSXElement' && parent.children.length !== 1) {
if (jsxUtil.isJSX(parent) && parent.children.length !== 1) {
return false;
}

Expand All @@ -220,7 +218,7 @@ module.exports = {
return false;
}

return areRuleConditionsSatisfied(parentType, config, OPTION_NEVER);
return areRuleConditionsSatisfied(parent, config, OPTION_NEVER);
}

function shouldCheckForMissingCurly(parent, config) {
Expand All @@ -232,7 +230,7 @@ module.exports = {
return false;
}

return areRuleConditionsSatisfied(parent.type, config, OPTION_ALWAYS);
return areRuleConditionsSatisfied(parent, config, OPTION_ALWAYS);
}

// --------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions lib/rules/jsx-curly-spacing.js
Expand Up @@ -331,6 +331,7 @@ module.exports = {
break;

case 'JSXElement':
case 'JSXFragment':
config = childrenConfig;
break;

Expand Down
47 changes: 25 additions & 22 deletions lib/rules/jsx-filename-extension.js
Expand Up @@ -43,38 +43,41 @@ module.exports = {
},

create: function(context) {
let invalidExtension;
let invalidNode;

function getExtensionsConfig() {
return context.options[0] && context.options[0].extensions || DEFAULTS.extensions;
}

let invalidExtension;
let invalidNode;
function handleJSX(node) {
const filename = context.getFilename();
if (filename === '<text>') {
return;
}

// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
if (invalidNode) {
return;
}

return {
JSXElement: function(node) {
const filename = context.getFilename();
if (filename === '<text>') {
return;
}
const allowedExtensions = getExtensionsConfig();
const isAllowedExtension = allowedExtensions.some(extension => filename.slice(-extension.length) === extension);

if (invalidNode) {
return;
}
if (isAllowedExtension) {
return;
}

const allowedExtensions = getExtensionsConfig();
const isAllowedExtension = allowedExtensions.some(extension => filename.slice(-extension.length) === extension);
invalidNode = node;
invalidExtension = path.extname(filename);
}

if (isAllowedExtension) {
return;
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------

invalidNode = node;
invalidExtension = path.extname(filename);
},
return {
JSXElement: handleJSX,
JSXFragment: handleJSX,

'Program:exit': function() {
if (!invalidNode) {
Expand Down
77 changes: 41 additions & 36 deletions lib/rules/jsx-indent.js
Expand Up @@ -205,43 +205,48 @@ module.exports = {
}
}

return {
JSXOpeningElement: function(node) {
let prevToken = sourceCode.getTokenBefore(node);
if (!prevToken) {
return;
}
// Use the parent in a list or an array
if (prevToken.type === 'JSXText' || prevToken.type === 'Punctuator' && prevToken.value === ',') {
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
prevToken = prevToken.type === 'Literal' || prevToken.type === 'JSXText' ? prevToken.parent : prevToken;
// Use the first non-punctuator token in a conditional expression
} else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
do {
prevToken = sourceCode.getTokenBefore(prevToken);
} while (prevToken.type === 'Punctuator');
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
prevToken = prevToken.parent;
}
}
prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;

const parentElementIndent = getNodeIndent(prevToken);
const indent = (
prevToken.loc.start.line === node.loc.start.line ||
isRightInLogicalExp(node) ||
isAlternateInConditionalExp(node)
) ? 0 : indentSize;
checkNodesIndent(node, parentElementIndent + indent);
},
JSXClosingElement: function(node) {
if (!node.parent) {
return;
function handleOpeningElement(node) {
let prevToken = sourceCode.getTokenBefore(node);
if (!prevToken) {
return;
}
// Use the parent in a list or an array
if (prevToken.type === 'JSXText' || prevToken.type === 'Punctuator' && prevToken.value === ',') {
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
prevToken = prevToken.type === 'Literal' || prevToken.type === 'JSXText' ? prevToken.parent : prevToken;
// Use the first non-punctuator token in a conditional expression
} else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
do {
prevToken = sourceCode.getTokenBefore(prevToken);
} while (prevToken.type === 'Punctuator' && prevToken.value !== '/');
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
prevToken = prevToken.parent;
}
const peerElementIndent = getNodeIndent(node.parent.openingElement);
checkNodesIndent(node, peerElementIndent);
},
}
prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
const parentElementIndent = getNodeIndent(prevToken);
const indent = (
prevToken.loc.start.line === node.loc.start.line ||
isRightInLogicalExp(node) ||
isAlternateInConditionalExp(node)
) ? 0 : indentSize;
checkNodesIndent(node, parentElementIndent + indent);
}

function handleClosingElement(node) {
if (!node.parent) {
return;
}
const peerElementIndent = getNodeIndent(node.parent.openingElement || node.parent.openingFragment);
checkNodesIndent(node, peerElementIndent);
}

return {
JSXOpeningElement: handleOpeningElement,
JSXOpeningFragment: handleOpeningElement,
JSXClosingElement: handleClosingElement,
JSXClosingFragment: handleClosingElement,
JSXExpressionContainer: function(node) {
if (!node.parent) {
return;
Expand Down

0 comments on commit 95d3c3f

Please sign in to comment.