JavaScript Plugin Finds Tricky Bugs, Thanks to Execution Flow

by pierre yves nicolas|

    Over the last few months, the SonarAnalyzer for JavaScript has made major advances in bug detection. Until recently, it only caught rather simple bugs, like function calls passing extra arguments, which didn’t really need more than a correct identification of symbols. Things changed a lot when we made the analyzer aware of execution flow: in other words, it is now able to determine the precise order of execution inside a JavaScript function and detect bugs based on it.

    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 JavaScript every value has an inherent boolean value and is therefore either “truthy” or “falsy”. False, null, undefined, NaN, 0, and the empty string are “falsy”. Everything else is “truthy”.)

    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.

    Our symbolic execution engine is still in its early stages and can only evaluate simple constructs right now, but the current results of these new rules look very promising to us. As we improve our engine, the rules which are based on it will get more accurate. We’ve gotten this far by following the lead of the SonarAnalyzer for Java, which overshadowed FindBugs, turning it from a great tool to a great tool of the past. We hope we can bring as much value to JavaScript developers.

    However, following Java’s lead is only part of the story. Because of the dynamic nature of JavaScript, symbolic execution is more crucial than for other languages like Java. For example, the type of a variable may not be the same for all branches of a given piece of code: many rules will therefore be improved as soon as we start to infer types based on symbolic execution. We have a lot to do, so stay tuned!