Skip to content

Commit

Permalink
Accounting for quotes
Browse files Browse the repository at this point in the history
  • Loading branch information
jackyho112 committed Aug 10, 2017
1 parent 3a03eea commit 78e2ee2
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 35 deletions.
85 changes: 52 additions & 33 deletions lib/rules/jsx-curly-brace-presence.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,20 @@ const OPTION_NEVER = 'never';
const OPTION_IGNORE = 'ignore';
const OPTION_SINGLE_QUOTE = 'single';
const OPTION_DOUBLE_QUOTE = 'double';
const optionValues = [OPTION_ALWAYS, OPTION_NEVER, OPTION_IGNORE];
const OPTION_ORIGINAL_QUOTE = 'original';
const optionValues = [
OPTION_ALWAYS,
`${OPTION_ALWAYS},${OPTION_SINGLE_QUOTE}`,
`${OPTION_ALWAYS},${OPTION_DOUBLE_QUOTE}`,
`${OPTION_ALWAYS},${OPTION_ORIGINAL_QUOTE}`,
OPTION_NEVER,
OPTION_IGNORE
];
const quoteOptionsValues = [OPTION_SINGLE_QUOTE, OPTION_DOUBLE_QUOTE];
const quoteOptionValueStore = {
[OPTION_SINGLE_QUOTE]: '\'',
[OPTION_DOUBLE_QUOTE]: '"'
};
const defaultConfig = {props: OPTION_NEVER, children: OPTION_NEVER};

