Что такое Android Lint и как он помогает писать поддерживаемый код


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


Кроме того, в Java и Kotlin, как и в любых других языках программирования, есть свои собственные конструкции, которые могут привести к снижению производительности.


Привет, Lint


Мы используем инструмент под названием Lint (или Linter), чтобы избегать таких проблем. Lint — это инструмент для статического анализа кода, который помогает разработчикам уловить потенциальные проблемы ещё до того, как код скомпилируется. Он выполняет многократные проверки исходного кода, которые могут обнаружить такие проблемы, как неиспользуемые переменные или аргументы функций, упрощение условий, неправильные области видимости, неопределённые переменные или функции, плохо оптимизированный код и т.д. Когда мы говорим о разработке для Android, существуют сотни проверок Lint, доступных «из коробки».


Но иногда нам нужно найти конкретные проблемы в нашем коде, которые не охватываются этими существующими проверками.


Привет, пользовательские проверки Lint


Прежде чем мы начнем кодить, давайте определим нашу цель и посмотрим, как реализовать её шаг за шагом с помощью Lint API. Цель состоит в том, чтобы создать проверку для обнаружения неправильного вызова метода для объекта. Идея этой проверки состоит в том, чтобы определить, является ли метод установки слушателя на View-компонент таким, который будет прерывать несколько последовательных кликов по компоненту, чтобы мы могли избежать открытия одной и той же Activity или обращения к сети несколько раз.


Пользовательские проверки Lint написаны как часть стандартного модуля Java (или Kotlin). Самый простой способ начать — создать простой проект на основе Gradle (это не обязательно должен быть проект Android).


Затем добавим зависимости Lint. В файле build.gradle вашего модуля добавьте:


compileOnly "com.android.tools.lint:lint-api:$lintVersion"
compileOnly "com.android.tools.lint:lint-checks:$lintVersion"

Теперь есть хитрость, о которой я узнал, исследуя эту тему. lintVersion должен быть gradlePluginVersion + 23.0.0. gradlePluginVersion — это переменная, определённая в файле build.gradle на уровне проекта. И на данный момент последняя стабильная версия — 3.3.0. Это означает, что lintVersion должен быть 26.3.0.


Каждая проверка Линт состоит из 4 частей:


  • Проблема — проблема в нашем коде, которую мы пытаемся предотвратить. Когда проверка Lint завершается неудачей, то об этом сообщается разработчику.
  • Детектор — инструмент для поиска проблемы, который предоставляет API Lint.
  • Реализация — область, в которой может возникнуть проблема (исходный файл, файл XML, скомпилированный код и т.д.).
  • Реестр — настраиваемый реестр проверок Lint, который будет использоваться вместе с существующим реестром, содержащим предопределённые проверки.

Реализация


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


val correctClickListenerImplementation = Implementation(CorrectClickListenerDetector::class.java, Scope.JAVA_FILE_SCOPE)

Помните, что Scope.JAVA_FILE_SCOPE также будет работать для классов Kotlin.


Проблема


Следующим шагом является использование этой реализации для определения проблемы. Каждая проблема состоит из нескольких частей:


  • ID — уникальный идентификатор.
  • Описание — краткое (5-6 слов) изложение проблемы.
  • Объяснение — полное объяснение проблемы с предложением, как это исправить.
  • Категория — категория проблемы (производительность, перевод, безопасность и т.д.).
  • Приоритет — важность проблемы, в диапазоне от 1 до 10, где 10 является самым высоким. Это будет использоваться для сортировки проблем в отчёте, созданном при запуске Lint.
  • Серьёзность — серьёзность проблемы (фатальная, ошибка, предупреждение, информация или игнорирование).
  • Реализация — реализация, которая будет использоваться для обнаружения этой проблемы.

val ISSUE_CLICK_LISTENER = Issue.create(
    id = "UnsafeClickListener",
    briefDescription = "Unsafe click listener", 
    explanation = """"
        This check ensures you call click listener that is throttled 
        instead of a normal one which does not prevent double clicks.
        """.trimIndent(),
    category = Category.CORRECTNESS,
    priority = 6,
    severity = Severity.WARNING,
    implementation = correctClickListenerImplementation
)

