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

Issue:
It does not detect when I'm mutating the state directly.
There's no error (or warning) for when I mutate the state instead of using setState.

so:
`this.state.foo = "bar"`
should yield an error, as opposed to
`this.setState({foo: "bar"});`

Pull request description:
Add no-direct-mutation-state rule (fixes #133, #201)
Rule to call out direct state mutation.

First eslint rule I have written and am very open to feedback to make it better. Its very simple right now.

Patch:
diff --git a/README.md b/README.md
index cc04caa9..5a4230ed 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,6 @@ Finally, enable all of the rules that you would like to use.
     "react/jsx-indent-props": 1,
     "react/jsx-max-props-per-line": 1,
     "react/jsx-no-duplicate-props": 1,
-    "react/jsx-no-literals": 1,
     "react/jsx-no-undef": 1,
     "react/jsx-quotes": 1,
     "react/jsx-sort-prop-types": 1,
@@ -63,6 +62,7 @@ Finally, enable all of the rules that you would like to use.
     "react/no-danger": 1,
     "react/no-did-mount-set-state": 1,
     "react/no-did-update-set-state": 1,
+    "react/no-direct-mutation-state": 1,
     "react/no-multi-comp": 1,
     "react/no-set-state": 1,
     "react/no-unknown-property": 1,
@@ -95,6 +95,7 @@ Finally, enable all of the rules that you would like to use.
 * [no-danger](docs/rules/no-danger.md): Prevent usage of dangerous JSX properties
 * [no-did-mount-set-state](docs/rules/no-did-mount-set-state.md): Prevent usage of `setState` in `componentDidMount`
 * [no-did-update-set-state](docs/rules/no-did-update-set-state.md): Prevent usage of `setState` in `componentDidUpdate`
+* [no-direct-mutation-state](docs/rules/no-direct-mutation-state.md): Prevent direct mutation of `this.state`
 * [no-multi-comp](docs/rules/no-multi-comp.md): Prevent multiple component definition per file
 * [no-set-state](docs/rules/no-set-state.md): Prevent usage of `setState`
 * [no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property
diff --git a/docs/rules/no-direct-mutation-state.md b/docs/rules/no-direct-mutation-state.md
new file mode 100644
index 00000000..f42d86c0
--- /dev/null
+++ b/docs/rules/no-direct-mutation-state.md
@@ -0,0 +1,37 @@
+# Prevent direct mutation of this.state (no-direct-mutation-state)
+
+NEVER mutate `this.state` directly, as calling `setState()` afterwards may replace
+the mutation you made. Treat `this.state` as if it were immutable.
+
+## Rule Details
+
+This rule is aimed to forbid the use of mutating `this.state` directly.
+
+The following patterns are considered warnings:
+
+```js
+var Hello = React.createClass({
+  componentDidMount: function() {
+    this.state.name = this.props.name.toUpperCase();
+  },
+  render: function() {
+    return <div>Hello {this.state.name}</div>;
+  }
+});
+```
+
+
+The following patterns are not considered warnings:
+
+```js
+var Hello = React.createClass({
+  componentDidMount: function() {
+    this.setState({
+      name: this.props.name.toUpperCase();
+    });
+  },
+  render: function() {
+    return <div>Hello {this.state.name}</div>;
+  }
+});
+```
diff --git a/index.js b/index.js
index 2b9bd1c4..c654d598 100644
--- a/index.js
+++ b/index.js
@@ -27,7 +27,8 @@ module.exports = {
     'jsx-max-props-per-line': require('./lib/rules/jsx-max-props-per-line'),
     'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
     'jsx-indent-props': require('./lib/rules/jsx-indent-props'),
-    'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location')
+    'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'),
+    'no-direct-mutation-state': require('./lib/rules/no-direct-mutation-state')
   },
   rulesConfig: {
     'jsx-uses-react': 0,
@@ -55,6 +56,7 @@ module.exports = {
     'jsx-max-props-per-line': 0,
     'jsx-no-literals': 0,
     'jsx-indent-props': 0,
-    'jsx-closing-bracket-location': 0
+    'jsx-closing-bracket-location': 0,
+    'no-direct-mutation-state': 0
   }
 };
