Connect with us

Разработка

Архитектурный линтинг для Swift: часть 2

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

Опубликовано

/

     
     

В нашей первой статье об архитектурном линтинге в Swift мы рассказали о трудностях написания высокоуровневых правил линтинга в SwiftLint, которые касались бы чего-то, выходящего за рамки тривиального синтаксиса. Сложные регулярные выражения было трудно написать, а пользовательские SwiftSyntaxRules — еще сложнее.

В 2023 году в сообществе Android появился Konsist, архитектурный линтер для Kotlin, который позволил нашей Android-команде ответить на простой вопрос:

Можем ли мы гарантировать, что все наши логические классы (aka Domain/UseCase) будут предоставлять  только одну публичную функцию?

И это реализовали с помощью такого простого кода:

Given("A logic class") {
    val logicClasses = KonsistUtils.logicClassesProduction
        .withoutName(*baseline)

    Then("It should not have more than one function exposed") {
        logicClasses.withFunctions {
            it.withPublicOrDefaultModifier().size > 1
        }.assertEmpty(
            additionalMessage = MessageExposeOnlyInvokeFunction
        )
    }
}

Наша команда iOS, естественно, позавидовала и летом 2024 года представила Harmonize, первый архитектурный линтер для Swift. Harmonize позволяет внедрить любое архитектурное или структурное правило линтинга всего в нескольких строках кода, не требуя крутого обучения или мастерского владения регулярными выражениями. В отличие от SwiftLint, правила линтинга просто пишутся в виде юнит-тестов, а не регулярных выражений, и каждое правило имеет полный доступ к вашей кодовой базе (и AST) с помощью удобного, идиоматического DSL. Точно так же, как вы написали бы тест для функции вашего приложения, теперь вы пишете тесты и делаете утверждения об архитектуре вашего кода.

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

final class LogicClassShouldOnlyExposeOneFunction: QuickSpec {
    override class func spec() {
        Given("A logic class in production code") {
            let logicClasses = PSSHarmonize.logicClassesProduction.withoutName(baseline)

            Then("It exposes callAsFunction as the only public function") {
                logicClasses.assertTrue(message: message) {
                    return $0.functions.withoutModifier(.private).count == 1
                }
            }
        }
    }

    private static let message = "Logic classes should only expose one function."
    private static let baseline: [String] = []
}

Теперь у нас есть решение, которое легко читается и требует только одной реальной логики. Оно будет выполняться только при запуске тестовой цели Harmonize, но если разработчики будут периодически выполнять тесты этой цели, они получат обратную связь по архитектуре гораздо раньше, чем при обычном PR-рецензировании.

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

(В примере выше также используется одно из наших полезных расширений Gherkin для тестов Quick).

В нашей текущей библиотеке определены следующие функции расширения для массивов этих объектов:

  • assertCount
  • assertEmpty/assertNotEmpty
  • assertTrue/assertFalse

Многие из этих методов также принимают необязательный аргумент message, чтобы вы, как разработчик, могли явно указать причину правила и неудачи. В приведенном выше правиле вы можете увидеть использование assertTrue и дополнительное пользовательское сообщение.

Вылавливайте ошибки быстрее

Harmonize дает вам доступ к каждому компоненту в вашей кодовой базе, что открывает новые возможности для выявления и предотвращения ошибок. Возьмем наш пример из первой статьи о том, как трудно понять, что вы не использовали weak self. Благодаря возможности различать вложенные структуры внутри AST, в Harmonize мы определили следующее правило:

final class ShouldCaptureSelfWeaklyOnViewModels: QuickSpec {
    override class func spec() {
        Given("a View model class in production code") {
            let viewModels = PSSHarmonize.viewModelsProduction

            When("you capture self on a closure") {
                Then("It should be captured self weakly") {
                    viewModels.functions().filter(\.hasAnyClosureWithSelfReference).assertTrue(message: message) {
                        $0.closures()
                            .filter(\.hasSelfReference)
                            .allSatisfy { $0.isCapturingWeak(valueOf: "self") }
                    }
                }
            }
        }
    }

    private static let message = "ViewModels should capture self weakly on closures to avoid retain cycles"
}

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

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

Негативная ретроспектива (negative lookbehind) в регулярных выражениях — это способ задать условие, что перед текущей позицией не должно быть определённого шаблона. Обозначается как (?<!...).

Архитектурный линтинг для Swift: часть 2

ИИ-генераторы кода, создающие регулярные выражения

 missing_weak_self_in_viewmodels:
    name: "Missing [weak self] in closures in ViewModels"
    included: "Sources/ViewModels"
    regex: '(?<!\[weak self\]\s*)\{\s*(.*?)self\.\w+'
    match_kinds:
      - source.lang.swift.expr.closure
    message: "Use [weak self] to avoid retain cycles in closures in ViewModels."
    severity: warning

Путешествие по переулку памяти (Java)

Java стала объектом многочисленных шуток, когда речь заходит об архитектуре, но это не делает ее неправильной! ArchUnit, представленный в 2017 году, позволил Java-разработчикам делать проверки и утверждения об архитектуре Java-кода. После ArchUnit в 2023 году появился Konsist, который привнес аналогичные возможности и идиомы в Kotlin, а значит, и в сообщество Android-разработчиков.

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

class UseCaseExposedFunctionsRule : Rule() {
    private val message = "A UseCase should only expose one public function. Properties should not be exposed"

    override val issue: Issue = Issue(
        javaClass.simpleName,
        Severity.Style,
        message,
        Debt(days = 0, hours = 3, mins = 0)
    )

    private var ktClass: KtClassOrObject? = null

    override fun visitClassOrObject(classOrObject: KtClassOrObject) {
        if (classOrObject.isFunctionalLogicClass()) {
            ktClass = classOrObject
        }
        super.visitClassOrObject(classOrObject)
    }

    override fun visitClassBody(classBody: KtClassBody) {
        super.visitClassBody(classBody)
        ktClass?.let {
            val hasNonConstantPublicProperty = classBody.properties.any { it.isPublic && !it.isConstant() }
            val publicFunctions = classBody.functions.filter { it.isPublic }.size
            if (hasNonConstantPublicProperty || publicFunctions != 1) {
                report(UseCaseRuleFinding(issue, Entity.from(ktClass as PsiElement), message))
            }
        }
    }
}

Как мы видели в начале статьи, создание и применение этого правила в Konsist не составило труда.

Далее

В следующем посте мы расскажем о лучших практиках после почти годовой работы с Harmonize.

Источник

Статьи из этой серии

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: