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.' + }] + }*/ + ] +});