diff --git a/lib/rules/no-direct-mutation-state.js b/lib/rules/no-direct-mutation-state.js
new file mode 100644
index 00000000..d6e575c8
--- /dev/null
+++ b/lib/rules/no-direct-mutation-state.js
@@ -0,0 +1,37 @@
+/**
+ * @fileoverview Prevent usage of setState in componentDidMount
+ * @author David Petersen
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = function(context) {
+
+  // --------------------------------------------------------------------------
+  // Public
+  // --------------------------------------------------------------------------
+
+  return {
+
+    AssignmentExpression: function(node) {
+      var item;
+      if (!node.left || !node.left.object || !node.left.object.object) {
+        return;
+      }
+      item = node.left.object;
+      while (item.object.property) {
+        item = item.object;
+      }
+      if (
+        item.object.type === 'ThisExpression' &&
+        item.property.name === 'state'
+      ) {
+        context.report(node.left.object, 'Do not mutate state directly. Use setState().');
+      }
+    }
+  };
+
+};


Test patch:
diff --git a/tests/lib/rules/no-direct-mutation-state.js b/tests/lib/rules/no-direct-mutation-state.js
new file mode 100644
index 00000000..5194cf07
--- /dev/null
+++ b/tests/lib/rules/no-direct-mutation-state.js
@@ -0,0 +1,123 @@
+/**
+ * @fileoverview Prevent direct mutation of this.state
+ * @author David Petersen
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/no-direct-mutation-state');
+var RuleTester = require('eslint').RuleTester;
+
+require('babel-eslint');
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester();
+ruleTester.run('no-direct-mutation-state', rule, {
+
+  valid: [{
+    code: [
+      'var Hello = React.createClass({',
+      '  render: function() {',
+      '    return <div>Hello {this.props.name}</div>;',
+      '  }',
+      '});'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    }
+  }, {
+    code: [
+      'var Hello = React.createClass({',
+      '  render: function() {',
+      '    var obj = {state: {}};',
+      '    obj.state.name = "foo";',
+      '    return <div>Hello {obj.state.name}</div>;',
+      '  }',
+      '});'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    }
+  }, {
+    code: [
+      'var Hello = "foo";',
+      'module.exports = {};'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    }
+  }],
+
+  invalid: [{
+    code: [
+      'var Hello = React.createClass({',
+      '  render: function() {',
+      '    this.state.foo = "bar"',
+      '    return <div>Hello {this.props.name}</div>;',
+      '  }',
+      '});'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    },
+    errors: [{
+      message: 'Do not mutate state directly. Use setState().'
+    }]
+  }, {
+    code: [
+      'var Hello = React.createClass({',
+      '  render: function() {',
+      '    this.state.person.name= "bar"',
+      '    return <div>Hello {this.props.name}</div>;',
+      '  }',
+      '});'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    },
+    errors: [{
+      message: 'Do not mutate state directly. Use setState().'
+    }]
+  }, {
+    code: [
+      'var Hello = React.createClass({',
+      '  render: function() {',
+      '    this.state.person.name.first = "bar"',
+      '    return <div>Hello</div>;',
+      '  }',
+      '});'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    },
+    errors: [{
+      message: 'Do not mutate state directly. Use setState().'
+    }]
+  }
+  /**
+   * Would be nice to prevent this too
+  , {
+    code: [
+      'var Hello = React.createClass({',
+      '  render: function() {',
+      '    var that = this;',
+      '    that.state.person.name.first = "bar"',
+      '    return <div>Hello</div>;',
+      '  }',
+      '});'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    },
+    errors: [{
+      message: 'Do not mutate state directly. Use setState().'
+    }]
+  }*/
+  ]
+});