You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
256 lines
7.8 KiB
256 lines
7.8 KiB
/** |
|
* @fileoverview Enforce curly braces or disallow unnecessary curly brace in JSX |
|
* @author Jacky Ho |
|
* @author Simon Lydell |
|
*/ |
|
|
|
'use strict'; |
|
|
|
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', |
|
|
|
schema: [ |
|
{ |
|
oneOf: [ |
|
{ |
|
type: 'object', |
|
properties: { |
|
props: {enum: OPTION_VALUES}, |
|
children: {enum: OPTION_VALUES} |
|
}, |
|
additionalProperties: false |
|
}, |
|
{ |
|
enum: OPTION_VALUES |
|
} |
|
] |
|
} |
|
] |
|
}, |
|
|
|
create(context) { |
|
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 rawStringValue.includes('\\'); |
|
} |
|
|
|
function containsHTMLEntity(rawStringValue) { |
|
return /&[A-Za-z\d#]+;/.test(rawStringValue); |
|
} |
|
|
|
function containsDisallowedJSXTextChars(rawStringValue) { |
|
return /[{<>}]/.test(rawStringValue); |
|
} |
|
|
|
function containsQuoteCharacters(value) { |
|
return /['"]/.test(value); |
|
} |
|
|
|
function escapeDoubleQuotes(rawStringValue) { |
|
return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"'); |
|
} |
|
|
|
function escapeBackslashes(rawStringValue) { |
|
return rawStringValue.replace(/\\/g, '\\\\'); |
|
} |
|
|
|
function needToEscapeCharacterForJSX(raw) { |
|
return ( |
|
containsBackslash(raw) || |
|
containsHTMLEntity(raw) || |
|
containsDisallowedJSXTextChars(raw) |
|
); |
|
} |
|
|
|
function containsWhitespaceExpression(child) { |
|
if (child.type === 'JSXExpressionContainer') { |
|
const value = child.expression.value; |
|
return value ? !(/\S/.test(value)) : false; |
|
} |
|
return false; |
|
} |
|
|
|
/** |
|
* 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, |
|
message: 'Curly braces are unnecessary here.', |
|
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 { |
|
textToReplace = expressionType === 'TemplateLiteral' ? |
|
expression.quasis[0].value.cooked : expression.value; |
|
} |
|
|
|
return fixer.replaceText(JSXExpressionNode, textToReplace); |
|
} |
|
}); |
|
} |
|
|
|
function reportMissingCurly(literalNode) { |
|
context.report({ |
|
node: literalNode, |
|
message: 'Need to wrap this literal in a JSX expression.', |
|
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 ( |
|
containsHTMLEntity(literalNode.raw) || |
|
containsLineTerminators(literalNode.raw) |
|
) { |
|
return null; |
|
} |
|
|
|
const expression = literalNode.parent.type === 'JSXAttribute' ? |
|
`{"${escapeDoubleQuotes(escapeBackslashes( |
|
literalNode.raw.substring(1, literalNode.raw.length - 1) |
|
))}"}` : |
|
`{${JSON.stringify(literalNode.value)}}`; |
|
|
|
return fixer.replaceText(literalNode, expression); |
|
} |
|
}); |
|
} |
|
|
|
// 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; |
|
|
|
if ( |
|
(expressionType === 'Literal' || expressionType === 'JSXText') && |
|
typeof expression.value === 'string' && |
|
!needToEscapeCharacterForJSX(expression.raw) && ( |
|
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 && |
|
!needToEscapeCharacterForJSX(expression.quasis[0].value.raw) && ( |
|
jsxUtil.isJSX(JSXExpressionNode.parent) || |
|
!containsQuoteCharacters(expression.quasis[0].value.cooked) |
|
) |
|
) { |
|
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 shouldCheckForUnnecessaryCurly(parent, config) { |
|
// If there are more than one JSX child, there is no need to check for |
|
// unnecessary curly braces. |
|
if (jsxUtil.isJSX(parent) && parent.children.length !== 1) { |
|
return false; |
|
} |
|
|
|
if ( |
|
parent.children && |
|
parent.children.length === 1 && |
|
containsWhitespaceExpression(parent.children[0]) |
|
) { |
|
return false; |
|
} |
|
|
|
return areRuleConditionsSatisfied(parent, config, OPTION_NEVER); |
|
} |
|
|
|
function shouldCheckForMissingCurly(parent, config) { |
|
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, userConfig)) { |
|
lintUnnecessaryCurly(node); |
|
} |
|
}, |
|
|
|
'Literal, JSXText': (node) => { |
|
if (shouldCheckForMissingCurly(node.parent, userConfig)) { |
|
reportMissingCurly(node); |
|
} |
|
} |
|
}; |
|
} |
|
};
|
|
|