diff --git a/lib/rules/no-direct-mutation-state.js b/lib/rules/no-direct-mutation-state.js index 96169bc8a3..40e877a19a 100644 --- a/lib/rules/no-direct-mutation-state.js +++ b/lib/rules/no-direct-mutation-state.js @@ -45,6 +45,36 @@ module.exports = { } } + /** + * Walks throughs the MemberExpression to the top-most property. + * @param {Object} node The node to process + * @returns {Object} The outer-most MemberExpression + */ + function getOuterMemberExpression(node) { + while (node.object && node.object.property) { + node = node.object; + } + return node; + } + + /** + * Determine if this MemberExpression is for `this.state` + * @param {Object} node The node to process + * @returns {Boolean} + */ + function isStateMemberExpression(node) { + return node.object.type === 'ThisExpression' && node.property.name === 'state'; + } + + /** + * Determine if we should currently ignore assignments in this component. + * @param {?Object} component The component to process + * @returns {Boolean} True if we should skip assignment checks. + */ + function shouldIgnoreComponent(component) { + return !component || (component.inConstructor && !component.inCallExpression); + } + // -------------------------------------------------------------------------- // Public // -------------------------------------------------------------------------- @@ -64,19 +94,12 @@ module.exports = { }, AssignmentExpression(node) { - let item; const component = components.get(utils.getParentComponent()); - if (!component || (component.inConstructor && !component.inCallExpression) || !node.left || !node.left.object) { + if (shouldIgnoreComponent(component) || !node.left || !node.left.object) { return; } - item = node.left; - while (item.object && item.object.property) { - item = item.object; - } - if ( - item.object.type === 'ThisExpression' && - item.property.name === 'state' - ) { + const item = getOuterMemberExpression(node.left); + if (isStateMemberExpression(item)) { const mutations = (component && component.mutations) || []; mutations.push(node.left.object); components.set(node, { @@ -86,6 +109,22 @@ module.exports = { } }, + UpdateExpression(node) { + const component = components.get(utils.getParentComponent()); + if (shouldIgnoreComponent(component) || node.argument.type !== 'MemberExpression') { + return; + } + const item = getOuterMemberExpression(node.argument); + if (isStateMemberExpression(item)) { + const mutations = (component && component.mutations) || []; + mutations.push(item); + components.set(node, { + mutateSetState: true, + mutations + }); + } + }, + 'CallExpression:exit': function(node) { components.set(node, { inCallExpression: false diff --git a/tests/lib/rules/no-direct-mutation-state.js b/tests/lib/rules/no-direct-mutation-state.js index 153b943ca0..b1e788992a 100644 --- a/tests/lib/rules/no-direct-mutation-state.js +++ b/tests/lib/rules/no-direct-mutation-state.js @@ -69,6 +69,14 @@ ruleTester.run('no-direct-mutation-state', rule, { ' }', '}' ].join('\n') + }, { + code: [ + 'class Hello extends React.Component {', + ' constructor() {', + ' this.state.foo = 1;', + ' }', + '}' + ].join('\n') }, { code: ` class OneComponent extends Component { @@ -97,6 +105,18 @@ ruleTester.run('no-direct-mutation-state', rule, { errors: [{ message: 'Do not mutate state directly. Use setState().' }] + }, { + code: [ + 'var Hello = createReactClass({', + ' render: function() {', + ' this.state.foo++;', + ' return