Android Lint: оптимизируем проверку мердж-реквестов

от автора

Привет, это Android-разработчик из МТС Диджитал Никита Пятаков. Когда я только начал работать над приложением «Мой МТС», мне нужно было время, чтобы адаптироваться и ознакомиться с проектом. На первых МР-ах коллеги подсвечивали готовые решения, которые можно переиспользовать. Когда к нам стали приходить новые разработчики, такие комментарии оставлял уже я. Это натолкнуло меня на мысль, что использование синтаксического анализатора оптимизирует процесс проверки. К тому моменту мы уже использовали Android Lint, так что выбирать не пришлось. 

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

К чему стремимся

Функция, которую мы используем вместо конструкции «?: false», имеет вид:

val Boolean?.safeBoolean: Boolean     get() {         return this == true     } 

В итоге мы хотим получить такой результат:

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

Issue

Сначала нам нужно создать issue — зарегистрировать новое правило для Lint:

class MyMtsIssueRegistry : IssueRegistry() {      override val api: Int         get() = CURRENT_API      override val issues: List<Issue>         get() = listOf(ISSUE_ELVIS_OPERATOR_WITH_FALSE)      companion object {          val ISSUE_ELVIS_OPERATOR_WITH_FALSE = Issue.create(                 id = "ElvisOperatorWithFalse",                 briefDescription = "Elvis expression with false is used",                 explanation = "Replace Elvis expression with .safeBoolean function",                 category = Category.CORRECTNESS,                 priority = 10,                 severity = Severity.WARNING,                 implementation = Implementation(ElvisOperatorWithFalseDetector::class.java, JAVA_FILE_SCOPE)         )     } }

Наследуемся от абстрактного класса IssueRegistry. Переопределяем два поля:

  • api — версия, с которой будут скомпилированы наши issue (можно указать актуальную CURRENT_API из Lint)

  • issues — список кастомных правил, добавляем ISSUE_ELVIS_OPERATOR_WITH_FALSE

В issue указываем:

  • id — должен быть уникальным

  • briefDescription — краткое описание проблемы

  • explanation — более подробное описание проблемы

  • category, priority — категоризация issue, можно задать любое (обычно используется для репортов)

  • severity — уровень серьёзности проблемы, Lint подсвечивает их по-разному. В нашем случае мы хотим, чтобы участок кода подчёркивался зелёным, выбираем WARNING

  • implementation — указываем детектор. В нём опишем процесс поиска кейса, который нужно поправить. Также нужно указать тип файлов, по которому Lint будет проходиться. Так как Kotlin-файлы декомпилируются в Java, используем JAVA_FILE_SCOPE. Lint умеет анализировать Gradle, manifest и так далее, здесь можно выбрать соответствующий scope

Детектор

class ElvisOperatorWithFalseDetector : Detector(), SourceCodeScanner {      override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(UIfExpression::class.java)      override fun createUastHandler(context: JavaContext): UElementHandler = ElvisOperatorWithFalseHandler(context) }

В Lint для обработки кода используется так называемое Uast-дерево, в виде которого представляется код. Например, для показанного на видео в начале статьи класса Test (до исправления) дерево будет выглядеть вот так:

UFile (package = ru.mts.accordion.presentation.extensions)     UImportStatement (isOnDemand = false)     UClass (name = Test)         UField (name = options)             UAnnotation (fqName = org.jetbrains.annotations.Nullable)         UMethod (name = boo)             UBlockExpression                 UReturnExpression                     UExpressionList (elvis)                         UDeclarationsExpression                             ULocalVariable (name = var223e1170)                                 UQualifiedReferenceExpression                                     UQualifiedReferenceExpression                                         USimpleNameReferenceExpression (identifier = options)                                         USimpleNameReferenceExpression (identifier = titleFontSize)                                     UCallExpression (kind = UastCallKind(name='method_call'), argCount = 0))                                         UIdentifier (Identifier (isEmpty))                                         USimpleNameReferenceExpression (identifier = isEmpty, resolvesTo = null)                         UIfExpression                             UBinaryExpression (operator = !=)                                 USimpleNameReferenceExpression (identifier = var223e1170)                                 ULiteralExpression (value = null)                             USimpleNameReferenceExpression (identifier = var223e1170)                             ULiteralExpression (value = false)         UMethod (name = Test)             UParameter (name = options)                 UAnnotation (fqName = org.jetbrains.annotations.Nullable)

Lint проходится по деревьям файлов в проекте по алгоритму, который мы опишем в детекторе, и если согласно этому алгоритму будет найден соответствующий участок кода, Lint его подсветит. UFile, UImportSatement, UClass — это всё интерфейсы-наследники UElement, об экземплярах которых (в реализации Java или Kotlin) можно получать необходимую информацию — например, представление кода в виде строки.

