go: proposal: x/tools/go/analysis/passes/errorstringcompare: new check that warns when programmers compare against Error() strings
Problem
On GitHub, there are too many projects that strictly compare error strings outside of test code: `.Error() == “…”
Go 1 and the Future of Go Programs does not explicitly promise that error strings are protected, nor do we want to encourage this amongst third-party libraries. See @neild’s https://github.com/golang/go/issues/49172#issuecomment-954071349.
Proposed Implementation
Now that error wrapping was introduced in Working with Errors in Go 1.13 - go.dev, we should encourage developers to use errors.Is and errors.As whenever possible.
Linting is one way of providing this hint. I propose writing an optional analysis pass that detects when errors strings are compared for equality, substrings, or with regular expressions. This analysis pass should not be incorporated into go vet, because any reported errors are not guaranteed to be bugs.
-
Add x/tools/go/analysis/passes/errorstringcompare that implements the following Analysis.Doc string:
const Doc = `report code that compares error strings when alternatives exist The errorstringcompare analysis reports when code compares error strings, when the underlying error supports the errors.Is interface. Examples of such comparisons include: if err.Error() == "..." { } if err.Error()[i:j] == "..." { } if err.Error()[i] == '.' { } if strings.Contains(err.Error(), "...") { } if strings.HasPrefix(err.Error(), "...") { } if strings.HasSuffix(err.Error(), "...") { } if re.MatchString(err.Error()) { } if re.FindString(err.Error()) != "" { } The -ignorepkgs flag specifies a comma-separated list of packages which will be ignored by the analysis. `Proposed algorithm:
- Find error string comparisons in the current package, where the Error method is compared against another string. If the developer has assigned the error string to a variable or used fmt.Sprintf, assume they know what they’re doing.
- For each of the errors that had their Error method called, try to determine the underlying type:
- Determine if the error was directly initialized, or created by concrete type assertion, and record its actual sum type. Errors wrapped with fmt.Errorf can be handled in this step.
- Determine if the error was the result of a static function call, and add it to the list of errors to inspect.
- If the type of the error is indeterminate, drop the error from the analysis, since we want to avoid false positives.
- After resolving the sum type for each error, keep the errors that can only be exported error sentinels or that implement the errors.Is interface. The Is method implies that there are exported error sentinels to check against.
- Report these remaining errors with a message like
fmt.Sprintf("prefer using errors.Is for %s", err.Name()).
-
Add this to gopls Analyzers, after deciding whether to enable it by default.
-
Encourage metalinters like golangci-lint to add this to their corpus of checks.
Concerns
- Some of these warnings might be encounter too many false positives. We should run errorstringcompare against a representative corpus of Go code to see how noisy it would be.
- Some packages may have tests that actually do promise backwards-compatibility for their APIs, which is why the -ignorepkgs flag exists.
See also
- #49358 which can catch more edge cases if they are covered by tests.
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Reactions: 2
- Comments: 27 (24 by maintainers)
For me at least it seems like the answer is not particularly strongly.
Again, I’m not convinced this is something we should add to gopls and warn users about. It is not nearly at the correctness level that we usually strive for. There will be many of these, users will learn to ignore them, and then they will start ignoring other more precise reports.
I think it’s a great candidate for organization and project’s custom linter - unfortunately,
goplsdoesn’t support a custom linter. Moreover,goplsdoes not provide much flexibility in tuning an individual analyzer (like-ignorepkgsper analyzer) beyond completely turning it on/off. Can popular checkers with more flexible customization options (e.g.,staticcheckorgolangci-lint) be a better place?@findleyr does
goplshave criteria for adding analyzers? Even thoughgoplsis less strict thanvet, we still need to think about long-term maintenance cost and benefit for Go users.Comparing against the Error() string is not the best practice, but personally I don’t feel strongly about enforcing it widely because I think whether a package provides stability guarantee on their Error return values and employs a policy like Go 1 compatibility guarantee is up to the package owners. But people with more experience in this domain have different thoughts.
@dgryski The point of this proposal, and related proposals, is to explore how strongly we want to discourage comparing error strings. This analysis pass would be the point on the spectrum where tooling that understands the standard analysis framework (e.g. gopls, golangci-lint, GoLand, etc.) are easily able to enable this check.
That’s not to discount the work of ruleguard or semgrep. I think that your errclosed check is good evidence that the problem exists. Perhaps we should be more emphatic by moving the check into the golang.org/x ecosystem?
@guodongli-google This proposal explicitly mentions that it should not be enabled for go vet, since an error string match might not actually be a bug with a given set of dependencies, and we don’t want to overwhelm developers with spurious errors.
Note that errstringcompare is very similar to deepequalerrors, which is also an analysis pass that isn’t enabled for go vet, but is widely used to warn about a similar footgun. In deepequalerrors’s case, it is about comparing the error value itself.