Хабрахабр

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Для начала, нужно установить сам 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

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

$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)!

Доступна эта документация через подкоманду doc: Вся документация об установленных проверках находится внутри mylinter.

# Выводит список всех установленных проверок:
$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

Если мы уберём аргумент -pluginPath, это перестанет быть истиной. Обратите внимание, что panicNil отображается в списке включенных проверок.

./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 в первую очередь интегрируют другие, зачастую очень по-разному реализованные, линтеры, предоставляют к ним удобный доступ.

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть