diff --git a/README.md b/README.md
index 35312840e3..5472fef70c 100644
--- a/README.md
+++ b/README.md
@@ -101,6 +101,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](#
* [react/no-is-mounted](docs/rules/no-is-mounted.md): Prevent usage of `isMounted`
* [react/no-multi-comp](docs/rules/no-multi-comp.md): Prevent multiple component definition per file
* [react/no-redundant-should-component-update](docs/rules/no-redundant-should-component-update.md): Prevent usage of `shouldComponentUpdate` when extending React.PureComponent
+* [react/no-mutation-props](docs/rules/no-mutation-props.md): Prevent mutation of `this.props`
* [react/no-render-return-value](docs/rules/no-render-return-value.md): Prevent usage of the return value of `React.render`
* [react/no-set-state](docs/rules/no-set-state.md): Prevent usage of `setState`
* [react/no-typos](docs/rules/no-typos.md): Prevent common casing typos
diff --git a/docs/rules/no-mutation-props.md b/docs/rules/no-mutation-props.md
new file mode 100644
index 0000000000..d6b23082a8
--- /dev/null
+++ b/docs/rules/no-mutation-props.md
@@ -0,0 +1,19 @@
+# Prevent mutation of this.props (no-mutation-props)
+
+NEVER mutate `this.props`, as all React components must act like pure functions with respect to their props.
+Treat `this.props` as if it were immutable. More info available at [https://facebook.github.io/react/docs/components-and-props.html#props-are-read-only](https://facebook.github.io/react/docs/components-and-props.html#props-are-read-only)
+
+## Rule Details
+
+This rule is aimed to forbid the use of mutating `this.props`.
+
+The following patterns are considered warnings:
+
+```jsx
+var Hello = React.createClass({
+ render: function() {
+ this.props.name = this.props.name.toUpperCase();
+ return
Hello {this.props.name}
;
+ }
+});
+```
diff --git a/index.js b/index.js
index 13b72fdd4d..afbc43502f 100644
--- a/index.js
+++ b/index.js
@@ -57,6 +57,7 @@ const allRules = {
'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
'no-unused-state': require('./lib/rules/no-unused-state'),
'no-will-update-set-state': require('./lib/rules/no-will-update-set-state'),
+ 'no-mutation-props': require('./lib/rules/no-mutation-props'),
'prefer-es6-class': require('./lib/rules/prefer-es6-class'),
'prefer-stateless-function': require('./lib/rules/prefer-stateless-function'),
'prop-types': require('./lib/rules/prop-types'),
@@ -123,6 +124,7 @@ module.exports = {
'react/no-danger-with-children': 2,
'react/no-deprecated': 2,
'react/no-direct-mutation-state': 2,
+ 'react/no-mutation-props': 2,
'react/no-find-dom-node': 2,
'react/no-is-mounted': 2,
'react/no-render-return-value': 2,
diff --git a/lib/rules/no-mutation-props.js b/lib/rules/no-mutation-props.js
new file mode 100644
index 0000000000..2cc5e4ddf2
--- /dev/null
+++ b/lib/rules/no-mutation-props.js
@@ -0,0 +1,89 @@
+/**
+ * @fileoverview Prevent direct mutation of this.props
+ * @author Ian Schmitz
+ */
+'use strict';
+
+var has = require('has');
+var Components = require('../util/Components');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ docs: {
+ description: 'Prevent direct mutation of this.props',
+ category: 'Possible Errors',
+ recommended: true
+ }
+ },
+
+ create: Components.detect(function (context, components, utils) {
+
+ /**
+ * Checks if the component is valid
+ * @param {Object} component The component to process
+ * @returns {Boolean} True if the component is valid, false if not.
+ */
+ function isValid(component) {
+ return Boolean(component && !component.mutateProps);
+ }
+
+ /**
+ * Reports this.props mutations for a given component
+ * @param {Object} component The component to process
+ */
+ function reportMutations(component) {
+ var mutation;
+ for (var i = 0, j = component.mutations.length; i < j; i++) {
+ mutation = component.mutations[i];
+ context.report({
+ node: mutation,
+ message: 'Do not mutate props.'
+ });
+ }
+ }
+
+ // --------------------------------------------------------------------------
+ // 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 === 'props') {
+ var component = components.get(utils.getParentComponent());
+ var mutations = component && component.mutations || [];
+ mutations.push(node.left.object);
+ components.set(node, {
+ mutateProps: true,
+ mutations: mutations
+ });
+ }
+ },
+
+ 'Program:exit': function () {
+ var list = components.list();
+ for (var component in list) {
+ if (!has(list, component) || isValid(list[component])) {
+ continue;
+ }
+ reportMutations(list[component]);
+ }
+ }
+ };
+
+ })
+};
diff --git a/tests/lib/rules/no-mutation-props.js b/tests/lib/rules/no-mutation-props.js
new file mode 100644
index 0000000000..0ae982eab4
--- /dev/null
+++ b/tests/lib/rules/no-mutation-props.js
@@ -0,0 +1,146 @@
+/**
+ * @fileoverview Prevent mutation of this.props
+ * @author Ian Schmitz
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/no-mutation-props');
+var RuleTester = require('eslint').RuleTester;
+
+var parserOptions = {
+ ecmaVersion: 6,
+ ecmaFeatures: {
+ jsx: true
+ }
+};
+
+require('babel-eslint');
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester();
+ruleTester.run('no-mutation-props', rule, {
+
+ valid: [{
+ code: [
+ 'var Hello = React.createClass({',
+ ' render: function() {',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' render: function() {',
+ ' var obj = {props: {}};',
+ ' obj.props.name = "foo";',
+ ' return Hello {obj.props.name}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'var Hello = "foo";',
+ 'module.exports = {};'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }, {
+ code: [
+ 'class Hello {',
+ ' getFoo() {',
+ ' this.props.foo = \'bar\'',
+ ' return this.props.foo;',
+ ' }',
+ '}'
+ ].join('\n'),
+ parserOptions: parserOptions
+ }],
+
+ invalid: [{
+ code: [
+ 'var Hello = React.createClass({',
+ ' render: function() {',
+ ' this.props.foo = "bar"',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: 'Do not mutate props.'
+ }]
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' render: function() {',
+ ' this.props.person.name= "bar"',
+ ' return Hello {this.props.name}
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: 'Do not mutate props.'
+ }]
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' render: function() {',
+ ' this.props.person.name.first = "bar"',
+ ' return Hello
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: 'Do not mutate props.'
+ }]
+ }, {
+ code: [
+ 'var Hello = React.createClass({',
+ ' render: function() {',
+ ' this.props.person.name.first = "bar"',
+ ' this.props.person.name.last = "baz"',
+ ' return Hello
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: 'Do not mutate props.',
+ line: 3,
+ column: 5
+ }, {
+ message: 'Do not mutate props.',
+ line: 4,
+ column: 5
+ }]
+ }
+ /**
+ * Would be nice to prevent this too
+ , {
+ code: [
+ 'var Hello = React.createClass({',
+ ' render: function() {',
+ ' var that = this;',
+ ' that.props.person.name.first = "bar"',
+ ' return Hello
;',
+ ' }',
+ '});'
+ ].join('\n'),
+ parserOptions: parserOptions,
+ errors: [{
+ message: 'Do not mutate props.'
+ }]
+ }*/
+ ]
+});