module.exports = {
Expand All @@ -39,9 +51,6 @@ module.exports = {
},
{
enum: optionValues
},
{
enum: quoteOptionsValues
}
]
},
Expand All @@ -53,16 +62,18 @@ module.exports = {

create: function(context) {
const ruleOptions = context.options[0];
const userConfig = (
typeof ruleOptions === 'string' && optionValues.includes(ruleOptions) ?
{props: ruleOptions, children: ruleOptions} :
Object.assign({}, defaultConfig, ruleOptions)
);
const userConfig = typeof ruleOptions === 'string' ?
{props: ruleOptions, children: ruleOptions} :
Object.assign({}, defaultConfig, ruleOptions);

function containsBackslashForEscaping(rawStringValue) {
return JSON.stringify(rawStringValue).includes('\\');
}

function containsQuote(string) {
return string.match(/'|"/g);
}

/**
* Report an unnecessary curly brace violation on a node
* @param {ASTNode} node - The AST node with an unnecessary JSX expression
Expand All @@ -83,28 +94,39 @@ module.exports = {
});
}

function translateQuoteOptionToStringLiteral(option) {
return option === OPTION_SINGLE_QUOTE ? '\'' : '"';
}

function wrapLiteralNodeInJSXExpression(fixer, literalNode) {
let quoteStyle = translateQuoteOptionToStringLiteral(OPTION_DOUBLE_QUOTE);
const ruleOptionSlotOne = context.options[0];
const ruleOptionSlotTwo = context.options[1];
const newDefault = context.options[1];
const defaultQuoteOption = newDefault || OPTION_DOUBLE_QUOTE;
const {parent: {type: parentType}} = literalNode;
const {raw, value} = literalNode;
const valueContainsQuote = containsQuote(value);
let text = raw;
let userSetQuoteOption;

if (parentType === 'JSXAttribute') {
userSetQuoteOption = userConfig.props.split(',')[1];
text = raw.substring(1, raw.length - 1);
} else if (parentType === 'JSXElement') {
userSetQuoteOption = userConfig.children.split(',')[1];
}

if (ruleOptionSlotTwo) {
quoteStyle = translateQuoteOptionToStringLiteral(ruleOptionSlotTwo);
} else if (
typeof ruleOptionSlotOne === 'string' &&
quoteOptionsValues.includes(ruleOptionSlotOne)
if (
userSetQuoteOption === OPTION_ORIGINAL_QUOTE ||
(parentType === 'JSXAttribute' && valueContainsQuote)
) {
quoteStyle = translateQuoteOptionToStringLiteral(ruleOptionSlotOne);
return fixer.replaceText(literalNode, `{${raw}}`);
}

// The only possible case here is a string literal as a JSX child
if (parentType === 'JSXElement' && valueContainsQuote) {
return fixer.replaceText(literalNode, `{${JSON.stringify(value)}}`);
}

const quoteStyle = quoteOptionValueStore[
userSetQuoteOption || defaultQuoteOption
];
return fixer.replaceText(
literalNode,
`{${quoteStyle}${literalNode.raw}${quoteStyle}}`
`{${quoteStyle}${text}${quoteStyle}}`
);
}

Expand All @@ -113,13 +135,6 @@ module.exports = {
node: literalNode,
message: 'Need to wrap this literal in a JSX expression.',
fix: function(fixer) {
if (literalNode.parent.type === 'JSXAttribute') {
return [
fixer.insertTextBefore(literalNode, '{'),
fixer.insertTextAfter(literalNode, '}')
];
}

return wrapLiteralNodeInJSXExpression(fixer, literalNode);
}
});
Expand Down Expand Up @@ -148,9 +163,13 @@ module.exports = {

function areRuleConditionsSatisfied(parentType, config, ruleCondition) {
return (
parentType === 'JSXAttribute' && config.props === ruleCondition
parentType === 'JSXAttribute' &&
typeof config.props === 'string' &&
config.props.split(',')[0] === ruleCondition
) || (
parentType === 'JSXElement' && config.children === ruleCondition
parentType === 'JSXElement' &&
typeof config.children === 'string' &&
config.children.split(',')[0] === ruleCondition
);
}

Expand Down
136 changes: 134 additions & 2 deletions tests/lib/rules/jsx-curly-brace-presence.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ const unnecessaryCurlyMessage = 'Curly braces are unnecessary here.';
const ruleTester = new RuleTester({parserOptions});
ruleTester.run('jsx-curly-brace-presence', rule, {
valid: [
{
code: '<App>{\'foo "bar"\'}</App>',
options: [{children: 'never'}]
},
{
code: '<App>{\"foo \'bar\'\"}</App>',
options: [{children: 'never'}]
},
{
code: '<App prop={\'foo "bar"\'}>foo</App>',
options: [{props: 'never'}]
},
{
code: '<App prop={\'foo "bar"\'}>foo</App>',
options: [{props: 'never'}]
},
{
code: '<App {...props}>foo</App>'
},
Expand Down Expand Up @@ -180,7 +196,7 @@ ruleTester.run('jsx-curly-brace-presence', rule, {
},
{
code: '<MyComponent prop=\'bar\'>foo</MyComponent>',
output: '<MyComponent prop={\'bar\'}>foo</MyComponent>',
output: '<MyComponent prop={\"bar\"}>foo</MyComponent>',
options: [{props: 'always'}],
errors: [{message: missingCurlyMessage}]
},
Expand Down Expand Up @@ -212,11 +228,127 @@ ruleTester.run('jsx-curly-brace-presence', rule, {
},
{
code: '<MyComponent prop=\'bar\'>foo</MyComponent>',
output: '<MyComponent prop={\'bar\'}>{\"foo\"}</MyComponent>',
output: '<MyComponent prop={\"bar\"}>{\"foo\"}</MyComponent>',
options: ['always'],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
},
{
code: '<MyComponent prop=\'bar\'>foo</MyComponent>',
output: '<MyComponent prop={\'bar\'}>{\'foo\'}</MyComponent>',
options: ['always', 'single'],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
},
{
code: '<MyComponent prop=\'bar\'>foo</MyComponent>',
output: '<MyComponent prop={\"bar\"}>{\"foo\"}</MyComponent>',
options: ['always', 'double'],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
},
{
code: '<MyComponent prop=\'bar\'>foo</MyComponent>',
output: '<MyComponent prop={\"bar\"}>{\'foo\'}</MyComponent>',
options: [{props: 'always,double', children: 'always,single'}],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
},
{
code: '<MyComponent prop=\"bar\">foo</MyComponent>',
output: '<MyComponent prop={\'bar\'}>{\"foo\"}</MyComponent>',
options: [{props: 'always,single', children: 'always,double'}],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
},
{
code: '<MyComponent prop=\"bar\">foo</MyComponent>',
output: '<MyComponent prop={\'bar\'}>{\"foo\"}</MyComponent>',
options: [{props: 'always', children: 'always,double'}, 'single'],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
},
{
code: '<MyComponent prop=\"bar\">foo</MyComponent>',
output: '<MyComponent prop={\'bar\'}>{\'foo\'}</MyComponent>',
options: [{props: 'always', children: 'always'}, 'single'],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
},
{
code: '<MyComponent prop=\'bar\'>foo</MyComponent>',
output: '<MyComponent prop={\"bar\"}>{\"foo\"}</MyComponent>',
options: [{props: 'always,double', children: 'always,double'}, 'single'],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
},
{
code: '<MyComponent prop=\'bar\'>foo<App/></MyComponent>',
output: '<MyComponent prop={\"bar\"}>{\"foo\"}<App/></MyComponent>',
options: ['always', 'double'],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
},
{
code: '<MyComponent prop=\'bar\'><App/>foo</MyComponent>',
output: '<MyComponent prop={\"bar\"}><App/>{\"foo\"}</MyComponent>',
options: ['always', 'double'],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
},
{
code: '<MyComponent prop=\'bar\'>foo</MyComponent>',
output: '<MyComponent prop={\'bar\'}>foo</MyComponent>',
options: [{props: 'always,original'}],
errors: [{message: missingCurlyMessage}]
},
{
code: '<MyComponent prop=\"bar\">foo</MyComponent>',
output: '<MyComponent prop={\"bar\"}>foo</MyComponent>',
options: [{props: 'always,original'}],
errors: [{message: missingCurlyMessage}]
},
{
code: '<MyComponent prop=\'bar\"bar\"foo\'>foo"bar"</MyComponent>',
output: '<MyComponent prop={\"bar\\"bar\\"foo\"}>{\"foo\\"bar\\"\"}</MyComponent>',
options: ['always', 'single'],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
},
{
code: '<MyComponent prop=\'bar\"bar\"foo\'>foo"bar"</MyComponent>',
output: '<MyComponent prop={\"bar\\"bar\\"foo\"}>{\"foo\\"bar\\"\"}</MyComponent>',
options: ['always', 'double'],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
},
{
code: '<MyComponent prop="bar\'bar\'foo">foo\'bar\'</MyComponent>',
output: '<MyComponent prop={"bar\'bar\'foo"}>{"foo\'bar\'"}</MyComponent>',
options: ['always', 'single'],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
},
{
code: '<MyComponent prop="bar\'bar\'foo">foo\'bar\'</MyComponent>',
output: '<MyComponent prop={"bar\'bar\'foo"}>{"foo\'bar\'"}</MyComponent>',
options: ['always', 'double'],
errors: [
{message: missingCurlyMessage}, {message: missingCurlyMessage}
]
}
]
});

0 comments on commit 78e2ee2

Please sign in to comment.