/** * @name Potentially inconsistent state update * @description Updating the state of a component based on the current value of * 'this.state' or 'this.props' may lead to inconsistent component * state. * @kind problem * @problem.severity warning * @id js/react/inconsistent-state-update * @tags reliability * frameworks/react * @precision very-high */ import semmle.javascript.frameworks.React /** * Gets an unsafe property access, that is, an expression that reads (a property of) * `this.state` or `this.prop` on component `c`. */ DataFlow::PropRead getAnUnsafeAccess(ReactComponent c) { result = c.getAPropRead() or result = c.getAStateAccess() } /** * Gets at unsafe property access that is not the base of another unsafe property * access. */ DataFlow::PropRead getAnOutermostUnsafeAccess(ReactComponent c) { result = getAnUnsafeAccess(c) and not exists(DataFlow::PropRead outer | outer = getAnUnsafeAccess(c) | result = outer.getBase()) } /** * Gets a property write through `setState` for state property `name` of `c`. */ DataFlow::PropWrite getAStateUpdate(ReactComponent c, string name) { exists(DataFlow::ObjectLiteralNode newState | newState.flowsTo(c.getAMethodCall("setState").getArgument(0)) and result = newState.getAPropertyWrite(name) ) } /** * Gets a property write through `setState` for a state property of `c` that is only written at this property write. */ DataFlow::PropWrite getAUniqueStateUpdate(ReactComponent c) { exists(string name | count(getAStateUpdate(c, name)) = 1 and result = getAStateUpdate(c, name) ) } /** * Holds for "self dependent" component state updates. E.g. `this.setState({toggled: !this.state.toggled})`. */ predicate isAStateUpdateFromSelf(ReactComponent c, DataFlow::PropWrite pwn, DataFlow::PropRead prn) { exists(string name | pwn = getAStateUpdate(c, name) and c.getADirectStateAccess().flowsTo(prn.getBase()) and prn.getPropertyName() = name and pwn.getRhs().asExpr() = prn.asExpr().getParentExpr*() and pwn.getContainer() = prn.getContainer() ) } from ReactComponent c, MethodCallExpr setState, Expr getState where setState = c.getAMethodCall("setState").asExpr() and getState = getAnOutermostUnsafeAccess(c).asExpr() and getState.getParentExpr*() = setState.getArgument(0) and getState.getEnclosingFunction() = setState.getEnclosingFunction() and // ignore self-updates that only occur in one location: `setState({toggled: !this.state.toggled})`, they are most likely safe in practice not exists(DataFlow::PropWrite pwn | pwn = getAUniqueStateUpdate(c) and isAStateUpdateFromSelf(c, pwn, DataFlow::valueNode(getState)) ) select setState, "Component state update uses $@.", getState, "potentially inconsistent value"