Welcome go-critic

We’re announcing a new linter for Go that also will be a sandbox for prototyping your ideas in the world of static code analysis.

go-critic is based on these observations:

  • It’s better to have a “good enough” implementation rather than don’t have it at all.
  • If a check is opinionated it doesn’t mean that it isn’t useful at all, we can mark it as “opinionated” and give it a try.
  • Writing yet another linter from scratch is always harder than adding a new check to an existing platform that is simple and well supported.

In this article we’re going to describe the architecture of go-critic, some already implemented checks and write a new check from scratch.

Quick start

$ cd $GOPATH
$ go get -u github.com/go-critic/go-critic/...
$ ./bin/gocritic check-package strings
See full output: https://gist.github.com/Quasilyte/22db23699108a0a04ce7544a552c6caa

gocritic program is able to check separate packages by import path (check-package command) and recursively scan the whole tree for a given directory (check-package command). For example, you can run gocritic over whole GOROOT or GOPATH by one of this command:

$ gocritic check-project $GOROOT/src
$ gocritic check-project $GOPATH/src

There are plans to add go-critic to gometalinter & golangci-lint.

How it was started

While doing a regular code review of a Go project or auditing a 3rd party lib you can see the same problems again and again.

Unfortunately, you’ve failed to find a linter that could detect such problems.

Your first step could be problem categorization, then contacting some existing linter maintainers to propose a new check. Chances that your proposal will be approved & merged heavily depends on the project and might be quite low. Also, this might take some time…

But what if your check is controversial and someone might decide that it’s very opinionated?

go-critic was created to become a sandbox and a home for such checks, that is easier to implement rather than asking to merge into existing linter.

go-critic minimizes the amount of work to be done to create a new linter. We can say that it’s enough to add only one file (except tests 😉).

How does go-critic works?

Critic is a collection of rules that describe diagnostic properties and micro-linters (checkers) that perform source code inspection to find rule violations.

Application that embeds linter (ex. cmd/gocritic or golangci-lint) receives a list of supported rules, filters them by their properties, creates a check function and runs each of them on analyzing package.

Adding new checker consists of 3 steps:

  • Adding tests
  • Adding implementation of check
  • Adding documentation for a linter

Let’s go through all of this steps for captLocal linter that suggests renaming parameters those name start with a capital letter.


Adding new tests

To add testing data for a new check we need to create a new directory in cmd/gocritic/testdata.

Each of this directories must have a positive_tests.go that describes code examples that must trigger check. To test absence of false positive cases we can (and should 😉) create negative_tests.go where check must not be triggered.

Examples

Code: https://gist.github.com/cristaloleg/8cbccc3eedc63e4b05921bf09895fde1
Code: https://gist.github.com/cristaloleg/52fc78c911e03d53a1aec6a87ad8a634

To run test it’s enough to run one of this command:

$ go test -v ./...
# or
$ make test

Implementing a checker

Let’s create a lint/captLocal_checker.go

By convention all files with checks have suffix _checker.go

Code: https://gist.github.com/cristaloleg/2073d243af04f4fe9512f73c342d8056

checkerBase is a type that must be embedded in every checker. It presents default implementation of useful methods that allow to write less code in every linter.

checkerBase also includes a pointer to lint.context that contains types info and other metadata about the package being checked.

Field upcaseNames will contain a map of well-known names that we will propose to replace with a lowercase version of them. For all other names we’ll just suggest not using capitalized form.

The internal state is initialized once for each checker instance.

Init method must be defined only for those linter that needs some state initialization.

Code: https://gist.github.com/cristaloleg/8b98569cb86ff46c95b9441cb70110ec

Now we’re going to implement verification function. In case of captLocal we need to verify all ast.Ident nodes that provide new variables.

To verify all local variables definitions we need to implement a method with this signature:

VisitLocalDef(name astwalk.Name, initializer ast.Expr)

List of all available visitor interfaces can be found in lint/internal/visitor.go

captLocal implements LocalDefVisitor.

Code: https://gist.github.com/cristaloleg/d07e51265b99b4d1a2beee3c1036fefc

By convention methods that triggers warning are excluded to separate methods. There are a rare exceptions, but following this rule is a good practice.

And the last and very important step:

Code: https://gist.github.com/cristaloleg/3249c76b3343df74cb220fa52e00e21b

addChecker expects a pointer to zero-value of a new linter and an optional variadic parameter that allows us to specify linter’s attributes.

attrSyntaxOnly is an optional attribute for linters that doesn’t depend on information about types in verifying code. For example, golang-lint marks such linters as fast (’cause they’re fast 😉).

Now, when we’ve registered new linter via addChecker it’s possible to run tests:

# From GOPATH:
$ go test -v github.com/go-critic/go-critic/cmd/gocritic
# From GOPATH/src/github.com/go-critic/go-critic:
$ go test -v ./cmd/gocritic
# or run it as a make command:
$ make test

Adding documentation

To describe new linter we need to add a “magic” comment inside implementation file, here is a comment from captLocal_checker.go

Code…well comment: https://gist.github.com/cristaloleg/c403c45f4a3b32b9a5bff8c64a21a094

Structure of this comment is strict and is verified during documentation generation. Simplest documentation comment consist of short one-line summary and two sections Before and After.


Optimistic merging, almost…

During pull request review we’re trying to follow optimistic merging. In general, it means that we’re going to merge some PR’s with small inconsistency or places for future improvements. Right after that PR merge there might be a next PR from a reviewer that will fix leftovers from the previous PR. Original patch author will be CCed in this follow-up PR 😉.

There are two markers for linter that will avoid its rejection if a consensus wasn’t met:

  • Experimental — implementation might have a big amount of false positives, be ineffective (but the source of a problem is defined) or fail in some corner cases. This kind of implementation can be merged with attrExperimental attribute. This attribute can be also used for linters with an unclear name.
  • VeryOpinionated — for implementation of linter that has cons and pros. This will prevent linters from being rejected due to some code style preferences differences between gophers. attrVeryOpinionated attribute is used for this linters.

We recommend to create a [checker-request] issue on github before sending an implementation of it, but if you did an implementation before and have pushed PR — no problem, someone else will make an issue for you.

More information regarding development process can be found in CONTRIBUTING.md.

Main rules are described in main rules section.

Afterwords

There are ways to contribute other than adding a new check, for example:

  • Run it on your or some else’s code, on big or small projects, on open or closed source repositories, share false positives, false negatives or other features, interesting findings, whatever you think is interesting to share. We appreciate if you will add a note to trophies page.
  • Propose new ideas. It is enough to add a new issue.
  • Add tests for existing checks.

go-critic criticizes your code using voices of many Go developers.

Everyone can criticize — join us!

yeah, pixel glasses 😎

Original: https://habr.com/post/414739/

Like what you read? Give Oleg Kovalov a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.