Статический анализ#

Инструменты статического анализа проверяют код на соответствие правилам качества без его выполнения. В проекте используется WartRemover — расширяемый анализатор для Scala, работающий на этапе компиляции.

Статический анализ выполняется во время компиляции кода.

  • Частичная сборка - покажет где в измененных файлах есть ошибки.

  • Полная пересборка - покажет где в исходниках есть ошибки.

WartRemover предоставляет набор встроенных правил для выявления потенциальных проблем в коде. Ссылка ведёт на полный список правил, доступных в инструменте.

Настройка правил#

Где задаются правила

Правила можно задать двумя способами — локально в файле сборки модуля build.sbt или централизованно для всех модулей в общем плагине CommonSettings модуля sbt-plugin.

Файл build.sbt — это файл сборки конкретного модуля. В нём задаются правила, которые применяются только к этому модулю.

В build.sbt можно:

  • добавить собственные кастомные правила;

  • изменить уровень серьёзности правил из плагина CommonSettings для текущего модуля;

  • убрать правила, настроенные в плагине CommonSettings.

Плагин CommonSettings в модуле sbt-plugin — это общий плагин сборки, распространяющийся на все модули (включая проектные). По умолчанию все правила из этого плагина действуют на все модули.

Уровни серьёзности правил

Для каждого правила доступны два уровня серьёзности:

  • wartremoverErrors — ошибка компиляции. Сборка останавливается при обнаружении нарушения.

  • wartremoverWarnings — предупреждение. Сборка продолжается, но в выводе отображается сообщение.

Уровень серьёзности можно изменить, перенеся правило из wartremoverWarnings в wartremoverErrors или наоборот.

Примеры указания правила в файле сборки (build.sbt или CommonSettings) с разными уровнями серьёзности:

// Правила как ошибки
wartremoverErrors ++= Seq(
  Wart.custom("ru.bitec.warts.NetWart"),
  Wart.ListAppend
)
// Правила как предупреждения
wartremoverWarnings ++= Seq(
  Wart.custom("ru.bitec.warts.NetWart"),
  Wart.ListAppend
)

Размещение правил

Код правил (файлы .scala) размещается в отдельной sbt-библиотеке:

  • Для разработчиков вендора: sbt-warts, пакет ru.bitec.warts.

  • Для заказчика: отдельная sbt-библиотека в своём проекте.

Эта библиотека подключается к файлу сборки (build.sbt или CommonSettings) как зависимость через настройку wartremoverDependencies.

wartremoverDependencies ++= Seq(
  "ru.bitec" %% "sbt-warts" % "1.1.+"
)

Внимание

  • Подключение библиотеки делает правило доступным, но не активным. Чтобы правило заработало, его нужно дополнительно указать в файле сборки.

  • Если правило должно использоваться в нескольких модулях, зависимость wartremoverDependencies необходимо прописать в build.sbt каждого из этих модулей.

Написание правил#

Чтобы добавить новое кастомное правило, выполните следующие шаги:

1. Создайте sbt-библиотеку для правил и подключите её

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

  1. Создайте свой проект.

  2. В созданном проекте настройте файл build.sbt:

name := "my-warts"
scalaVersion := "2.13.10"
libraryDependencies += "org.wartremover" %% "wartremover" % "3.0.5"
  1. Опубликуйте проект локально:

   sbt publishLocal

После каждого изменения правил необходимо повторно выполнить sbt publishLocal, чтобы зависимые проекты получили обновлённую версию библиотеки.

  1. Подключите проект как зависимость в build.sbt модуля, где правило будет активно:

wartremoverDependencies ++= Seq("my-warts" %% "my-warts" % "1.0")

Примечание

Для разработчиков вендора библиотека уже существует — sbt-warts. Создавать новый проект не требуется.

2. Создайте правило

Создайте файл .scala и опишите в нем само правило, какие конструкции кода нужно проверять и как реагировать на их обнаружение.

Пример правила, запрещающего использование null:

package ru.bitec.warts

import org.wartremover.{WartTraverser, WartUniverse}

