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 lintpackfrom 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. experimentalmeant 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-criticdifferently. Most wanted to have it in the form of a CI linter that comes in delivery with gometalinter.

    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 lintpackas a framework will be called lintpack-compatible or lintpack-compatible packages.

    If it go-criticwas 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:

    1. The core set, where all the stable and supported checks fall.
    2. The contrib repository contains code that is either too experimental or does not have a meinteyner.
    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 stay at the level go-critic, for users, almost nothing has changed. lintpackcreates an almost identical linter, and golangci-lintencapsulates all the different implementation details.

    But something has changed. If lintpacknew 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 panicwith an nilargument, or if there was no panic at all.

    Example with panic (nil)

    Код ниже пытается описать значение, полученное из 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.
    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 skipNilEfaceLitto 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

    И 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:

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

    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 \

    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 panicNilto the linter, but we cannot reassemble 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 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 -verbosecan help you figure out which check is on or off, and, most importantly, which filter will turn off the check.

    Example with -verbose

    Обратите внимание, что 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 lintpackcompatible 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-criticon lintpackalmost 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-criticis golangci-lintno great sense to use out , but it lintpackcan 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- lintpackcompatible 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 lintpackof 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.

    Also popular now: