Repository: yannickcr/eslint-plugin-react (js)

Issue:
Rule proposal: ensure style prop on DOM element tags is an object
React will issue a warning in dev if you pass a non-object to the `style` prop on DOM element tags

https://github.com/facebook/react/blob/85dcbf83/src/renderers/dom/shared/ReactDOMComponent.js#L204-L207

This should be easy enough to enforce for most cases with a linter rule.

Bad:

```jsx
<div style="foo" />
```

```jsx
<div style={true} />
```

```jsx
const styles = true;
<div style={styles} />
```

Good:

```jsx
<div style={{ color: 'red' }} />
```

```jsx
const styles = { color: 'red' };
<div style={styles} />
```

It would be even nicer if it validated that the values of the style object are all strings.

Pull request description:
Add new dom-elements-style-is-object rule (Fixes #715)
Fixes #715

Patch:
diff --git a/README.md b/README.md
index 3a9cdf9e..0ea49a94 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,7 @@ Finally, enable all of the rules that you would like to use.  Use [our preset](#
 * [react/self-closing-comp](docs/rules/self-closing-comp.md): Prevent extra closing tags for components without children (fixable)
 * [react/sort-comp](docs/rules/sort-comp.md): Enforce component methods order
 * [react/sort-prop-types](docs/rules/sort-prop-types.md): Enforce propTypes declarations alphabetical sorting
+* [react/style-prop-object](docs/rules/style-prop-object.md): Enforce style prop value being an object
 
 ## JSX-specific rules
 
diff --git a/docs/rules/style-prop-object.md b/docs/rules/style-prop-object.md
new file mode 100644
index 00000000..648c3056
--- /dev/null
+++ b/docs/rules/style-prop-object.md
@@ -0,0 +1,51 @@
+# Enforce style prop value being an object (style-prop-object)
+
+Require that the value of the prop `style` be an object or a variable that is
+an object.
+
+## Rule Details
+
+The following patterns are considered warnings:
+
+```jsx
+<div style="color: 'red'" />
+
+<div style={true} />
+
+<Hello style={true} />
+
+const styles = true;
+<div style={styles} />
+```
+
+```js
+React.createElement("div", { style: "color: 'red'" });
+
+React.createElement("div", { style: true });
+
+React.createElement("Hello", { style: true });
+
+const styles = true;
+React.createElement("div", { style: styles });
+```
+
+
+The following patterns are not considered warnings:
+
+```jsx
+<div style={{ color: "red" }} />
+
+<Hello style={{ color: "red" }} />
+
+const styles = { color: "red" };
+<div style={styles} />
+```
+
+```js
+React.createElement("div", { style: { color: 'red' }});
+
+React.createElement("Hello", { style: { color: 'red' }});
+
+const styles = { height: '100px' };
+React.createElement("div", { style: styles });
+```
diff --git a/index.js b/index.js
index 88ab4dcb..8d221d2c 100644
--- a/index.js
+++ b/index.js
@@ -54,7 +54,8 @@ var rules = {
   'jsx-filename-extension': require('./lib/rules/jsx-filename-extension'),
   'require-optimization': require('./lib/rules/require-optimization'),
   'no-find-dom-node': require('./lib/rules/no-find-dom-node'),
-  'no-danger-with-children': require('./lib/rules/no-danger-with-children')
+  'no-danger-with-children': require('./lib/rules/no-danger-with-children'),
+  'style-prop-object': require('./lib/rules/style-prop-object')
 };
 
 var ruleNames = Object.keys(rules);
diff --git a/lib/rules/style-prop-object.js b/lib/rules/style-prop-object.js
new file mode 100644
index 00000000..11c9fb1e
--- /dev/null
+++ b/lib/rules/style-prop-object.js
@@ -0,0 +1,80 @@
+/**
+ * @fileoverview Enforce style prop value is an object
+ * @author David Petersen
+ */
+'use strict';
+
+var variableUtil = require('../util/variable');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Enforce style prop value is an object',
+      category: '',
+      recommended: false
+    },
+    schema: []
+  },
+
+  create: function(context) {
+    /**
+     * @param {object} node A Identifier node
+     */
+    function checkIdentifiers(node) {
+      var variable = variableUtil.variablesInScope(context).find(function (item) {
+        return item.name === node.name;
+      });
+
+      if (!variable || !variable.defs[0].node.init) {
+        return;
+      }
+
+      if (variable.defs[0].node.init.type === 'Literal') {
+        context.report(node, 'Style prop value must be an object');
+      }
+    }
+
+    return {
+      CallExpression: function(node) {
+        if (
+          node.callee
+          && node.callee.type === 'MemberExpression'
+          && node.callee.property.name === 'createElement'
+          && node.arguments.length > 1
+        ) {
+          if (node.arguments[1].type === 'ObjectExpression') {
+            var style = node.arguments[1].properties.find(function(property) {
+              return property.key.name === 'style';
+            });
+            if (style) {
+              if (style.value.type === 'Identifier') {
+                checkIdentifiers(style.value);
+              } else if (style.value.type === 'Literal') {
+                context.report(style.value, 'Style prop value must be an object');
+              }
+            }
+          }
+        }
+      },
+
+      JSXAttribute: function(node) {
+        if (node.name.name !== 'style') {
+          return;
+        }
+
+        if (
+          node.value.type !== 'JSXExpressionContainer'
+          || node.value.expression.type === 'Literal'
+        ) {
+          context.report(node, 'Style prop value must be an object');
+        } else if (node.value.expression.type === 'Identifier') {
+          checkIdentifiers(node.value.expression);
+        }
+      }
+    };
+  }
+};