object NoNullWart extends WartTraverser {
  def apply(u: WartUniverse): u.Traverser = {
    import u.universe._

    new u.Traverser {
      override def traverse(tree: Tree): Unit = {
        tree match {
          // Пропускаем участки кода с аннотацией @SuppressWarnings
          case t if hasWartAnnotation(u)(t) =>

          // Если встретили литерал null — выводим предупреждение
          case Literal(Constant(null)) =>
            warning(u)(tree.pos, "Использование null запрещено. Используйте Option.")

          case _ =>
            super.traverse(tree)
        }
      }
    }
  }
}

3. Разместите файл с правилом

Сохраните файл в папку src/main/scala вашей библиотеки с правильным путём пакета:

  • Для разработчиков вендора: проект sbt-warts, пакет ru.bitec.warts.

  • Для заказчика: отдельная sbt-библиотека в своём проекте.

4. Подключите правило в проекте

Добавьте правило в build.sbt модуля и задайте уровень серьёзности:

wartremoverWarnings ++= Seq(
  Wart.custom("ru.bitec.warts.NoNullWart")
)

Каждое правило необходимо явно добавить в настройки WartRemover. После этого при компиляции модуля WartRemover будет проверять код на соответствие новому правилу.

Анализ синтаксического дерева#

При написании правил полезно понимать, как выглядит код в виде синтаксического дерева. Для этого используйте функцию reify:

import scala.reflect.runtime.universe._
val tree = reify {
  println(2)
}.tree // это дерево не содержит информацию о типах

/* 
 Apply( вызов метода
      Select(
          Select(This(TypeName("scala")), TermName("Predef")), на чем вызывают метод
          TermName("println") что вызывают
        ),
      List(Literal(Constant(2))) параметры
    )
 */
val raw = showRaw(tree)

Этот пример показывает структуру вызова println(2). Аналогично можно анализировать любые конструкции, чтобы понять, как их отловить в своём правиле.

Полезные ресурсы

  • Официальная документация wartremover.

  • Документация по работе с синтаксическим деревом Scala Reflection.

Подавление правил#

Подавление отключает проверку правил в отдельных участках кода. Доступны два механизма:

  • @SuppressWarnings — подавляет конкретные правила WartRemover. Работает как с предупреждениями, так и с ошибками.

  • @nowarn — подавляет все предупреждения компилятора Scala. Применяется к встроенным проверкам компилятора и предупреждениям WartRemover. Правила, настроенные как ошибки (wartremoverErrors), не подавляются.

Оба механизма применяются к ближайшему объявляемому элементу: классу, методу, переменной или локальному блоку кода.

1. Подавление через @SuppressWarnings

При подавлении необходимо указать полный путь к правилу или группе правил в виде массива строк.

// Подавление одного правила
@SuppressWarnings(Array("ru.bitec.warts.AsciiIdentifiersWart"))
val переменная = 42

// Подавление нескольких правил
@SuppressWarnings(Array(
  "ru.bitec.warts.AsciiIdentifiersWart",
  "ru.bitec.warts.MixedLanguagesWart"
))
def метод() = { ... }

// Подавление всех правил wartremover (не рекомендуется)
@SuppressWarnings(Array("org.wartremover.warts.All"))
class класс { ... }

// Подавление для отдельного вызова метода
@SuppressWarnings(Array("ru.bitec.warts.NetWart"))
{
  httpClient.execute(request)
}

// Подавление внутри метода для локального блока
def process() = {
  @SuppressWarnings(Array("ru.bitec.warts.CommitWart"))
  {
    for ((item, i) <- items.zipWithIndex) {
      process(item)
      if (i % 100 == 0) session.commitWork()
    }
  }
}

2. Универсальное подавление через @nowarn

Подавляет все предупреждения компилятора для аннотированного элемента.

@nowarn
private[btk] case class GlClosableHttpClient(u: CloseableHttpClient)(implicit val session: Session) 
  extends CloseableHttpClient

Рекомендации для подавления

Подавление используйте только в обоснованных случаях — например, для совместимости с внешними библиотеками или при работе с унаследованным кодом. Для глобального отключения правила или изменения его уровня серьёзности настройте параметры в файле build.sbt.

Предпочтительнее указывать конкретное правило через @SuppressWarnings, а не применять универсальный @nowarn, чтобы не скрыть другие потенциальные проблемы в коде.