/** * @fileoverview Prevent direct mutation of this.state * @author David Petersen * @author Nicolas Fernandez <@burabure> */ 'use strict'; const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ module.exports = { meta: { docs: { description: 'Prevent direct mutation of this.state', category: 'Possible Errors', recommended: true, url: docsUrl('no-direct-mutation-state') } }, create: Components.detect((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.mutateSetState); } /** * Reports undeclared proptypes for a given component * @param {Object} component The component to process */ function reportMutations(component) { let mutation; for (let i = 0, j = component.mutations.length; i < j; i++) { mutation = component.mutations[i]; context.report({ node: mutation, message: 'Do not mutate state directly. Use setState().' }); } } /** * 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 // -------------------------------------------------------------------------- return { MethodDefinition(node) { if (node.kind === 'constructor') { components.set(node, { inConstructor: true }); } }, CallExpression: function(node) { components.set(node, { inCallExpression: true }); }, AssignmentExpression(node) { const component = components.get(utils.getParentComponent()); if (shouldIgnoreComponent(component) || !node.left || !node.left.object) { return; } const item = getOuterMemberExpression(node.left); if (isStateMemberExpression(item)) { const mutations = (component && component.mutations) || []; mutations.push(node.left.object); components.set(node, { mutateSetState: true, mutations }); } }, 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 }); }, 'MethodDefinition:exit': function (node) { if (node.kind === 'constructor') { components.set(node, { inConstructor: false }); } }, 'Program:exit': function () { const list = components.list(); Object.keys(list).forEach(key => { if (!isValid(list[key])) { reportMutations(list[key]); } }); } }; }) };