Go lintpack: менеджер компонуемых линтеров

65mxmsgairo1a_s7civi_cgklqq.png

lintpack — это утилита для сборки линтеров (статических анализаторов), которые написаны с использованием предоставляемого API. На основе него сейчас переписывается знакомый некоторым статический анализатор go-critic.

Сегодня мы подробнее разберём что такое lintpack с точки зрения пользователя.

go-critic начинался как экспериментальный проект, который являлся песочницей для прототипирования практически любых идей в области статического анализа для Go.

Приятным удивлением было то, что некоторые люди действительно отправляли реализации детекторов тех или иных проблем в коде. Всё было под контролем, пока не начал накапливаться технический долг, устранять который было практически некому. Люди приходили, добавляли проверку, а затем исчезали. Кто после этого должен исправлять ошибки и дорабатывать реализацию?

Знаменательным событием было предложение добавить проверки, требующие дополнительной конфигурации, то есть такие, которые зависят от локальных для проекта договорённостей. Примером является выявление наличия copyright заголовка в файле (license header) по особому шаблону или запрет импортирования некоторых пакетов с предложением заданной альтернативы.

Другой трудностью была расширяемость. Отправлять свой код в чужой репозиторий удобно не каждому. Некоторым хотелось динамического подключения своих проверок, чтобы не нужно было модифицировать исходные коды go-critic.

Резюмируя, вот проблемы, которые стояли на пути развития go-critic:


  • Груз сложности. Слишком много поддерживать, наличие бесхозного кода.
  • Низкий средний уровень качества. experimental означал как «почти готово к использованию», так и «лучше не запускать вообще».
  • Иногда трудно принимать решение включения проверки в go-critic, а отклонять их противоречит исходной философии проекта.
  • Разные люди видели go-critic по-разному. Большинству хотелось иметь его в виде CI линтера, который идёт в поставке с gometalinter.

Чтобы хоть как-то ограничить количество разночтений и несовпадающих интерпретаций проекта, был написан манифест.


Если вам хочется дополнительного исторического контекста и ещё больше размышлений на тему категоризации статических анализаторов, можете послушать запись GoCritic — новый статический анализатор для Go. В тот момент lintpack ещё не существовал, но часть идей родилась именно в тот день, после доклада.

А что если бы нам не нужно было хранить все проверки в одном репозитории?

_hs3wriufjyktqsybtqsnkhka6m.png

go-critic состоит из двух основных компонентов:


  1. Реализация самих проверок.
  2. Программа, которая загружает проверяемые Go пакеты и запускает на них проверки.

Наша цель: иметь возможность хранить проверки для линтера в разных репозиториях и собирать их воедино, когда это необходимо.

lintpack делает именно это. Он определяет функции, позволяющие описывать свои проверки таким образом, что их затем можно запускать через генерируемый линтер.


Пакеты, которые реализованы с использованием lintpack как фреймворка, будем называть lintpack-совместимыми или lintpack-compatible пакетами.

Если бы сам go-critic был реализован на основе lintpack, все проверки можно было бы разделить на несколько репозиториев. Одним из вариантов разделения может быть следующий:


  1. Основной набор, куда попадают все стабильные и поддерживаемые проверки.
  2. contrib репозиторий, где лежит код, который либо слишком экспериментальный, либо не имеет меинтейнера.
  3. Что-то вроде go-police, где могут находится те самые настраиваемые под конкретный проект проверки.

Первый пункт имеет особо важное значение в связи с интеграцией go-critic в golangci-lint.

Если оставаться на уровне go-critic, то для пользователей практически ничего не изменилось. lintpack создаёт почти идентичный прежнему линтер, а golangci-lint инкапсулирует все различающиеся детали реализации.

Но кое-что всё же изменилось. Если на основе lintpack будут создаваться новые линтеры, у вас появится более богатый выбор готовых диагностик для генерации линтера. На минуту представим, что это так, и в мире существует более 10 разных наборов проверок.

jkkn2fql7m45ueypxb7dytrvol8.jpeg

Для начала, нужно установить сам lintpack:

# lintpack будет установлен в `$(go env GOPATH)/bin`.
go get -v github.com/go-lintpack/lintpack/...

Создадим линтер, используя тестовый пакет из lintpack:

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

В набор входит panicNil, который находит в коде panic(nil) и просить выполнить замену на что-то различимое, поскольку в противном случае recover() не сможет подсказать, был ли вызван panic с nil аргументом, или паники не было вовсе.