Чтобы написать своё правило, нужно унаследоваться от Detector, SourceCodeScanner и переопределить две функции:

  • getApplicableUastType — указываем, какие Uast-элементы нас интересуют

  • createUastHandler — подставляем свой обработчик для выбранных выше Uast-элементов

Так как оператор Элвиса декомпилируется в Java в виде простого if-else, нам нужен UIfExpression.

Обработчик

class ElvisOperatorWithFalseHandler(private val context: JavaContext) : UElementHandler() {     override fun visitIfExpression(node: UIfExpression) {         node.accept(object : AbstractUastVisitor() {             override fun afterVisitIfExpression(node: UIfExpression) {                 if ((node.uastParent as? UExpressionList)?.kind?.name == "elvis" &&                         node.elseExpression?.asRenderString() == "false") {                     val elvisExpressionString = node.uastParent?.sourcePsi?.text                     node.getParentOfType<UFile>()?.let { uClassWithElvis ->                         reportIssue(context, uClassWithElvis, node, elvisExpressionString)                     }                 }             }         })     }

Наследуемся от UElementHandler и переопределяем visitIfExpression. Если в коде встретится if, то мы попадём сюда. По дереву можно двигаться в двух направлениях — вверх и вниз. Чтобы «провалиться» ниже, используется функция accept, передаём в неё анонимный объект класса AbstractUastVisitor и переопределяем afterIfExpression. Так как в Java нет оператора Элвиса, нет отдельного expression для него, но тем не менее есть поле, в котором хранится информация о его использовании — kind.name. Данный подход по обработке оператора Элвиса был взят из исходников Jetbrains. Помимо того что используется Элвис, надо проверить, что после него стоит false. 

Далее, так как мы хотим в этом файле добавить новый импорт, нам нужно подняться «вверх» по дереву и дойти до уровня файла. Для этого используется функция getParentOfType, вызываем и указываем интересующий нас класс — Ufile. После этого переходим в reportIssue, передавая контекст, весь файл и код, в котором используется оператор.

Репорт

Для того чтобы мы в студии получили сообщение о том, что нужно поправить код, необходимо вызвать функцию report:

private fun reportIssue(context: JavaContext, nodeFile: UFile, nodeElvisExpression: UIfExpression, elvisExpressionString: String?) {     elvisExpressionString?.let {         context.report(                 ISSUE_ELVIS_OPERATOR_WITH_FALSE,                 context.getLocation(nodeElvisExpression),                 "Elvis expression can be replaced with .safeBoolean function",                 createFix(nodeFile, it)         )     } }

При вызове передаём:

  • Issue

  • Location — то место в коде, которое будет подчёркнуто и заменено при фиксе

  • Message — краткое описание

  • QuickFixData — объект класса LintFix. Используем, если хотим не только подсветить проблему, но и предложить исправление

Исправление

private fun createFix(nodeFile: UFile, oldText: String): LintFix {     val newString = oldText.substringBeforeLast("?:").trim() + ".safeBoolean"     val lastFileImport = nodeFile.imports.lastOrNull()     val elvisFix = LintFix             .create()             .name("Replace")             .replace()             .text(oldText)             .with(newString)              .reformat(false)             .build()      return if (lastFileImport != null && "import ru.mts.utils.extensions.safeBoolean" !in nodeFile.imports.map { it.sourcePsi?.text }) {         val addImportFix = LintFix.create()                 .replace()                 .with("\nimport ru.mts.utils.extensions.safeBoolean")                 .reformat(true)                 .range(context.getLocation(lastFileImport))                 .end()                 .autoFix()                 .build()         LintFix.create().composite(addImportFix, elvisFix)     } else {         elvisFix     } }

Как мы помним, фикс будет двойной, нужно вместо оператора Элвиса подставить вызов функции safeBoolean и добавить новый импорт. Для исправления оператора используем функцию replace и указываем, какой код (text) на что меняем (with).

Область исправления определяется location, который мы указали в report выше. Чтобы добавить фикс импортов, необходимо location поменять. Для этого нужно в range передать новый location (поле imports в UFile), в reformat поменять флаг на true. Далее указываем, что хотим добавить новый импорт в конец (end), и используем replace и with с указанием нового текста. 

После этого, если мы хотим, чтобы у нас это сработало в рамках одного фикса, необходимо их объединить, для этого используем composite.

Ура, новое правило успешно создано! Спасибо Вам за уделённое время. Если возникнут какие-либо вопросы, я с удовольствием отвечу на них в комментариях. А если захотите присоединиться к нашей команде, следите за вакансиями!


ссылка на оригинал статьи https://habr.com/ru/articles/797053/


Комментарии

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

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