Detekt — пишем свои правила

117f2787656d38f364ac8716fdd4bc26.png

Мы в «Ситимобил», используем статический анализатор кода Detekt. Это инструмент, который при запуске проходит по проекту и показывает допущенные в коде code smell. И самостоятельно исправляет некоторые из них, если вы включите эту функцию. Detekt решает такие проблемы, как:

  • трата времени команды на дискуссии о незначительных правках (стиль кода);

  • отсутствие единообразного стиля кода в большой команде;

  • незнание разработчиками некоторых best-practices, которые отражены в наборе правил.

Хоть у Detekt из коробки достаточно большая база правил, иногда этого недостаточно. Наверняка у вас есть свои договоренности о том, как писать код в проекте, какие конструкции использовать, какие нет и какие на усмотрение разработчика, и это называется code-style of your awesome project! И вот вы на очередной встрече решили, каким будет ваш код, а соответствующего правила в списке Detekt не нашли. Как настоящий программист вы не унываете и решаете написать своё правило, с блекджеком и корутинами. А как это сделать, мы расскажем в этой статье!

Шаг 1: создаём модуль

Создаём модуль для наших новых правил и назовём его detekt-custom-rules. Сразу идем в .gradle модуля и удаляем всё, что там есть, оно нам не понадобится. Нужно подключить плагин Kotlin и несколько зависимостей. Получится вот такой файл:

apply plugin: 'kotlin'

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin"
    implementation "io.gitlab.arturbosch.detekt:detekt-api:$versions.detekt"
    implementation "io.gitlab.arturbosch.detekt:detekt-cli:$versions.detekt"
}

Затем нужно создать специальный класс Provider, в котором будут перечислены наши кастомные правила:

package provider

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.RuleSet
import io.gitlab.arturbosch.detekt.api.RuleSetProvider

class CustomDetektRuleSetProvider : RuleSetProvider {

    override val ruleSetId = "custom-detekt-rules"

    override fun instance(config: Config) = RuleSet(
        id = ruleSetId,
        rules = listOf(
            // тут будет наше правило
        )
    )
}

А теперь зарегистрируем его: создаём ресурсную папку, папку META_INF, еще services, и в ней текстовый файл io.gitlab.arturbosch.detekt.api.RuleSetProvider. В файле нужно будет указать путь до нашего класса. Вот что получится:

d17944161168696d2fe096d5e42aef99.png

А внутри текстового файла будет в нашем случае так:

2bc86a61113cbc4a0b3d1575fca52a3a.png

Напоследок нужно подключить наш модуль там же, где описана задача Detekt:

dependencies {
    detektPlugins detektFormattingPlugin
    detekt project(':detekt-custom-rules')
}

Чтобы вам было понятно, где именно мы подключали модуль, приведем и наш вариант подключения Detekt. Мы вынесли его задачи в отдельный файл detekt.gradle, и выглядит он так:

buildscript {
    apply from: "dependencies.gradle"
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath detektPlugin
    }
}

apply plugin: io.gitlab.arturbosch.detekt.DetektPlugin

tasks {
    task detektAll(type: io.gitlab.arturbosch.detekt.Detekt) {
       // только проверяет
    }

    task detektFixAll(type: io.gitlab.arturbosch.detekt.Detekt) {
       // проверяет и исправляет
    }

    task detektAllCreateBaseline(type: io.gitlab.arturbosch.detekt.DetektCreateBaselineTask) {
        // создает/обновляет baseline файл
    }
}

dependencies {
    detektPlugins detektFormattingPlugin
    detekt project(':detekt-custom-rules')
}

А подключается этот файл в build.gradle проекта:

apply from: "detekt/detekt.gradle"

Делаем Clean Project && sync, и мы готовы к следующему шагу.

Шаг 2: пишем своё первое правило

Создаем класс по пути: detekt-custom-rules/src/main/java/rules

class MyRule(config: Config = Config.empty) : Rule(config) {
    // code
}

Переопределяем константу Issue, где:

  • id — идентификатор, по которому мы можем включить или выключить правило в detekt-config.yml. Важно, чтобы в yml и у идентификатор были одинаковые строковые значения;

  • description — короткое описание, которое можно увидеть в .html-отчёте;

  • severity — enum с возможными значениями, рекомендую ставить просто CodeSmell, это не так важно. Но можно самостоятельно изучить другие типы :);

  • debt — описывает предполагаемый объём работы, необходимой для устранения данной проблемы.

