Fast, extensible, configurable, and beautiful linter for Go

Edit · May 28, 2018 · 8 minutes read · Go Static analysis Linting Tooling

About a year ago I decided to polish my Go skills. Although the language is pretty small compared to most others that I use on a daily basis, it still has some useful syntax constructs that I didn’t use enough. What a better way to brush up your skills in a programming language other than building tools with it…for analyzing programs written in it?

You can find revive on GitHub at github.com/mgechev/revive.
Revive demo

Introducing Revive

When I started using Go, a few years ago, I noticed how opinionated the ecosystem was. First, the syntax of the language is very minimalistic. Such property reduces the expressiveness but also makes the code much easier to read, compared to languages such as Perl and Ruby.

Gopher

Second, the “tabs vs. spaces” debate is not relevant in the Go world. We all use tabs. That’s it. Do I prefer tabs over spaces? It doesn’t matter - gofmt already picked tabs for me. I also noticed that there’s golint. It’s a tool which enforces semantical & syntactical practices. There are few things in golint different from most linters, I’ve used in the past:

  • It’s not extensible. You can’t include/exclude rules, and the project is not wide open for new rules.
  • It doesn’t let us disable rules for specific files or a range of lines in a file.
  • In golint failures have a level of confidence. Sometimes failures are false negatives.

The Go team is keeping the tool opinionated and minimalistic. It follows the Go philosophy, and I respect that. The closed scope of the project has few interesting implications:

Logo of Revive
The logo of revive by Georgi Serev

That’s why I decided to make my toy linter project public and share it with the community. Revive implements all the rules which golint has and the failures have the same concept of “confidence” that golint introduced. In fact, invoking revive with no flags has the same behavior as golint, with the difference that it runs faster. Revive builds on top of golint by:

  • Allowing us to enable or disable rules using a configuration file.
  • Allowing us to configure the linting rules with a TOML file.
  • Providing functionality for disabling a specific rule or the entire linter for a file or a range of lines.
    • golint allows this only for generated files.
  • Providing multiple formatters which let us customize the output.
  • Allowing us to customize the return code for the entire linter or based on the failure of only some rules.
  • Everyone can extend it easily with custom rules or formatters.
  • Revive provides more rules compared to golint.
  • Revive runs faster. It runs the rules over each file in a separate goroutine.

I love the opinionated culture in the Go community. I believe that this is the right direction which lets us focus on the important things instead of losing time in discussions on trivial matters. That’s why with revive, we can make even more syntax-related arguments irrelevant by defining more opinionated rules and providing stricter presets.

Later, I’ll explain how we can quickly develop more rules for revive but before that, let me share a few instructions on how you can use the linter!

Usage

Install revive with:

go get github.com/mgechev/revive

The command above adds the revive binary under $GOPATH/bin.

Using the tool with no flags has the same behavior as golint. The magic happens when we add the -formatter flag:

Friendly formatter

From the image above, we can see that we got 31 warnings for the "exported" rule. This rule is port of a built-in rule from golint which enforces practices for exported symbols (find the full set of rules here).

If we prefer to ignore these warnings for the entire project, we can use a config file in TOML format:

Edit config

What if we want to disable a specific rule for only part of the file? In such case, we can use the following technique:

package models

//revive:disable

type Expression struct {
    Value      string
    IsStar     bool
    IsVariadic bool
    IsWriter   bool
    Underlying string
}
//revive:enable

The annotations above disables all revive rules for the entire struct. In case we prefer to disable only the exported rule, we should use:

package models

//revive:disable:exported

type Expression struct {
    Value      string
    IsStar     bool
    IsVariadic bool
    IsWriter   bool
    Underlying string
}
//revive:enable:exported

Keep in mind that comments which are meant for the linter, should not start with a whitespace. For more details see revive: unidiomatic // revive: syntax.

Finally, if we want to ignore all files from a directory, we can use the -exclude flag:

revive -exclude tests/... ./...