Детектор


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


  • UastScanner — файлы Java или Kotlin (UAST — Unified Abstract Syntax Tree (русс. унифицированное абстрактное синтаксическое дерево)).
  • ClassScanner — скомпилированные файлы (байт-код).
  • BinaryResourceScanner — двоичные ресурсы, такие как растровые изображения или файлы res/raw.
  • ResourceFolderScanner — папки ресурсов (не конкретные файлы в них).
  • XmlScanner — XML-файлы.
  • GradleScanner — Gradle-файлы.
  • OtherFileScanner — всё остальное.

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


Теперь мы готовы реализовать детектор, который будет проверять правильный вызов метода для объекта.


private const val REPORT_MESSAGE = "Use setThrottlingClickListener"

/**
 * Пользовательский класс детектора, который расширяет базовый класс детектора и конкретный 
 * интерфейс в зависимости от того, какую часть кода мы хотим проанализировать.
 */
class CorrectClickListenerDetector : Detector(), Detector.UastScanner {

    /**
    * Метод, который определяет, какие элементы кода мы хотим проанализировать.
    * Существует много похожих методов для различных элементов в коде,
    * но для нашего варианта использования мы хотим проанализировать вызовы методов, 
    * поэтому мы возвращаем только один элемент, представляющий вызовы методов.
    */
    override fun getApplicableUastTypes(): List<Class<out UElement>>? {
        return listOf<Class<out UElement>>(UCallExpression::class.java)
    }

    /**
    * Поскольку мы определили применимые типы UAST, мы должны переопределить метод, 
    * который создаст обработчик UAST для этих типов. Обработчик требует реализации UElementHandler, 
    * который является классом, который определяет ряд различных методов, 
    * которые обрабатывают такие элементы, как аннотации, разрывы, циклы, импорт и т.д.
    * В нашем случае мы определили только выражения вызова, поэтому мы переопределяем 
    * только этот один метод. Реализация метода довольно проста — он проверяет, имеет ли 
    * вызываемый метод имя, которого мы хотим избежать, и сообщает о проблеме в противном случае.
    */
    override fun createUastHandler(context: JavaContext): UElementHandler? {
        return object: UElementHandler() {

            override fun visitCallExpression(node: UCallExpression) {
                if (node.methodName != null && node.methodName?.equals("setOnClickListener", ignoreCase = true) == true) {
                    context.report(ISSUE_CLICK_LISTENER, node, context.getLocation(node), REPORT_MESSAGE, createFix())
                }
            }
        }
    }

    /**
     * Метод создаст рекомендуемое исправление, которое можно будет 
     * вызвать в IDE, и заменит неправильный метод на правильный.
     */
    private fun createFix(): LintFix {
        return fix().replace().text("setOnClickListener").with("setThrottlingClickListener").build()
    }
}

Реестр


Последнее, что нам нужно сделать, это добавить проблемы в наш реестр и сообщить Lint, что существует специальный реестр проблем, который он должен использовать наряду со стандартным.


class MyIssueRegistry : IssueRegistry() {
    override val issues: List<Issue> = listOf(ISSUE_CLICK_LISTENER)
}

В build.gradle уровня модуля:


jar {
    manifest {
        attributes("Lint-Registry-v2": "co.infinum.lint.MyIssueRegistry")
    }
}

где co.infinum.lint — это пакет класса MyIssueRegistry. Теперь вы можете запустить задачу jar, используя скрипт gradlew, и библиотека должна появиться в каталоге build/libs.


Здесь есть ещё один пример пользовательской проверки Lint, где вы можете увидеть, как обрабатывать XML-файлы.


Использование


Ваша новая проверка Lint готова к использованию в проекте. Если эту проверку можно применить ко всем проектам, вы можете поместить её в папку ~/.android/lint (вы можете создать её, если она ещё не существует).


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


Стоит ли оно того?


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

Комментарии (0)