Test patch:
diff --git a/tests/lib/rules/style-prop-object.js b/tests/lib/rules/style-prop-object.js
new file mode 100644
index 00000000..eda99667
--- /dev/null
+++ b/tests/lib/rules/style-prop-object.js
@@ -0,0 +1,184 @@
+/**
+ * @fileoverview Enforce style prop value is an object
+ * @author David Petersen
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/style-prop-object');
+var RuleTester = require('eslint').RuleTester;
+
+var parserOptions = {
+  ecmaVersion: 6,
+  ecmaFeatures: {
+    experimentalObjectRestSpread: true,
+    jsx: true
+  }
+};
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester();
+ruleTester.run('style-prop-object', rule, {
+  valid: [
+    {
+      code: '<div style={{ color: "red" }} />',
+      parserOptions: parserOptions
+    },
+    {
+      code: '<Hello style={{ color: "red" }} />',
+      parserOptions: parserOptions
+    },
+    {
+      code: [
+        'function redDiv() {',
+        '  const styles = { color: "red" };',
+        '  return <div style={styles} />;',
+        '}'
+      ].join('\n'),
+      parserOptions: parserOptions
+    },
+    {
+      code: [
+        'function redDiv() {',
+        '  const styles = { color: "red" };',
+        '  return <Hello style={styles} />;',
+        '}'
+      ].join('\n'),
+      parserOptions: parserOptions
+    },
+    {
+      code: [
+        'const styles = { color: "red" };',
+        'function redDiv() {',
+        '  return <div style={styles} />;',
+        '}'
+      ].join('\n'),
+      parserOptions: parserOptions
+    },
+    {
+      code: [
+        'function redDiv(props) {',
+        '  return <div style={props.styles} />;',
+        '}'
+      ].join('\n'),
+      parserOptions: parserOptions
+    },
+    {
+      code: [
+        'import styles from \'./styles\';',
+        'function redDiv() {',
+        '  return <div style={styles} />;',
+        '}'
+      ].join('\n'),
+      parserOptions: Object.assign({sourceType: 'module'}, parserOptions)
+    },
+    {
+      code: [
+        'import mystyles from \'./styles\';',
+        'const styles = Object.assign({ color: \'red\' }, mystyles);',
+        'function redDiv() {',
+        '  return <div style={styles} />;',
+        '}'
+      ].join('\n'),
+      parserOptions: Object.assign({sourceType: 'module'}, parserOptions)
+    },
+    {
+      code: [
+        'const otherProps = { style: { color: "red" } };',
+        'const { a, b, ...props } = otherProps;',
+        '<div {...props} />'
+      ].join('\n'),
+      parserOptions: parserOptions
+    },
+    {
+      code: [
+        'const styles = Object.assign({ color: \'red\' }, mystyles);',
+        'React.createElement("div", { style: styles });'
+      ].join('\n'),
+      parserOptions: Object.assign({sourceType: 'module'}, parserOptions)
+    }
+  ],
+  invalid: [
+    {
+      code: '<div style="color: \'red\'" />',
+      errors: [{
+        message: 'Style prop value must be an object',
+        line: 1,
+        column: 6,
+        type: 'JSXAttribute'
+      }],
+      parserOptions: parserOptions
+    },
+    {
+      code: '<Hello style="color: \'red\'" />',
+      errors: [{
+        message: 'Style prop value must be an object',
+        line: 1,
+        column: 8,
+        type: 'JSXAttribute'
+      }],
+      parserOptions: parserOptions
+    },
+    {
+      code: '<div style={true} />',
+      errors: [{
+        message: 'Style prop value must be an object',
+        line: 1,
+        column: 6,
+        type: 'JSXAttribute'
+      }],
+      parserOptions: parserOptions
+    },
+    {
+      code: [
+        'const styles = \'color: "red"\';',
+        'function redDiv2() {',
+        '  return <div style={styles} />;',
+        '}'
+      ].join('\n'),
+      errors: [{
+        message: 'Style prop value must be an object',
+        line: 3,
+        column: 22,
+        type: 'Identifier'
+      }],
+      parserOptions: parserOptions
+    },
+    {
+      code: [
+        'const styles = \'color: "red"\';',
+        'function redDiv2() {',
+        '  return <Hello style={styles} />;',
+        '}'
+      ].join('\n'),
+      errors: [{
+        message: 'Style prop value must be an object',
+        line: 3,
+        column: 24,
+        type: 'Identifier'
+      }],
+      parserOptions: parserOptions
+    },
+    {
+      code: [
+        'const styles = true;',
+        'function redDiv() {',
+        '  return <div style={styles} />;',
+        '}'
+      ].join('\n'),
+      errors: [{
+        message: 'Style prop value must be an object',
+        line: 3,
+        column: 22,
+        type: 'Identifier'
+      }],
+      parserOptions: parserOptions
+    }
+  ]
+});