Go lintpack: Compileable Link Manager
lintpack is a utility for building linters (static analyzers) that are written using the provided API. On the basis of it, the go-critic static analyzer familiar to some is now being rewritten .
Today we will examine in more detail what is lintpack
from the user's point of view.
In the beginning was the go-critic ...
The go-critic began as a pilot project that was a sandbox for prototyping virtually any static analysis idea for Go.
It was a pleasant surprise that some people actually sent implementations of detectors of various problems in the code. Everything was under control until technical debt began to accumulate, which was virtually no one to eliminate. People came, added checks, and then disappeared. Who then should correct the errors and refine the implementation?
A significant event was the proposal to add checks that require additional configuration, that is, those that depend on local agreements for the project. An example is the detection of the presence of a copyright header in a file (license header) according to a specific pattern or the prohibition of importing some packages with the suggestion of a given alternative.
Another difficulty was extensibility. It’s not convenient for everyone to send their code to someone else’s repository. Some wanted to dynamically connect their checks so that they would not need to modify the source codes go-critic
.
Summarizing, here are the problems that stood in the way of development go-critic
:
- Load of complexity. Too much to maintain, the presence of ownerless code.
- Low average quality level.
experimental
meant both "almost ready to use", and "it is better not to run at all." - It is sometimes difficult to decide to include a check in
go-critic
, and rejecting them contradicts the original philosophy of the project. - Different people saw
go-critic
differently. Most wanted to have it in the form of a CI linter that comes in delivery withgometalinter
.
In order to somehow limit the number of discrepancies and mismatched interpretations of the project, a manifesto was written .
If you want an additional historical context and even more thoughts on the categorization of static analyzers, you can listen to the GoCritic recording - a new static analyzer for Go . At that moment, lintpack did not exist yet, but some of the ideas were born on that day, after the report.
But what if we didn’t have to store all the checks in one repository?
Meet - lintpack
go-critic
consists of two main components:
- The implementation of the checks themselves.
- A program that loads packages checked by Go and runs checks on them.
Our goal: to be able to store the checks for the linter in different repositories and put them together when necessary.
lintpack does exactly that. It defines functions that allow you to describe your checks in such a way that they can then be run through the generated linter.
Packages that are implemented usinglintpack
as a framework will be calledlintpack
-compatible orlintpack
-compatible packages.
If it go-critic
was implemented on the basis itself lintpack
, all the checks could be divided into several repositories. One of the options for separation may be the following:
- The core set, where all the stable and supported checks fall.
- The contrib repository contains code that is either too experimental or does not have a meinteyner.
- Something like go-police , where those customizable checks for a specific project can be found.
The first point is particularly important in connection with the integration of the go-critic into golangci-lint .
If you stay at the level go-critic
, for users, almost nothing has changed. lintpack
creates an almost identical linter, and golangci-lint
encapsulates all the different implementation details.
But something has changed. If lintpack
new linters will be created on the basis , you will have a richer choice of ready-made diagnostics for generating linter. Imagine for a moment that this is so, and there are more than 10 different sets of checks in the world.
Quick start
First you need to install it yourself lintpack
:
# lintpack будет установлен в `$(go env GOPATH)/bin`.
go get -v github.com/go-lintpack/lintpack/...
Create a linter using a test package of lintpack
:
lintpack build -o mylinter github.com/go-lintpack/lintpack/checkers
Enters the set panicNil
, which finds in the code panic(nil)
and ask to perform a replacement for something distinguishable, because otherwise recover()
it will not be able to tell whether it was called panic
with an nil
argument, or if there was no panic at all.
Код ниже пытается описать значение, полученное из recover()
:
r := recover()
fmt.Printf("%T, %v\n", r, r)
Результат будет идентичен для panic(nil)
и для программы, которая не паникует.
Запускаемый пример описываемого поведения.
You can run the linter on separate files, type arguments ./...
or packages (by their import path).
./mylinter check bytes
$GOROOT/src/bytes/buffer_test.go:276:3: panicNil: panic(nil) calls are discouraged
# Далее делается предположение, что go-lintpack есть под вашим $GOPATH.
mylinter=$(pwd)/mylinter
cd $(go env GOPATH)/src/github.com/go-lintpack/lintpack/checkers/testdata
$mylinter check ./panicNil/
./panicNil/positive_tests.go:5:3: panicNil: panic(nil) calls are discouraged
./panicNil/positive_tests.go:9:3: panicNil: panic(interface{}(nil)) calls are discouraged
By default, this check also responds to panic(interface{}(nil))
. To override this behavior, you need to set the value skipNilEfaceLit
to true
. This can be done via the command line:
$mylinter check -@panicNil.skipNilEfaceLit=true ./panicNil/
./panicNil/positive_tests.go:5:3: panicNil: panic(nil) calls are discouraged
И lintpack
, и генерируемый линтер, используют первый аргумент для выбора подкоманды. Список доступных подкоманд и примеров их запуска можно получить вызвав утилиту без аргументов.
lintpack
not enough arguments, expected sub-command name
Supported sub-commands:
build - build linter from made of lintpack-compatible packages
$ lintpack build -help
$ lintpack build -o gocritic github.com/go-critic/checkers
$ lintpack build -linter.version=v1.0.0 .
version - print lintpack version
$ lintpack version
Предположим, мы назвали созданный линтер именем gocritic
:
./gocritic
not enough arguments, expected sub-command name
Supported sub-commands:
check - run linter over specified targets
$ linter check -help
$ linter check -disableTags=none strings bytes
$ linter check -enableTags=diagnostic ./...
version - print linter version
$ linter version
doc - get installed checkers documentation
$ linter doc -help
$ linter doc
$ linter doc checkerName
Для некоторых подкоманд доступен флаг -help
, который предоставляет дополнительную информацию (я вырезал некоторые слишком широкие строки):
./gocritic check -help# Информация о всех доступных флагах.
Documentation of installed checks
The answer to the question "how do I know about that skipNilEfaceLit parameter?" - read the fancy manual (RTFM)!
All documentation of the installed checks is inside mylinter
. This documentation is available through the subcommand doc
:
# Выводит список всех установленных проверок:$mylinter doc
panicNil [diagnostic]
# Выводит детальную документацию по запрашиваемой проверке:$mylinter doc panicNil
panicNil checker documentation
URL: github.com/go-lintpack/lintpack
Tags: [diagnostic]
Detects panic(nil) calls.
Such panic calls are hard to handle during recover.
Non-compliant code:
panic(nil)
Compliant code:
panic("something meaningful")
Checker parameters:
-@panicNil.skipNilEfaceLit bool
whether to ignore interface{}(nil) arguments (default false)
Similar to template support in go list -f
, you can pass a template string that is responsible for the output format of the documentation, which can be useful when drawing up markdown documents.
Where to find checks for installation?
To simplify the search for useful sets of checks, there is a centralized list of lintpack
-compatible packages: https://go-lintpack.github.io/ .
Here are some of the list:
- https://github.com/go-critic/go-critic/checkers
- https://github.com/go-critic/checkers-contrib
- https://github.com/Quasilyte/go-police
This list is periodically updated and it is open for applications to add. Any of these packages can be used to create a linter.
The command below creates a linter that contains all the checks from the list above:
# Сначала нужно убедиться, что исходные коды всех проверок# доступны для Go компилятора.
go get -v github.com/go-critic/go-critic/checkers
go get -v github.com/go-critic/checkers-contrib
go get -v github.com/Quasilyte/go-police
# build принимает список пакетов.
lintpack build \
github.com/go-critic/go-critic/checkers \
github.com/go-critic/checkers-contrib \
github.com/Quasilyte/go-police
lintpack build
includes all checks at the compilation stage, the resulting linter can be placed in an environment where there are no source codes for the implementation of installed diagnostics, all as usual with static linking.
Dynamic Packet Connection
In addition to the static build, it is possible to load plugins that provide additional checks.
The peculiarity is that the checker implementation does not know whether it will be used for static compilation or whether it will be loaded as a plug-in. No code changes are required.
Suppose we want to add panicNil
to the linter, but we cannot reassemble it from all the sources that were used during the first compilation.
- Create
linterPlugin.go
:
package main
// Если требуется включить в плагин более одного набора проверок,// просто добавьте требуемые import'ы.import (
_ "github.com/go-lintpack/lintpack/checkers"
)
- Build a dynamic library:
go build -buildmode=plugin -o linterPlugin.so linterPlugin.go
- Run the linter with the parameter
-pluginPath
:
./linter check -pluginPath=linterPlugin.so bytes
Warning: Support for dynamic modules is implemented through a plugin package that does not work on Windows.
The flag -verbose
can help you figure out which check is on or off, and, most importantly, which filter will turn off the check.
Обратите внимание, что panicNil
отображается в списке включенных проверок. Если мы уберём аргумент -pluginPath
, это перестанет быть истиной.
./linter check -verbose -pluginPath=./linterPlugin.so bytes
debug: appendCombine: disabled by tags (-disableTags)
debug: boolExprSimplify: disabled by tags (-disableTags)
debug: builtinShadow: disabled by tags (-disableTags)
debug: commentedOutCode: disabled by tags (-disableTags)
debug: deprecatedComment: disabled by tags (-disableTags)
debug: docStub: disabled by tags (-disableTags)
debug: emptyFallthrough: disabled by tags (-disableTags)
debug: hugeParam: disabled by tags (-disableTags)
debug: importShadow: disabled by tags (-disableTags)
debug: indexAlloc: disabled by tags (-disableTags)
debug: methodExprCall: disabled by tags (-disableTags)
debug: nilValReturn: disabled by tags (-disableTags)
debug: paramTypeCombine: disabled by tags (-disableTags)
debug: rangeExprCopy: disabled by tags (-disableTags)
debug: rangeValCopy: disabled by tags (-disableTags)
debug: sloppyReassign: disabled by tags (-disableTags)
debug: typeUnparen: disabled by tags (-disableTags)
debug: unlabelStmt: disabled by tags (-disableTags)
debug: wrapperFunc: disabled by tags (-disableTags)
debug: appendAssign is enabled
debug: assignOp is enabled
debug: captLocal is enabled
debug: caseOrder is enabled
debug: defaultCaseOrder is enabled
debug: dupArg is enabled
debug: dupBranchBody is enabled
debug: dupCase is enabled
debug: dupSubExpr is enabled
debug: elseif is enabled
debug: flagDeref is enabled
debug: ifElseChain is enabled
debug: panicNil is enabled
debug: regexpMust is enabled
debug: singleCaseSwitch is enabled
debug: sloppyLen is enabled
debug: switchTrue is enabled
debug: typeSwitchVar is enabled
debug: underef is enabled
debug: unlambda is enabled
debug: unslice is enabled
# ... результат работы линтера.
Comparison with gometalinter and golangci-lint
To avoid confusion, it is worth describing the main differences between the projects.
gometalinter and golangci-lint primarily integrate other, often very differently implemented, linters, provide convenient access to them. They target end users who will use static analyzers.
lintpack simplifies the creation of new linters, provides a framework that makes different packages, implemented on its basis, compatible within one executable file. These checks (for golangci-lint) or the executable file (for the gometalinter) can then be embedded in the aforementioned meta linters.
Suppose some of the lintpack
compatible checks is part of golangci-lint
. If there is any problem related to the convenience of its use, this may be a zone of responsibility golangci-lint
, but if we are talking about an error in the implementation of the verification itself, then this is the problem of the authors of the verification, the lintpack ecosystem.
In other words, these projects solve various problems.
And what about the go-critic?
Porting go-critic
on lintpack
almost completed. work-in-progress can be found in the go-critic / checkers repository . After the transition is complete, the checks will be moved to go-critic/go-critic/checkers
.
# Установка go-critic до:
go get -v github.com/go-critic/go-critic/...
# Установка go-critic после:
lintpack -o gocritic github.com/go-critic/go-critic/checkers
There go-critic
is golangci-lint
no great sense to use out , but it lintpack
can allow to establish those checks that are not included in the set go-critic
. For example, it may be a diagnosis written by you.
To be continued
How to create your own- lintpack
compatible checks you will learn in the next article.
In the same place, we will analyze what advantages you get when you implement your linter on the basis lintpack
of compared with the implementation from scratch.
I hope you have an appetite for new checks for Go. Let us know how static analysis will become too much, we will quickly solve this problem together.