Repository: yannickcr/eslint-plugin-react
Pull request #1349 (2017-08-07 20:23:32)

[new] [jsx-curly-brace-presence] Disallow unnecessary JSX expressions or enforce them
For issue https://github.com/yannickcr/eslint-plugin-react/issues/1310:

This rule checks for unnecessary JSX expressions when literals alone will do or enforce the presence of JSX expressions.

References: 
[String literals in JSX](https://facebook.github.io/react/docs/jsx-in-depth.html#string-literals)
[JSX gotchas](https://github.com/facebook/react/blob/v15.4.0-rc.3/docs/docs/02.3-jsx-gotchas.md#html-entities)


Patch:
diff --git a/README.md b/README.md
index 6bb08367..1d19113a 100644
--- a/README.md
+++ b/README.md
@@ -141,6 +141,7 @@ Finally, enable all of the rules that you would like to use.  Use [our preset](#
 * [react/jsx-no-literals](docs/rules/jsx-no-literals.md): Prevent usage of unwrapped JSX strings
 * [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md): Prevent usage of unsafe `target='_blank'`
 * [react/jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX
+* [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md): Enforce curly braces or disallow unnecessary curly braces in JSX
 * [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components
 * [react/jsx-sort-props](docs/rules/jsx-sort-props.md): Enforce props alphabetical sorting (fixable)
 * [react/jsx-space-before-closing](docs/rules/jsx-space-before-closing.md): Validate spacing before closing bracket in JSX (fixable)
diff --git a/docs/rules/jsx-curly-brace-presence.md b/docs/rules/jsx-curly-brace-presence.md
new file mode 100644
index 00000000..b4e27a2b
--- /dev/null
+++ b/docs/rules/jsx-curly-brace-presence.md
@@ -0,0 +1,150 @@
+# Enforce curly braces or disallow unnecessary curly braces in JSX props and/or children. (react/jsx-curly-brace-presence)
+
+This rule allows you to enforce curly braces or disallow unnecessary curly braces in JSX props and/or children.
+
+For situations where JSX expressions are unnecessary, please refer to [the React doc](https://facebook.github.io/react/docs/jsx-in-depth.html) and [this page about JSX gotchas](https://github.com/facebook/react/blob/v15.4.0-rc.3/docs/docs/02.3-jsx-gotchas.md#html-entities).
+
+**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line
+
+## Rule Details
+
+By default, this rule will check for and warn about unnecessary curly braces in both JSX props and children.
+
+You can pass in options to enforce the presence of curly braces on JSX props or children or both. The same options are available for not allowing unnecessary curly braces as well as ignoring the check.
+
+## Rule Options
+
+```js
+...
+"react/jsx-curly-brace-presence": [<enabled>, { "props": <string>, "children": <string> }]
+...
+```
+
+or alternatively
+
+```js
+...
+"react/jsx-curly-brace-presence": [<enabled>, <string>]
+...
+```
+
+### Valid options for <string>
+
+They are `always`, `never` and `ignore` for checking on JSX props and children.
+
+* `always`: always enforce curly braces inside JSX props or/and children
+* `never`: never allow unnecessary curly braces inside JSX props or/and children
+* `ignore`: ignore the rule for JSX props or/and children
+
+If passed in the option to fix, this is how a style violation will get fixed
+
+* `always`: wrap a JSX attribute in curly braces/JSX expression and/or a JSX child the same way but also with double quotes
+* `never`: get rid of curly braces from a JSX attribute and/or a JSX child
+
+- All fixing operations use double quotes.
+
+For examples:
+
+When `{ props: "always", children: "always" }` is set, the following patterns will be given warnings.
+
+```jsx
+<App>Hello world</App>;
+<App prop='Hello world'>{'Hello world'}</App>;
+```
+
+They can be fixed to:
+
+```jsx
+<App>{"Hello world"}</App>;
+<App prop={"Hello world"}>{'Hello world'}</App>;
+```
+
+When `{ props: "never", children: "never" }` is set, the following patterns will be given warnings.
+
+```jsx
+<App>{'Hello world'}</App>;
+<App prop={'Hello world'} attr={"foo"} />;
+```
+
+They can be fixed to:
+
+```jsx
+<App>Hello world</App>;
+<App prop="Hello world" attr="foo" />;
+```
+
+### Alternative syntax
+
+The options are also `always`, `never` and `ignore` for the same meanings.
+
+In this syntax, only a string is provided and the default will be set to that option for checking on both JSX props and children.
+
+For examples:
+
+When `'always'` is set, the following patterns will be given warnings.
+
+```jsx
+<App>Hello world</App>;
+<App prop='Hello world' attr="foo">Hello world</App>;
+```
+
+They can be fixed to:
+```jsx
+<App>{"Hello world"}</App>;
+<App prop={"Hello world"} attr={"foo"}>{"Hello world"}</App>;
+```
+
+When `'never'` is set, the following pattern will be given warnings.
+
+```jsx
+<App prop={'foo'} attr={"bar"}>{'Hello world'}</App>;
+```
+
+It can fixed to:
+
+```jsx
+<App prop="foo" attr="bar">Hello world</App>;
+```
+
+## Edge cases
+
+The fix also deals with template literals, strings with quotes, and strings with escapes characters.
+
+* If the rule is set to get rid of unnecessary curly braces and the template literal inside a JSX expression has no expression, it will throw a warning and be fixed with double quotes. For example:
+
+```jsx
+<App prop={`Hello world`}>{`Hello world`}</App>;
+```
+
+will be warned and fixed to:
+
+```jsx
+<App prop="Hello world">Hello world</App>;
+```
+
+* If the rule is set to enforce curly braces and the strings have quotes, it will be fixed with double quotes for JSX children and the normal way for JSX attributes.
+
+For example:
+
+
+```jsx
+<App prop='Hello "foo" world'>Hello 'foo' "bar" world</App>;
+```
+
+will warned and fixed to:
+
+```jsx
+<App prop={"Hello \"foo\" world"}>{"Hello 'foo' \"bar\" world"}</App>;
+```
+
+* If the rule is set to get rid of unnecessary curly braces and the strings have escaped characters, it will not warn or fix for JSX children because JSX expressions are necessary in this case. For instance:
+
+The following pattern will not be given a warning even if `'never'` is passed.
+
+```jsx
+<App>{"Hello \u00b7 world"}</App>;
+```
+
+## When Not To Use It
+
+You should turn this rule off if you are not concerned about maintaining consistency regarding the use of curly braces in JSX props and/or children as well as the use of unnecessary JSX expressions.
diff --git a/index.js b/index.js
index 4a1e118c..13b72fdd 100644
--- a/index.js
+++ b/index.js
@@ -28,6 +28,7 @@ const allRules = {
   'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
   'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'),
   'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
+  'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),
   'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),
   'jsx-sort-props': require('./lib/rules/jsx-sort-props'),
   'jsx-space-before-closing': require('./lib/rules/jsx-space-before-closing'),
diff --git a/lib/rules/boolean-prop-naming.js b/lib/rules/boolean-prop-naming.js
index 105b3efa..d8c1423f 100644
--- a/lib/rules/boolean-prop-naming.js
+++ b/lib/rules/boolean-prop-naming.js
@@ -180,7 +180,7 @@ module.exports = {
           return;
         }
         const component = utils.getRelatedComponent(node);
-        if (!component || !node.parent.right.properties) {
+        if (!component) {
           return;
         }
         validatePropNaming(component.node, node.parent.right.properties);
diff --git a/lib/rules/jsx-curly-brace-presence.js b/lib/rules/jsx-curly-brace-presence.js
new file mode 100644
index 00000000..51ef422b
--- /dev/null
+++ b/lib/rules/jsx-curly-brace-presence.js
@@ -0,0 +1,189 @@
+/**
+ * @fileoverview Enforce curly braces or disallow unnecessary curly brace in JSX
+ * @author Jacky Ho
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// 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
+    },
+    fixable: 'code',
+
+    schema: [
+      {
+        oneOf: [
+          {
+            type: 'object',
+            properties: {
+              props: {enum: OPTION_VALUES, default: OPTION_NEVER},
+              children: {enum: OPTION_VALUES, default: OPTION_NEVER}
+            },
+            additionalProperties: false
+          },
+          {
+            enum: OPTION_VALUES
+          }
+        ]
+      }
+    ]
+  },
+
+  create: function(context) {
+    const ruleOptions = context.options[0];
+    const userConfig = typeof ruleOptions === 'string' ?
+      {props: ruleOptions, children: ruleOptions} :
+      Object.assign({}, DEFAULT_CONFIG, ruleOptions);
+
+    function containsBackslashForEscaping(rawStringValue) {
+      return rawStringValue.includes('\\');
+    }
+
+    function escapeDoubleQuotes(rawStringValue) {
+      return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"');
+    }
+
+    /**
+     * Report and fix an unnecessary curly brace violation on a node
+     * @param {ASTNode} node - The AST node with an unnecessary JSX expression
+     * @param {String} text - The text to replace the unnecessary JSX expression
+     */
+    function reportUnnecessaryCurly(JSXExpressionNode) {
+      context.report({
+        node: JSXExpressionNode,
+        message: 'Curly braces are unnecessary here.',
+        fix: function(fixer) {
+          const expression = JSXExpressionNode.expression;
+          const expressionType = expression.type;
+          const parentType = JSXExpressionNode.parent.type;
+
+          let textToReplace;
+          if (parentType === 'JSXAttribute') {
+            textToReplace = `"${escapeDoubleQuotes(
+              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: function(fixer) {
+          const expression = literalNode.parent.type === 'JSXAttribute' ?
+            `{"${escapeDoubleQuotes(
+              literalNode.raw.substring(1, literalNode.raw.length - 1)
+            )}"}` :
+            `{${JSON.stringify(literalNode.value)}}`;
+
+          return fixer.replaceText(literalNode, expression);
+        }
+      });
+    }
+
+    function lintUnnecessaryCurly(JSXExpressionNode) {
+      const expression = JSXExpressionNode.expression;
+      const expressionType = expression.type;
+      const parentType = JSXExpressionNode.parent.type;
+
+      if (
+        expressionType === 'Literal' &&
+          typeof expression.value === 'string' && (
+          parentType === 'JSXAttribute' ||
+          !containsBackslashForEscaping(expression.raw))
+      ) {
+        reportUnnecessaryCurly(JSXExpressionNode);
+      } else if (
+        expressionType === 'TemplateLiteral' &&
+          expression.expressions.length === 0 && (
+          parentType === 'JSXAttribute' ||
+          !containsBackslashForEscaping(expression.quasis[0].value.raw))
+      ) {
+        reportUnnecessaryCurly(JSXExpressionNode);
+      }
+    }
+
+    function areRuleConditionsSatisfied(parentType, config, ruleCondition) {
+      return (
+        parentType === 'JSXAttribute' &&
+          typeof config.props === 'string' &&
+          config.props === ruleCondition
+      ) || (
+        parentType === 'JSXElement' &&
+          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) {
+        return false;
+      }
+
+      return areRuleConditionsSatisfied(parentType, config, OPTION_NEVER);
+    }
+
+    function shouldCheckForMissingCurly(parentType, config) {
+      return areRuleConditionsSatisfied(parentType, config, OPTION_ALWAYS);
+    }
+
+    // --------------------------------------------------------------------------
+    // Public
+    // --------------------------------------------------------------------------
+
+    return {
+      JSXExpressionContainer: node => {
+        const parent = node.parent;
+
+        if (shouldCheckForUnnecessaryCurly(parent, userConfig)) {
+          lintUnnecessaryCurly(node);
+        }
+      },
+
+      Literal: node => {
+        const parentType = node.parent.type;
+
+        if (shouldCheckForMissingCurly(parentType, userConfig)) {
+          reportMissingCurly(node);
+        }
+      }
+    };
+  }
+};
diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js
index 6f2a610b..32b83ed6 100644
--- a/lib/rules/prop-types.js
+++ b/lib/rules/prop-types.js
@@ -116,20 +116,6 @@ module.exports = {
       return false;
     }
 
-    /**
-    * Checks if a prop is being assigned a value props.bar = 'bar'
-    * @param {ASTNode} node The AST node being checked.
-    * @returns {Boolean}
-    */
-
-    function isAssignmentToProp(node) {
-      return (
-        node.parent &&
-        node.parent.type === 'AssignmentExpression' &&
-        node.parent.left === node
-      );
-    }
-
     /**
      * Checks if we are using a prop
      * @param {ASTNode} node The AST node being checked.
@@ -140,7 +126,7 @@ module.exports = {
         (utils.getParentES6Component() || utils.getParentES5Component()) &&
         node.object.type === 'ThisExpression' && node.property.name === 'props'
       );
-      const isStatelessFunctionUsage = node.object.name === 'props' && !isAssignmentToProp(node);
+      const isStatelessFunctionUsage = node.object.name === 'props';
       const isNextPropsUsage = node.object.name === 'nextProps' && inComponentWillReceiveProps();
       return isClassUsage || isStatelessFunctionUsage || isNextPropsUsage;
     }


Test patch:
diff --git a/tests/lib/rules/boolean-prop-naming.js b/tests/lib/rules/boolean-prop-naming.js
index 5ff5cc28..6945bc1b 100644
--- a/tests/lib/rules/boolean-prop-naming.js
+++ b/tests/lib/rules/boolean-prop-naming.js
@@ -101,16 +101,6 @@ ruleTester.run('boolean-prop-naming', rule, {
     options: [{
       rule: '^is[A-Z]([A-Za-z0-9]?)+'
     }]
-  }, {
-    code: [
-      'class Hello extends React.Component {',
-      '  render () { return <div />; }',
-      '}',
-      'Hello.propTypes = wrap({ a: PropTypes.bool })'
-    ].join('\n'),
-    options: [{
-      rule: '^is[A-Z]([A-Za-z0-9]?)+'
-    }]
   }, {
     // ES6 components as React.Component with non-boolean PropTypes
     code: [
diff --git a/tests/lib/rules/jsx-curly-brace-presence.js b/tests/lib/rules/jsx-curly-brace-presence.js
new file mode 100644
index 00000000..1ae02da4
--- /dev/null
+++ b/tests/lib/rules/jsx-curly-brace-presence.js
@@ -0,0 +1,393 @@
+/**
+ * @fileoverview Enforce curly braces or disallow unnecessary curly braces in JSX
+ * @author Jacky Ho
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/jsx-curly-brace-presence');
+const RuleTester = require('eslint').RuleTester;
+const parserOptions = {
+  sourceType: 'module',
+  ecmaFeatures: {
+    jsx: true
+  }
+};
+
+const missingCurlyMessage = 'Need to wrap this literal in a JSX expression.';
+const unnecessaryCurlyMessage = 'Curly braces are unnecessary here.';
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({parserOptions});
+ruleTester.run('jsx-curly-brace-presence', rule, {
+  valid: [
+    {
+      code: '<App {...props}>foo</App>'
+    },
+    {
+      code: '<App {...props}>foo</App>',
+      options: [{props: 'never'}]
+    },
+    {
+      code: '<App {...props}>foo</App>',
+      options: [{props: 'always'}]
+    },
+    {
+      code: '<App>{`Hello ${word} World`}</App>',
+      options: [{children: 'never'}]
+    },
+    {
+      code: '<App>{`Hello \\n World`}</App>',
+      options: [{children: 'never'}]
+    },
+    {
+      code: '<App>{`Hello ${word} World`}{`foo`}</App>',
+      options: [{children: 'never'}]
+    },
+    {
+      code: '<App prop={`foo ${word} bar`}>foo</App>',
+      options: [{props: 'never'}]
+    },
+    {
+      code: '<App prop={`foo ${word} bar`} />',
+      options: [{props: 'never'}]
+    },
+    {
+      code: '<App>{<myApp></myApp>}</App>'
+    },
+    {
+      code: '<App>{[]}</App>'
+    },
+    {
+      code: '<App>foo</App>'
+    },
+    {
+      code: '<App>{"foo"}{<Component>bar</Component>}</App>'
+    },
+    {
+      code: '<App prop=\'bar\'>foo</App>'
+    },
+    {
+      code: '<App prop={true}>foo</App>'
+    },
+    {
+      code: '<App prop>foo</App>'
+    },
+    {
+      code: '<App prop=\'bar\'>{\'foo \\n bar\'}</App>'
+    },
+    {
+      code: '<MyComponent prop=\'bar\'>foo</MyComponent>',
+      options: [{props: 'never'}]
+    },
+    {
+      code: '<MyComponent prop=\"bar\">foo</MyComponent>',
+      options: [{props: 'never'}]
+    },
+    {
+      code: '<MyComponent>foo</MyComponent>',
+      options: [{children: 'never'}]
+    },
+    {
+      code: '<MyComponent>{<App/>}{"123"}</MyComponent>',
+      options: [{children: 'never'}]
+    },
+    {
+      code: '<App>{\"foo \'bar\' \\\"foo\\\" bar\"}</App>',
+      options: [{children: 'never'}]
+    },
+    {
+      code: '<MyComponent prop={\'bar\'}>foo</MyComponent>',
+      options: [{props: 'always'}]
+    },
+    {
+      code: '<MyComponent>{\'foo\'}</MyComponent>',
+      options: [{children: 'always'}]
+    },
+    {
+      code: '<MyComponent prop={\"bar\"}>foo</MyComponent>',
+      options: [{props: 'always'}]
+    },
+    {
+      code: '<MyComponent>{\"foo\"}</MyComponent>',
+      options: [{children: 'always'}]
+    },
+    {
+      code: '<MyComponent>{\'foo\'}</MyComponent>',
+      options: [{children: 'ignore'}]
+    },
+    {
+      code: '<MyComponent prop={\'bar\'}>foo</MyComponent>',
+      options: [{props: 'ignore'}]
+    },
+    {
+      code: '<MyComponent>foo</MyComponent>',
+      options: [{children: 'ignore'}]
+    },
+    {
+      code: '<MyComponent prop=\'bar\'>foo</MyComponent>',
+      options: [{props: 'ignore'}]
+    },
+    {
+      code: '<MyComponent prop=\"bar\">foo</MyComponent>',
+      options: [{props: 'ignore'}]
+    },
+    {
+      code: '<MyComponent prop=\'bar\'>{\'foo\'}</MyComponent>',
+      options: [{children: 'always', props: 'never'}]
+    },
+    {
+      code: '<MyComponent prop={\'bar\'}>foo</MyComponent>',
+      options: [{children: 'never', props: 'always'}]
+    },
+    {
+      code: '<MyComponent prop={\'bar\'}>{\'foo\'}</MyComponent>',
+      options: ['always']
+    },
+    {
+      code: '<MyComponent prop={\"bar\"}>{\"foo\"}</MyComponent>',
+      options: ['always']
+    },
+    {
+      code: '<MyComponent prop={\"bar\"} attr={\'foo\'} />',
+      options: ['always']
+    },
+    {
+      code: '<MyComponent prop=\"bar\" attr=\'foo\' />',
+      options: ['never']
+    },
+    {
+      code: '<MyComponent prop=\'bar\'>foo</MyComponent>',
+      options: ['never']
+    },
+    {
+      code: '<MyComponent prop={`bar ${word} foo`}>{`foo ${word}`}</MyComponent>',
+      options: ['never']
+    }
+  ],
+
+  invalid: [
+    {
+      code: '<App prop={`foo`} />',
+      output: '<App prop="foo" />',
+      options: [{props: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<App prop={\'foo \\u00b7 bar\'}>foo</App>',
+      output: '<App prop=\"foo \\u00b7 bar\">foo</App>',
+      options: [{props: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<App prop={`foo \\n bar`}>foo</App>',
+      output: '<App prop="foo \\n bar">foo</App>',
+      options: [{props: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<App prop={`foo \\u00b7 bar`}>foo</App>',
+      output: '<App prop="foo \\u00b7 bar">foo</App>',
+      options: [{props: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<App prop={`foo \\\'foo\\\' bar`}>foo</App>',
+      output: '<App prop="foo \\\'foo\\\' bar">foo</App>',
+      options: [{props: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<App prop={`foo \\\"foo\\\" bar`}>foo</App>',
+      output: '<App prop="foo \\\"foo\\\" bar">foo</App>',
+      options: [{props: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<App prop={`foo "foo" bar`}>foo</App>',
+      output: '<App prop="foo \\\"foo\\\" bar">foo</App>',
+      options: [{props: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<App prop={`foo`}>foo</App>',
+      output: '<App prop="foo">foo</App>',
+      options: [{props: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<App>{`foo`}</App>',
+      output: '<App>foo</App>',
+      options: [{children: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<App>{`foo "foo" bar`}</App>',
+      output: '<App>foo "foo" bar</App>',
+      options: [{children: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<MyComponent>{\'foo\'}</MyComponent>',
+      output: '<MyComponent>foo</MyComponent>',
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<MyComponent prop={\'bar\'}>foo</MyComponent>',
+      output: '<MyComponent prop=\"bar\">foo</MyComponent>',
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<MyComponent>{\'foo\'}</MyComponent>',
+      output: '<MyComponent>foo</MyComponent>',
+      options: [{children: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<MyComponent prop={\'bar\'}>foo</MyComponent>',
+      output: '<MyComponent prop=\"bar\">foo</MyComponent>',
+      options: [{props: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<MyComponent prop={\"bar \'foo\' \"}>foo</MyComponent>',
+      output: '<MyComponent prop=\"bar \'foo\' \">foo</MyComponent>',
+      options: [{props: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<MyComponent prop={\'bar \"foo\" \'}>foo</MyComponent>',
+      output: '<MyComponent prop=\"bar \\\"foo\\\" \">foo</MyComponent>',
+      options: [{props: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<MyComponent prop=\'bar\'>foo</MyComponent>',
+      output: '<MyComponent prop={\"bar\"}>foo</MyComponent>',
+      options: [{props: 'always'}],
+      errors: [{message: missingCurlyMessage}]
+    },
+    {
+      code: '<MyComponent prop=\"foo \'bar\'\">foo</MyComponent>',
+      output: '<MyComponent prop={\"foo \'bar\'\"}>foo</MyComponent>',
+      options: [{props: 'always'}],
+      errors: [{message: missingCurlyMessage}]
+    },
+    {
+      code: '<MyComponent prop=\'foo "bar"\'>foo</MyComponent>',
+      output: '<MyComponent prop={\"foo \\\"bar\\\"\"}>foo</MyComponent>',
+      options: [{props: 'always'}],
+      errors: [{message: missingCurlyMessage}]
+    },
+    {
+      code: '<MyComponent prop=\"foo \'bar\'\">foo</MyComponent>',
+      output: '<MyComponent prop={\"foo \'bar\'\"}>foo</MyComponent>',
+      options: [{props: 'always'}],
+      errors: [{message: missingCurlyMessage}]
+    },
+    {
+      code: '<MyComponent>foo bar </MyComponent>',
+      output: '<MyComponent>{\"foo bar \"}</MyComponent>',
+      options: [{children: 'always'}],
+      errors: [{message: missingCurlyMessage}]
+    },
+    {
+      code: '<MyComponent>foo bar \'foo\'</MyComponent>',
+      output: '<MyComponent>{\"foo bar \'foo\'\"}</MyComponent>',
+      options: [{children: 'always'}],
+      errors: [{message: missingCurlyMessage}]
+    },
+    {
+      code: '<MyComponent>foo bar \"foo\"</MyComponent>',
+      output: '<MyComponent>{\"foo bar \\\"foo\\\"\"}</MyComponent>',
+      options: [{children: 'always'}],
+      errors: [{message: missingCurlyMessage}]
+    },
+    {
+      code: '<MyComponent>foo bar <App/></MyComponent>',
+      output: '<MyComponent>{\"foo bar \"}<App/></MyComponent>',
+      options: [{children: 'always'}],
+      errors: [{message: missingCurlyMessage}]
+    },
+    {
+      code: '<MyComponent>foo\\nbar</MyComponent>',
+      output: '<MyComponent>{\"foo\\\\nbar\"}</MyComponent>',
+      options: [{children: 'always'}],
+      errors: [{message: missingCurlyMessage}]
+    },
+    {
+      code: '<MyComponent prop={\'bar\'}>{\'foo\'}</MyComponent>',
+      output: '<MyComponent prop=\"bar\">foo</MyComponent>',
+      options: ['never'],
+      errors: [
+        {message: unnecessaryCurlyMessage}, {message: unnecessaryCurlyMessage}
+      ]
+    },
+    {
+      code: '<MyComponent prop=\'bar\'>foo</MyComponent>',
+      output: '<MyComponent prop={\"bar\"}>{\"foo\"}</MyComponent>',
+      options: ['always'],
+      errors: [
+        {message: missingCurlyMessage}, {message: missingCurlyMessage}
+      ]
+    },
+    {
+      code: '<App>{\'foo "bar"\'}</App>',
+      output: '<App>foo \"bar\"</App>',
+      options: [{children: 'never'}],
+      errors: [{message: unnecessaryCurlyMessage}]
+    },
+    {
+      code: '<App>{\"foo \'bar\'\"}</App>',
+      output: '<App>foo \'bar\'</App>',
+      errors: [{message: unnecessaryCurlyMessage}],
+      options: [{children: 'never'}]
+    },
+    {
+      code: '<App prop={\"foo \'bar\' foo \"}>foo</App>',
+      output: '<App prop=\"foo \'bar\' foo \">foo</App>',
+      errors: [{message: unnecessaryCurlyMessage}],
+      options: [{props: 'never'}]
+    },
+    {
+      code: '<App prop={\'foo "bar"\'}>foo</App>',
+      output: '<App prop=\"foo \\\"bar\\\"\">foo</App>',
+      errors: [{message: unnecessaryCurlyMessage}],
+      options: [{props: 'never'}]
+    },
+    {
+      code: '<App prop={\'foo\'} attr={\" foo \"} />',
+      output: '<App prop=\"foo\" attr=\" foo \" />',
+      errors: [
+        {message: unnecessaryCurlyMessage}, {message: unnecessaryCurlyMessage}
+      ],
+      options: [{props: 'never'}]
+    },
+    {
+      code: '<App prop=\'foo\' attr=\"bar\" />',
+      output: '<App prop={\"foo\"} attr={\"bar\"} />',
+      errors: [
+        {message: missingCurlyMessage}, {message: missingCurlyMessage}
+      ],
+      options: [{props: 'always'}]
+    },
+    {
+      code: '<App prop=\'foo\' attr={\"bar\"} />',
+      output: '<App prop={\"foo\"} attr={\"bar\"} />',
+      errors: [{message: missingCurlyMessage}],
+      options: [{props: 'always'}]
+    },
+    {
+      code: '<App prop={\'foo\'} attr=\'bar\' />',
+      output: '<App prop={\'foo\'} attr={\"bar\"} />',
+      errors: [{message: missingCurlyMessage}],
+      options: [{props: 'always'}]
+    }
+  ]
+});
diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js
index 660caa5e..bc6ec6fe 100644
--- a/tests/lib/rules/prop-types.js
+++ b/tests/lib/rules/prop-types.js
@@ -1557,18 +1557,7 @@ ruleTester.run('prop-types', rule, {
         '}'
       ].join('\n'),
       parser: 'babel-eslint'
-    },
-    // issue #1288
-    `function Foo() {
-      const props = {}
-      props.bar = 'bar'
-      return <div {...props} />
-    }`,
-    // issue #1288
-    `function Foo(props) {
-      props.bar = 'bar';
-      return <div {...props} />;
-    }`
+    }
   ],
 
   invalid: [