override val issue = Issue(
    id = "myAwesomeRule1",
    description = "Must use the MyRule!",
    severity = Severity.CodeSmell,
    debt = Debt.FIVE_MINS
)

Переопределяем один из методов visit. Их очень много, ознакомьтесь самостоятельно и выберите необходимый метод под конкретные нужды. Стоит учесть, что если используете visitKtFile, то это может ухудшить сложность прохода. Чтобы ускорить выполнение, нужно стараться подобрать наиболее подходящий visit-метод.

override fun visitNamedFunction(function: KtNamedFunction) {
    // посетили функцию
}

override fun visit(root: KtFile) {
    // посетили файл
}

override fun visitIfExpression(expression: KtIfExpression) {
    // посетили if блок
}

Теперь проинформируем анализатор о найденном codeSmell. Тут нужно считать offset, чтобы в ссылке в консоли или отчётах указатель был на той строке, где произошла ошибка. Иначе может быть непонятно, где конкретно в файле codeSmell, который нужно исправить.

override fun visitNamedFunction(function: KtNamedFunction) {

    var offset = 0
    val lines = function.text.lines()

    for (line in lines) {
        offset += line.length
        if (YOUR_CONDITION) {
            report(
                CodeSmell(
                    issue = issue,
                    entity = Entity.from(function, offset),
                    message = "Your message for the detekt.html report"
                )
             )
    }

    offset += 1 // '\n'
}

Шаг 3: добавляем новое правило в Provider

Мы создали класс CustomDetektRuleSetProvider, и теперь нужно добавить новое правило в список — rules.

package provider

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.RuleSet
import io.gitlab.arturbosch.detekt.api.RuleSetProvider

class CustomDetektRuleSetProvider : RuleSetProvider {

    override val ruleSetId = "custom-detekt-rules"

    override fun instance(config: Config) = RuleSet(
        id = ruleSetId,
        rules = listOf(
            MyAwesomeRule1(config),
            MyAwesomeRule2(config),
        )
    )
}

Шаг 4: добавляем новое правило в detekt-config.yml

Находим config в .yml с правилами Detekt:

config:
  validation: true
  warningsAsErrors: false
  excludes: "custom-detekt-rules"

Затем добавим новое правило в .yml и сделаем его активным, при этом название правила обязательно должно совпадать с id в Issue в шаге 1.

custom-detekt-rules:
  myAwesomeRule1:
    active: true
  myAwesomeRule2:
    active: false

Шаг 5: Clean project

Detekt кеширует данные, поэтому перед запуском нужно обязательно выполнить Build → Clean Project.

Шаг 6: обновляем файл baseline.xml

Новое правило, скорее всего, вызовет множество новых codeSmell, связанных с только что написанным правилом. И может не оказаться времени исправить сразу все их. Поэтому можно запустить задачу ./gradlew detektAllCreateBaseline, тем самым обновив baseline-файл, и в него добавятся новые codeSmell, которые будут игнорироваться при дальнейших запусках анализатора.

Наш пример правила

package rules

import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import org.jetbrains.kotlin.psi.KtNamedFunction

class NeedToUseSubscribeByRule(config: Config = Config.empty) : Rule(config) {

    companion object {
        private const val TRIGGER_VALUE = ".subscribe("
    }

    override val issue = Issue(
        id = "NeedToUseSubscribeBy",
        description = "Must use a .subscribeBy(...) ext instead .subscribe(...)",
        severity = Severity.CodeSmell,
        debt = Debt.FIVE_MINS
    )

    override fun visitNamedFunction(function: KtNamedFunction) {
        super.visitNamedFunction(function)

        var offset = 0
        val lines = function.text.lines()

        for (line in lines) {
            offset += line.length
            if (line.contains(TRIGGER_VALUE)) {
                report(
                    CodeSmell(
                        issue = issue,
                        entity = Entity.from(function, offset),
                        message = "The function ${function.name} using RxJava chain. " +
                            "You must use a .subscribeBy(...) ext instead .subscribe(...) here."
                    )
                )
            }
            offset += 1 // '\n'
        }
    }
}

Примеры на GitHub

Самый простой способ писать свои правила — подсмотреть у авторов проекта. Советую подобрать в GitHub-репозитории Detekt правило, которое похоже на ваше. На всякий случай оставлю тут несколько ссылок:)

Другие примеры можно найти в репозитории https://github.com/detekt/detekt

© Habrahabr.ru