Запускать линтер можно на отдельных файлах, аргументами типа ./... или пакетах (по их import пути).

./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

По умолчанию данная проверка также реагирует на panic(interface{}(nil)). Чтобы переопределить это поведение, нужно установить значение skipNilEfaceLit в true. Сделать это можно через командную строку:

$mylinter check -@panicNil.skipNilEfaceLit=true ./panicNil/
./panicNil/positive_tests.go:5:3: panicNil: panic(nil) calls are discouraged


usage для cmd/lintpack и генерируемого линтера

И 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
# Информация о всех доступных флагах.

Ответ на вопрос «как узнать о том самом параметре skipNilEfaceLit?» — read the fancy manual (RTFM)!

Вся документация об установленных проверках находится внутри mylinter. Доступна эта документация через подкоманду 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)

Подобно поддержке шаблонов в go list -f, вы можете передать строку шаблона, которая отвечает за формат вывода документации, что может быть полезным при составлении markdown документов.

Для упрощения поиска полезных наборов проверок есть централизованный список lintpack-совместимых пакетов: https://go-lintpack.github.io/.

Вот некоторые из списка:

Этот список периодически обновляется и он открыт для заявок на добавление. Любой из этих пакетов может использоваться для создания линтера.

Команда ниже создаёт линтер, который содержит все проверки из списка выше:

# Сначала нужно убедиться, что исходные коды всех проверок
# доступны для 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 включает все проверки на этапе компиляции, получаемый линтер может быть размещён в окружении, где отсутствуют исходные коды реализации установленных диагностик, всё как обычно при статической линковке.

В дополнение к статической сборке есть возможность загружать плагины, предоставляющие дополнительные проверки.

Особенностью является то, что реализация чекера не знает, будут ли её использовать при статической компиляции или будут подгружать в виде плагина. Никаких изменений в коде не требуется.

Допустим, мы хотим добавить panicNil в линтер, но мы не имеем возможности пересобрать его из всех исходников, которые использовались при первой компиляции.


  1. Создаём linterPlugin.go:
package main

// Если требуется включить в плагин более одного набора проверок,
// просто добавьте требуемые import'ы.
import (
    _ "github.com/go-lintpack/lintpack/checkers"
)


  1. Собираем динамическую библиотеку:
go build -buildmode=plugin -o linterPlugin.so linterPlugin.go


  1. Запускаем линтер с параметром -pluginPath:
./linter check -pluginPath=linterPlugin.so bytes


Предупреждение: Поддержка динамических модулей реализована через пакет plugin, который не работает на Windows.

Флаг -verbose может помочь разобраться какая проверка включена или выключена, а, самое главное, там будет отображено какой из фильтров отключил проверку.


Пример с -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
# ... результат работы линтера.

Во избежание путаницы, стоит описать основные различия между проектами.

gometalinter и golangci-lint в первую очередь интегрируют другие, зачастую очень по-разному реализованные, линтеры, предоставляют к ним удобный доступ. Они нацелены на конечных пользователей, которые будут использовать статические анализаторы.

lintpack упрощает создание новых линтеров, предоставляет фреймворк, делающий разные пакеты, реализованные на его основе, совместимыми в пределах одного исполняемого файла. Эти проверки (для golangci-lint) или исполняемый файл (для gometalinter) далее могут быть встроены в вышеупомянутые мета-линтеры.

Допустим, какая-то из lintpack-совместимых проверок является частью golangci-lint. Если существует какая-то проблема, связанная с удобством её использования — это может быть зоной ответственности golangci-lint, но если речь идёт об ошибке в реализации самой проверки, то это проблема авторов проверки, lintpack экосистемы.

Иными словами, эти проекты решают разные проблемы.

Процесс портирования go-critic на lintpack уже почти завершён. work-in-progress можно найти в репозитории go-critic/checkers. После завершения перехода, проверки будут перемещены в 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

Большого смысла использовать go-critic вне golangci-lint нет, а вот lintpack может позволить установить те проверки, которые не входят в набор go-critic. Например, это могут быть диагностики, написанные вами.

Как создавать свои lintpack-совместимые проверки вы узнаете в следующей статье.

Там же мы разберём какие преимущества вы получаете при реализации своего линтера на основе lintpack по сравнению с реализацией с чистого листа.

Надеюсь, у вас появился аппетит к новым проверкам для Go. Дайте знать, как статического анализа станет слишком много, будем оперативно решать эту проблему вместе.

© Habrahabr.ru