The latest rule we implemented based on execution flow aims at no less than detecting when a property is accessed on a value which might be null or undefined. In such a case, a TypeError may be thrown at runtime, and the application may crash. That rule catches obvious bugs in poor quality code, but it can also find more subtle issues, such as when a value is sometimes tested for nullability and sometimes not. That’s the case in the following code in the React project:
Conditions which are either always true or always false represent another bug pattern for which we have a relatively new rule. Sometimes, such a condition is simply redundant with the rest of the code as in in the following example in the Closure Library:
In other cases, a condition which is always true or false may be the visible part of a real bug, especially when it means that a full block of code will never be executed. Here’s an example from the Ionic framework which looks like a serious bug:
Detecting dead stores is another rule we added recently and that is based on execution flow. A dead store is a useless assignment to a variable, where the variable that’s assigned is never read after the assignment. Most often, this is not a bug, just useless and potentially confusing code. However, it’s so common that thousands of dead stores can be found in open-source projects. Here’s a very simple example in the AngularJS project:
Now, we’ve said all of these rules are based on execution flow, but some curious readers may ask: how do we describe execution flow? Except in the simplest cases, execution flow is rarely linear. As soon as a piece of code contains an if statement, its execution flow has to be described with alternatives branches: either the condition of the if statement is true and we execute the associated block, or it’s false and we don’t execute the block. Moreover, execution flow may go back to a point which was already executed thanks to loops. In the end, in order to represent all the possible paths, we use a graph structure which is known as a control flow graph (CFG).
Based on a control flow graph, it’s rather easy to identify dead stores by checking all paths which come out of an assignment. However, a CFG is certainly not enough to detect potential TypeErrors or conditions which are always true or false. To do that, we need symbolic execution. That is, we need to track the values which are referenced by variables. We can walk through the CFG and evaluate some parts of the code:
- Based on assignments, we may know the precise value of a variable at a given point.
- Based on conditions in if statements or loops, we may know which constraints are met by the value of a variable inside a given block.
Running symbolic execution means that we explore the possible execution paths based on the CFG and the possible constraints on variables.
- When looking for possible TypeErrors, we raise an issue as soon as one of the execution paths leads to a property access on a value which is constrained to null or undefined.
- When looking for conditions which are always true or false, we have to check all of the execution paths which go through a condition.