123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- /**
- * @fileoverview Enforce curly braces or disallow unnecessary curly brace in JSX
- * @author Jacky Ho
- * @author Simon Lydell
- */
- 'use strict';
- const arrayIncludes = require('array-includes');
- const docsUrl = require('../util/docsUrl');
- const jsxUtil = require('../util/jsx');
- // ------------------------------------------------------------------------------
- // Constants
- // ------------------------------------------------------------------------------
- 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};
- // ------------------------------------------------------------------------------
- // Rule Definition
- // ------------------------------------------------------------------------------
- module.exports = {
- meta: {
- docs: {
- description:
- 'Disallow unnecessary JSX expressions when literals alone are sufficient '
- + 'or enfore JSX expressions on literals in JSX children or attributes',
- category: 'Stylistic Issues',
- recommended: false,
- url: docsUrl('jsx-curly-brace-presence')
- },
- fixable: 'code',
- messages: {
- unnecessaryCurly: 'Curly braces are unnecessary here.',
- missingCurly: 'Need to wrap this literal in a JSX expression.'
- },
- schema: [
- {
- oneOf: [
- {
- type: 'object',
- properties: {
- props: {enum: OPTION_VALUES},
- children: {enum: OPTION_VALUES}
- },
- additionalProperties: false
- },
- {
- enum: OPTION_VALUES
- }
- ]
- }
- ]
- },
- create(context) {
- const HTML_ENTITY_REGEX = () => /&[A-Za-z\d#]+;/g;
- const ruleOptions = context.options[0];
- const userConfig = typeof ruleOptions === 'string'
- ? {props: ruleOptions, children: ruleOptions}
- : Object.assign({}, DEFAULT_CONFIG, ruleOptions);
- function containsLineTerminators(rawStringValue) {
- return /[\n\r\u2028\u2029]/.test(rawStringValue);
- }
- function containsBackslash(rawStringValue) {
- return arrayIncludes(rawStringValue, '\\');
- }
- 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 ? jsxUtil.isWhiteSpaces(value) : false;
- }
- return false;
- }
- function isLineBreak(text) {
- return containsLineTerminators(text) && text.trim() === '';
- }
- function wrapNonHTMLEntities(text) {
- const HTML_ENTITY = '<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, htmlEntitiy) => (
- acc.replace(HTML_ENTITY, htmlEntitiy)
- ), 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');
- }
- /**
- * Report and fix an unnecessary curly brace violation on a node
- * @param {ASTNode} JSXExpressionNode - The AST node with an unnecessary JSX expression
- */
- function reportUnnecessaryCurly(JSXExpressionNode) {
- context.report({
- node: JSXExpressionNode,
- messageId: 'unnecessaryCurly',
- fix(fixer) {
- const expression = JSXExpressionNode.expression;
- const expressionType = expression.type;
- const parentType = JSXExpressionNode.parent.type;
- let textToReplace;
- if (parentType === 'JSXAttribute') {
- textToReplace = `"${expressionType === 'TemplateLiteral'
- ? expression.quasis[0].value.raw
- : expression.raw.substring(1, expression.raw.length - 1)
- }"`;
- } else if (jsxUtil.isJSX(expression)) {
- const sourceCode = context.getSourceCode();
- textToReplace = sourceCode.getText(expression);
- } else {
- textToReplace = expressionType === 'TemplateLiteral'
- ? expression.quasis[0].value.cooked : expression.value;
- }
- return fixer.replaceText(JSXExpressionNode, textToReplace);
- }
- });
- }
- function reportMissingCurly(literalNode) {
- context.report({
- node: literalNode,
- messageId: 'missingCurly',
- fix(fixer) {
- // If a HTML entity name is found, bail out because it can be fixed
- // by either using the real character or the unicode equivalent.
- // If it contains any line terminator character, bail out as well.
- 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.substring(1, literalNode.raw.length - 1)
- ))}"}`
- : wrapWithCurlyBraces(literalNode.raw);
- return fixer.replaceText(literalNode, expression);
- }
- });
- }
- function isWhiteSpaceLiteral(node) {
- return node.type && node.type === 'Literal' && node.value && jsxUtil.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);
- }
- // Bail out if there is any character that needs to be escaped in JSX
- // because escaping decreases readiblity and the original code may be more
- // readible anyway or intentional for other specific reasons
- function lintUnnecessaryCurly(JSXExpressionNode) {
- const expression = JSXExpressionNode.expression;
- const expressionType = expression.type;
- // Curly braces containing comments are necessary
- if (context.getSourceCode().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) && (
- jsxUtil.isJSX(JSXExpressionNode.parent)
- || !containsQuoteCharacters(expression.value)
- )
- ) {
- reportUnnecessaryCurly(JSXExpressionNode);
- } else if (
- expressionType === 'TemplateLiteral'
- && expression.expressions.length === 0
- && expression.quasis[0].value.raw.indexOf('\n') === -1
- && !isStringWithTrailingWhiteSpaces(expression.quasis[0].value.raw)
- && !needToEscapeCharacterForJSX(expression.quasis[0].value.raw, JSXExpressionNode) && (
- jsxUtil.isJSX(JSXExpressionNode.parent)
- || !containsQuoteCharacters(expression.quasis[0].value.cooked)
- )
- ) {
- reportUnnecessaryCurly(JSXExpressionNode);
- } else if (jsxUtil.isJSX(expression)) {
- reportUnnecessaryCurly(JSXExpressionNode);
- }
- }
- function areRuleConditionsSatisfied(parent, config, ruleCondition) {
- return (
- parent.type === 'JSXAttribute'
- && typeof config.props === 'string'
- && config.props === ruleCondition
- ) || (
- jsxUtil.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 && arrayIncludes(['JSXExpressionContainer', 'JSXElement'], x.type));
- }
- function shouldCheckForUnnecessaryCurly(parent, node, config) {
- // Bail out if the parent is a JSXAttribute & its contents aren't
- // StringLiteral or TemplateLiteral since e.g
- // <App prop1={<CustomEl />} prop2={<CustomEl>...</CustomEl>} />
- 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 there are adjacent `JsxExpressionContainer` then there is no need,
- // to check for unnecessary curly braces.
- if (jsxUtil.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 (
- 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);
- }
- // --------------------------------------------------------------------------
- // Public
- // --------------------------------------------------------------------------
- return {
- JSXExpressionContainer: (node) => {
- if (shouldCheckForUnnecessaryCurly(node.parent, node, userConfig)) {
- lintUnnecessaryCurly(node);
- }
- },
- 'Literal, JSXText': (node) => {
- if (shouldCheckForMissingCurly(node, userConfig)) {
- reportMissingCurly(node);
- }
- }
- };
- }
- };
|