Photo by icon0.com from Pexels

Making Flow error suppressions more specific

Daniel Sainati
Flow
Published in
6 min readMar 16, 2020

--

We’re improving Flow error suppressions so that they don’t accidentally hide errors.

Over the coming year, Flow is planning to make many changes and improvements to its type system. One of the consequences of this, however, is that you may need to add more suppressions to your code as Flow gets better at finding existing issues.

This can be a problem because Flow’s suppressions affect more than just the original error you intended to suppress. This reduces Flow’s ability to catch errors. This is a result of two properties: First, that Flow suppressions can be located at any part of a type error, and second, that Flow suppressions can suppress any kind of type error. To fix these shortcomings, we are going to require that suppressions be placed at the location in your code where the error actually occurs, and that they include an error code specifying the kind of error you want to suppress.

To understand what all this means, let’s dig in to the nature of Flow’s type errors.

What is a type error?

When Flow encounters an issue while checking your code, like an incompatibility between two types or an invalid property access, it generates an error that reports to the user precisely what is wrong with their code. These errors can contain a wide variety of information depending on the particular circumstances that caused them, but the two that we care most about are the kind of the error and the locations of the error.

The kind of the error encodes the specific nature of the bad behavior that Flow detected in your code, whether it be something simple like passing too many arguments to a function, or something complex like spreading a union type. This information tells Flow exactly what kind of information to display to the user to best explain exactly where you went wrong.

The locations of the error contain information about where the error is located in your code. There are two kinds of locations an error can contain, a primary location (of which there can be only one), and secondary locations, of which there can be many or none at all. The primary location roughly corresponds to the location in your code where the error actually occurred, while the secondary locations carry information about the definition sites of the types and values involved in the error. For example:

function foo(x : number) : void {}
foo("hello");
/* error */
foo("hello");
^ Cannot call `foo` with `"hello"` bound to `x` because string [1] is incompatible with number [2].
References:
2: foo("hello");
^ [1]
1: function foo(x : number) : void {}
^ [2]

In this example, the error occurs when foo is called with "hello", since "hello" is not a number. This means that the primary location of the error, where it occurs, is on the second line. The secondary location of this error is on the first line, because foo is a part of this error, and the first line is where it is defined. If we were to call foo with a variable, however, like this:

function foo(x : number) : void {}
let y : string = "hello";
foo(y);
/* error */
foo(y);
^ Cannot call `foo` with `y` bound to `x` because string [1] is incompatible with number [2].
References:
2: let y : string = "hello";
^ [1]
1: function foo(x : number) : void {}
^ [2]

The error here has its primary location on the third line, but has two secondary locations at the definition sites of foo and y.

So what’s so wrong with suppressions?

Well, because suppressions can be applied on both the primary and secondary locations, putting a //$FlowFixMe comment above the definition of foo would suppress the errors in both of the above examples. Since foo appears as a secondary location in every error that uses it, that would mean that any use of foo that results in an error would be suppressed by this one suppression, anywhere that it might occur. You would be able to call foo with any number of arguments of any type and Flow would not surface any errors or warnings to you about any of this. This degrades the confidence of users in Flow’s type checking and allows bugs it would otherwise have caught to make it into production.

In addition to this, Flow’s suppressions affect all kinds of errors, as long as they contain a location that the suppression covers. This means that multiple errors of different kinds can be suppressed by a single suppression. To see the danger in this, consider the following example:

// library file lib.jsfunction foo(x : number) : void {}// impl file
const {foo} = require("lib.js");
let y : string = "hello";
// $FlowFixMe
foo(y);

This suppression comment prevents Flow from raising the type incompatibility issue here, but let’s imagine you’re okay with this because it works at runtime. Now imagine that an update to the library file added a second argument to foo, and uses it in such a way that not providing this argument would cause a runtime error. Now there is a second error on the call to foo: the call does not supply the right number of arguments, but Flow will not report this to you, because there is still a suppression on the previous line. Our change has caused Flow to raise a second type of error, but the suppression captures both of them, and the bug is able to escape Flow’s notice.

What are we going to do about this?

To address this we are going to make the following changes:

  • Enforce primary locations. We will be changing the suppression behavior such that suppressions only apply to errors when placed at their primary location. For example, instead of allowing you to suppress an invalid call at the definition of the function, we will now require that the suppression be located at the call-site itself. This way, Flow will still be able to alert you about other possibly invalid calls to the function.
  • Add error codes. We will also be adding error codes to our suppressions so they only suppress errors of the specified kind. This will prevent suppressions from unexpectedly suppressing a kind of error you were not expecting to encounter.
  • Standardize suppression syntax. Previously the acceptable syntax for an error suppression could be configured manually in the .flowconfig for a given project, allowing inconsistent suppression syntax across projects. As part of the above changes we will also be standardizing the suppression syntax; only supporting the $FlowFixMe[incompatible-type] or $FlowExpectedError[incompatible-type] formats.

Note that the enforcement of primary locations will be released in version 0.121.0, while error codes and standardized suppressions will be added in a later release.

Rollout

We recognize that this change may cause a significant number of errors in your codebases to become invalid, especially if you were in the habit of placing suppressions in library code. To ease this burden, we have a couple of suggestions:

  • To relocate your newly invalid suppressions to their primary locations, we recommend a combination of the add-comments and remove-comments utilities provided in the Flow tool. Running ./tool remove-comments will remove any comments that no longer suppress an error because they are not on a primary location, and ./tool add-comments will place new suppressions on unsuppressed locations. The ./tool script can be accessed by cloning the Flow repository on GitHub.
  • Suppressions without error codes will continue to suppress any and all errors on their location, but once we roll out the error codes feature, you can add the proper codes to your codebase via a similar process as above. Removing all your old comments and re-adding them with add-comments will include the error code in the newly added comment.

We look forward to bringing you a safer codebase!

--

--