The command above will lint all files from the current directory, excluding all files in tests, recursively. If we want to exclude more than one directory use:

revive -exclude tests/... -exclude utils/... ./...

Configurability

In an image in the last section, we saw that by using a config file in TOML format, we could configure the execution of revive. Here’s an example config file:

# Ignores files with "GENERATED" header, similar to golint
ignoreGeneratedHeader = true

# Sets the default severity to "warning"
severity = "warning"

# Sets the default failure confidence. The semantics behind this property
# is that revive ignores all failures with a confidence level below 0.8.
confidence = 0.8

# Sets the error code for failures with severity "error"
errorCode = 0

# Sets the error code for failures with severity "warning"
warningCode = 0

# Configuration of the `cyclomatic` rule. Here we specify that
# the rule should fail if it detects code with higher complexity than 10.
[rule.cyclomatic]
  arguments = [10]

# Sets the severity of the `package-comments` rule to "error".
[rule.package-comments]
  severity = "error"

Let’s quickly go over the individual properties.

  • ignoreGeneratedHeader - golint ignores files which have header GENERATED. To disable this behavior, set the flag to false.
  • severity - revive has two types of severity - warning and error. This way we can distinguish between critical failures with high confidence level and such with lower.
  • confidence - similarly to golint, revive lets us assign a confidence level to rules which may return false negatives. The confidence property lets us filter failures below given confidence level.
  • errorCode and warningCode - these two properties let us have different return codes for the different severities. We may want your errors to fail the CI but our warnings not to.

The remaining lines are related to rule configuration. Each rule can be configured by setting its arguments and its severity. For example, the rule for cyclomatic complexity above, fails if in our codebase there’s a construction which exceeds the cyclomatic complexity of 10.

Gopher

Extensibility and Contributions

Initially, I wanted to let developers use be able to create external plugins which later could be referenced by the configuration file and loaded dynamically. Unfortunately, -buildmode=plugin has very limited support with known issues.

Ignoring this limitation, there are other two easy ways to add new rules and run them against your code:

  • Contribute to the project - revive is open for external contributions. If a rule makes sense and passes its unit tests, it’s more than welcome to become part of revive! On top of that, creating a new rule is just a matter of implementing this simple interface:
type Rule interface {
    Name() string
    Apply(*File, Arguments) []Failure
}
  • Fork the project and push the rules there. If you think your rules won’t apply to others (although the chances are that they will), you can fork revive and not push them upstream. Just make sure that you sync your code with upstream once in a while to get all the new features and bug fixes from there!

Keep in mind that to create a rule you don’t have to be familiar with the entire codebase. All rules are well encapsulated into visitors with simple interface. A sample implementation of arguments-limit could be found here.

What about formatters?

Well, creating a new formatter is as simple as creating a new rule. Just implement the following interface:

type Formatter interface {
    Format(<-chan Failure, RulesConfig) (string, error)
    Name() string
}

Here you can find a sample implementation of a JSON formatter.

Performance

I run some basic benchmarks to compare the performance of golint and revive. Here’s what I found out after running both linters against kubernetes:

time golint be/…
real    0m25.389s
user    0m29.221s
sys     0m3.065s
time revive be/…
real    0m6.524s
user    0m22.882s
sys     0m1.114s

Since revive lints the individual files in separate goroutines, it outperforms golint about 4 times.

Conclusion

Revive is a simple, fast, configurable, extensible, flexible, and beautiful linter for Go. It runs the linting rules on top of each file in a separate goroutine which improves the performance significantly. Revive lets us configure the individual rules and disable them for the entire project, individual files, or range of lines within a file. Last but not least, revive lets us use a set of built-in formatters which output the failures in a digestible, accessible, and easy to consume format.

The project is continuously evolving and open for new contributions in the form of new rules, formatters, or bug fixes! If you want to create an even stricter linting preset, cut the coding style discussions in your team to the minimum, and focus on the essential things, revive may help.

Drop mic