📜 ⬆️ ⬇️

Go lintpack: composable linters 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 lintpack in more detail what 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 ideas 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 did not need to modify the source codes of the go-critic .


Summarizing, here are the problems that stood in the way of the development of the go-critic :



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:


  1. The implementation of the checks themselves.
  2. 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 using lintpack as a framework will be called lintpack -compatible or lintpack -compatible packages.

If the go-critic was implemented on the basis of lintpack , all the checks could be divided into several repositories. One of the options for separation may be the following:


  1. The core set, where all the stable and supported checks fall.
  2. The contrib repository, where the code lies, which is either too experimental or does not have a maintainer.
  3. 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 remain at the go-critic level, then almost nothing has changed for users. lintpack creates an almost identical linter, and golangci-lint encapsulates all the different implementation details.


But something has changed. If new lintpack are created on the basis of the lintpack , you will have a richer choice of ready-made diagnostics for the linter generation. 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 lintpack itself:


 # lintpack    `$(go env GOPATH)/bin`. go get -v github.com/go-lintpack/lintpack/... 

Create a linter using a lintpack test package:


 lintpack build -o mylinter github.com/go-lintpack/lintpack/checkers 

The set includes panicNil , which finds panic(nil) in the code and panicNil to perform a replacement with something distinguishable, because otherwise, recover() cannot tell if panic was called with the nil argument, or there was no panic at all.


Example with panic (nil)


The code below attempts to describe the value obtained from recover() :


 r := recover() fmt.Printf("%T, %v\n", r, r) 

The result will be identical for panic(nil) and for a program that does not panic(nil) .


A runable example of the described behavior .




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 of 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 

usage for cmd / lintpack and generated linter


Both lintpack and the generated linter use the first argument to select a subcommand. A list of available subcommands and examples of their launch can be obtained by calling the utility without arguments.


 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 

Suppose we called the created linter named 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 

For some subcommands, the -help flag is available, which provides additional information (I cut some lines that are too wide):


 ./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 for installed checks is inside mylinter . This documentation is available through the doc subcommand:


 #     : $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) 

Like the 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.imtqy.com/ .


Here are some of the list:



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 the source codes for the implementation of the installed diagnostics are missing, 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 plugin. No code changes are required.


Suppose we want to add panicNil to the linter, but we cannot rebuild it from all the sources that were used during the first compilation.


  1. Create linterPlugin.go :

 package main //         , //    import'. import ( _ "github.com/go-lintpack/lintpack/checkers" ) 

  1. Build a dynamic library:

 go build -buildmode=plugin -o linterPlugin.so linterPlugin.go 

  1. Run the linter with the -pluginPath parameter:

 ./linter check -pluginPath=linterPlugin.so bytes 

Warning: Support for dynamic modules is implemented through a plugin package that does not work on Windows.

The -verbose flag can help you figure out which check is on or off, and, most importantly, which filter will turn off the check.


Example with -verbose


Please note that panicNil displayed in the list of included checks. If we remove the -pluginPath argument, it will cease to be true.


 ./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 the same executable file. These checks (for golangci-lint) or the executable file (for gometalinter) can then be embedded in the above meta-linters.


Suppose one 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 the responsibility of golangci-lint , but if we are talking about an error in the implementation of the test itself, then this is the problem of the authors of the test, the lintpack ecosystem.


In other words, these projects solve various problems.


And what about the go-critic?


The process of porting a go-critic to lintpack is almost complete. work-in-progress can be found in the go-critic / checkers repository. After the transition is completed, the checks will be moved to the 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 is no big sense in using a go-critic outside of golangci-lint , but lintpack can allow to install those checks that are not included in the 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 selling your lintpack based lintpack compared to selling it 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.


')

Source: https://habr.com/ru/post/430196/


All Articles