'use strict'; var utils = require('../utils.js'); require('eslint-visitor-keys'); require('espree'); require('estraverse'); const OPTION_ALWAYS = "always"; const OPTION_NEVER = "never"; const OPTION_IGNORE = "ignore"; const OPTION_VALUES = [ OPTION_ALWAYS, OPTION_NEVER, OPTION_IGNORE ]; const DEFAULT_CONFIG = { props: OPTION_NEVER, children: OPTION_NEVER, propElementValues: OPTION_IGNORE }; const messages = { unnecessaryCurly: "Curly braces are unnecessary here.", missingCurly: "Need to wrap this literal in a JSX expression." }; var jsxCurlyBracePresence = utils.createRule({ name: "jsx-curly-brace-presence", package: "jsx", meta: { type: "layout", docs: { description: "Disallow unnecessary JSX expressions when literals alone are sufficient or enforce JSX expressions on literals in JSX children or attributes" }, fixable: "code", messages, schema: [ { anyOf: [ { type: "object", properties: { props: { type: "string", enum: OPTION_VALUES }, children: { type: "string", enum: OPTION_VALUES }, propElementValues: { type: "string", enum: OPTION_VALUES } }, additionalProperties: false }, { type: "string", enum: OPTION_VALUES } ] } ] }, create(context) { const HTML_ENTITY_REGEX = () => /&[A-Z\d#]+;/gi; const ruleOptions = context.options[0]; const userConfig = typeof ruleOptions === "string" ? { props: ruleOptions, children: ruleOptions, propElementValues: OPTION_IGNORE } : Object.assign({}, DEFAULT_CONFIG, ruleOptions); function containsLineTerminators(rawStringValue) { return /[\n\r\u2028\u2029]/.test(rawStringValue); } function containsBackslash(rawStringValue) { return rawStringValue.includes("\\"); } function containsHTMLEntity(rawStringValue) { return HTML_ENTITY_REGEX().test(rawStringValue); } function containsOnlyHtmlEntities(rawStringValue) { return rawStringValue.replace(HTML_ENTITY_REGEX(), "").trim() === ""; } function containsDisallowedJSXTextChars(rawStringValue) { return /[{<>}]/.test(rawStringValue); } function containsQuoteCharacters(value) { return /['"]/.test(value); } function containsMultilineComment(value) { return /\/\*/.test(value); } function escapeDoubleQuotes(rawStringValue) { return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"'); } function escapeBackslashes(rawStringValue) { return rawStringValue.replace(/\\/g, "\\\\"); } function needToEscapeCharacterForJSX(raw, node) { return containsBackslash(raw) || containsHTMLEntity(raw) || node.parent.type !== "JSXAttribute" && containsDisallowedJSXTextChars(raw); } function containsWhitespaceExpression(child) { if (child.type === "JSXExpressionContainer") { const value = child.expression.value; return value ? utils.isWhiteSpaces(value) : false; } return false; } function isLineBreak(text) { return containsLineTerminators(text) && text.trim() === ""; } function wrapNonHTMLEntities(text) { const HTML_ENTITY = ""; const withCurlyBraces = text.split(HTML_ENTITY_REGEX()).map((word) => word === "" ? "" : `{${JSON.stringify(word)}}`).join(HTML_ENTITY); const htmlEntities = text.match(HTML_ENTITY_REGEX()); return htmlEntities.reduce((acc, htmlEntity) => acc.replace(HTML_ENTITY, htmlEntity), withCurlyBraces); } function wrapWithCurlyBraces(rawText) { if (!containsLineTerminators(rawText)) return `{${JSON.stringify(rawText)}}`; return rawText.split("\n").map((line) => { if (line.trim() === "") return line; const firstCharIndex = line.search(/\S/); const leftWhitespace = line.slice(0, firstCharIndex); const text = line.slice(firstCharIndex); if (containsHTMLEntity(line)) return `${leftWhitespace}${wrapNonHTMLEntities(text)}`; return `${leftWhitespace}{${JSON.stringify(text)}}`; }).join("\n"); } function reportUnnecessaryCurly(JSXExpressionNode) { context.report({ messageId: "unnecessaryCurly", node: JSXExpressionNode, fix(fixer) { const expression = JSXExpressionNode.expression; let textToReplace; if (utils.isJSX(expression)) { const sourceCode = context.sourceCode; textToReplace = sourceCode.getText(expression); } else { const parentType = JSXExpressionNode.parent.type; if (parentType === "JSXAttribute") { textToReplace = `"${expression.type === "TemplateLiteral" ? expression.quasis[0].value.raw : expression.raw.slice(1, -1)}"`; } else if (utils.isJSX(expression)) { const sourceCode = context.sourceCode; textToReplace = sourceCode.getText(expression); } else { textToReplace = expression.type === "TemplateLiteral" ? expression.quasis[0].value.cooked : expression.value; } } return fixer.replaceText(JSXExpressionNode, textToReplace); } }); } function reportMissingCurly(literalNode) { context.report({ messageId: "missingCurly", node: literalNode, fix(fixer) { if (utils.isJSX(literalNode)) return fixer.replaceText(literalNode, `{${context.sourceCode.getText(literalNode)}}`); if (containsOnlyHtmlEntities(literalNode.raw) || literalNode.parent.type === "JSXAttribute" && containsLineTerminators(literalNode.raw) || isLineBreak(literalNode.raw)) { return null; } const expression = literalNode.parent.type === "JSXAttribute" ? `{"${escapeDoubleQuotes(escapeBackslashes( literalNode.raw.slice(1, -1) ))}"}` : wrapWithCurlyBraces(literalNode.raw); return fixer.replaceText(literalNode, expression); } }); } function isWhiteSpaceLiteral(node) { return node.type && node.type === "Literal" && node.value && utils.isWhiteSpaces(node.value); } function isStringWithTrailingWhiteSpaces(value) { return /^\s|\s$/.test(value); } function isLiteralWithTrailingWhiteSpaces(node) { return node.type && node.type === "Literal" && node.value && isStringWithTrailingWhiteSpaces(node.value); } function lintUnnecessaryCurly(JSXExpressionNode) { const expression = JSXExpressionNode.expression; const expressionType = expression.type; const sourceCode = context.sourceCode; if (sourceCode.getCommentsInside && sourceCode.getCommentsInside(JSXExpressionNode).length > 0) return; if ((expressionType === "Literal" || expressionType === "JSXText") && typeof expression.value === "string" && (JSXExpressionNode.parent.type === "JSXAttribute" && !isWhiteSpaceLiteral(expression) || !isLiteralWithTrailingWhiteSpaces(expression)) && !containsMultilineComment(expression.value) && !needToEscapeCharacterForJSX(expression.raw, JSXExpressionNode) && (utils.isJSX(JSXExpressionNode.parent) || !containsQuoteCharacters(expression.value))) { reportUnnecessaryCurly(JSXExpressionNode); } else if (expressionType === "TemplateLiteral" && expression.expressions.length === 0 && !expression.quasis[0].value.raw.includes("\n") && !isStringWithTrailingWhiteSpaces(expression.quasis[0].value.raw) && !needToEscapeCharacterForJSX(expression.quasis[0].value.raw, JSXExpressionNode) && !containsQuoteCharacters(expression.quasis[0].value.cooked)) { reportUnnecessaryCurly(JSXExpressionNode); } else if (utils.isJSX(expression)) { reportUnnecessaryCurly(JSXExpressionNode); } } function areRuleConditionsSatisfied(parent, config, ruleCondition) { return parent.type === "JSXAttribute" && typeof config.props === "string" && config.props === ruleCondition || utils.isJSX(parent) && typeof config.children === "string" && config.children === ruleCondition; } function getAdjacentSiblings(node, children) { for (let i = 1; i < children.length - 1; i++) { const child = children[i]; if (node === child) return [children[i - 1], children[i + 1]]; } if (node === children[0] && children[1]) return [children[1]]; if (node === children[children.length - 1] && children[children.length - 2]) return [children[children.length - 2]]; return []; } function hasAdjacentJsxExpressionContainers(node, children) { if (!children) return false; const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child)); const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral); return adjSiblings.some((x) => x.type && x.type === "JSXExpressionContainer"); } function hasAdjacentJsx(node, children) { if (!children) return false; const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child)); const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral); return adjSiblings.some((x) => x.type && ["JSXExpressionContainer", "JSXElement"].includes(x.type)); } function shouldCheckForUnnecessaryCurly(node, config) { const parent = node.parent; if (parent.type && parent.type === "JSXAttribute" && (node.expression && node.expression.type && node.expression.type !== "Literal" && node.expression.type !== "StringLiteral" && node.expression.type !== "TemplateLiteral")) { return false; } if (utils.isJSX(parent) && hasAdjacentJsxExpressionContainers(node, parent.children)) return false; if (containsWhitespaceExpression(node) && hasAdjacentJsx(node, parent.children)) return false; if (parent.children && parent.children.length === 1 && containsWhitespaceExpression(node)) { return false; } return areRuleConditionsSatisfied(parent, config, OPTION_NEVER); } function shouldCheckForMissingCurly(node, config) { if (utils.isJSX(node)) return config.propElementValues !== OPTION_IGNORE; if (isLineBreak(node.raw) || containsOnlyHtmlEntities(node.raw)) { return false; } const parent = node.parent; if (parent.children && parent.children.length === 1 && containsWhitespaceExpression(parent.children[0])) { return false; } return areRuleConditionsSatisfied(parent, config, OPTION_ALWAYS); } return { "JSXAttribute > JSXExpressionContainer > JSXElement": function(node) { if (userConfig.propElementValues === OPTION_NEVER) reportUnnecessaryCurly(node.parent); }, JSXExpressionContainer(node) { if (shouldCheckForUnnecessaryCurly(node, userConfig)) lintUnnecessaryCurly(node); }, "JSXAttribute > JSXElement, Literal, JSXText": function(node) { if (shouldCheckForMissingCurly(node, userConfig)) reportMissingCurly(node); } }; } }); module.exports = jsxCurlyBracePresence;