Сплющите структуру упаковки Kotlin с одним рецептом Openrewrite

Сплющите структуру упаковки Kotlin с одним рецептом Openrewrite

12 июня 2025 г.

Я некоторое время смотрел на Openrewrite, но у меня еще не было времени играть с этим. Если вы никогда не слышали о OpenRewrite, Openrewrite позаботится о рефакторинге вашей кодовой базы на более новые версии, структуры и парадигмы.

OpenRewrite-это автоматизированная экосистема с открытым исходным кодом для исходного кода, позволяющая разработчикам эффективно устранить технические долги в своих репозиториях.

Он состоит из двигателя автоматического повторного факторирования, который запускает предварительные рецепты рефакторирования с открытым исходным кодом для общих фреймворков, исправлений безопасности и стилистических задач согласованности-сокращение ваших усилий по кодированию с часов или дней до минут. Строительными плагинами инструментов, такие как плагин OpenWrite Gradle и плагин Openrewrite Maven, помогут запустить эти рецепты на одном хранилище за раз.

В то время как первоначальное внимание уделялось языку Java, сообщество Openrewrite постоянно расширяет языковые и структуру. Тысячи великих людей и команд работают вместе, чтобы программное обеспечение было бесшовным для обновления и постоянно безопасного.

-Введение в Openrewrite

Openrewrite имеет два компонента: рецепты и двигатель, который их запускает.

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

-Рецепты

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

Мой вариант использования

Мой вариант использования - структура пакета Kotlin. В Java класс вch.frankel.blogПакет должен соблюдать жесткую структуру папок: от корня,chВfrankel, а потомblogПолем В Kotlin вы можете поместить один и тот же класс в то же пакет в корне. Официальная документация по котлин имеет рекомендации по структуре источника:

В проектах Pure Kotlin рекомендуемая структура каталогов следует за структурой пакета с общим корневым пакетом опущены. Например, если весь код в проекте находится вorg.example.kotlinпакет и его подпакинги, файлы сorg.example.kotlinпакет должен быть размещен непосредственно под корнем исходного и файла вorg.example.kotlin.network.socketДолжен быть в подкаталоге сети/сокета исходного корня.

-Структура каталога

Рецепт будет перенести исходные файлы ближе к корневым пакетам согласно вышеуказанной рекомендации. Мы могли бы достичь того же с такими инструментами Sysadmin, какmvВsed, или Ides. Хотя можно было бы реализовать мою идею с этими инструментами, Openrewrite имеет несколько преимуществ:

  • Тестируемый: Дизайн API тестируется, поэтому вы можете убедиться, что не испортите свою кодовую базу
  • Масштабируемый: Это работает на огромных кодовых базах
  • Композитный: Это позволяет обеспечить более одного рецепта и запускать их за один проход

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

Основы рецепта

Openrewrite реализует шаблон посетителей. Вот сокращенная диаграмма класса для исходного кода Kotlin, которую я буду использовать позже.

Recipe class diagram

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

Kotlin-specific class diagram

Сделать все это вместе

Теперь, когда у нас есть более четкое представление об API, пришло время подумать о коде.

Я требую, чтобы пользователь вручную настраивал корневой пакет в этой версии прототипа. Я хотел бы вычислить корневой пакет с кодовой базы в будущей версии. Например, с исходными файлами с соответствующими объявлениями пакетовch.frankel.blogВch.frankel.blog.foo, иch.frankel.blog.bar, корневой пакет вычисляется какch.frankel.blogПолем Кроме того, я предполагаю, что исходные файлы находятся подsrc/main/resourcesПолем

Процесс должен, для каждого файла:

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

Вот реализация вышеизложенного:

class FlattenStructure(private val rootPackage: String) : Recipe() {                   //1

    override fun getDisplayName(): String = "Flatten Kotlin package directory structure" //2
    override fun getDescription(): String =                                            //2
        "Move Kotlin files to match idiomatic layout by omitting the root package according to the official recommendation."

    override fun getVisitor(): TreeVisitor<*, ExecutionContext> {
        return object : KotlinIsoVisitor<ExecutionContext>() {
            override fun visitCompilationUnit(cu: K.CompilationUnit, ctx: ExecutionContext): K.CompilationUnit {
                val packageName = cu.packageDeclaration?.packageName ?: return cu      //3
                if (!packageName.startsWith(rootPackage)) return cu                    //4
                val relativePath = packageName.removePrefix(rootPackage)
                    .removePrefix(".")
                    .replace('.', '/')                                                 //5
                val filename = cu.sourcePath.fileName.toString()
                val newPath: Path = Paths.get("src/main/kotlin")
                    .resolve(relativePath)
                    .resolve(filename)                                                 //6
                return cu.withSourcePath(newPath)                                      //7
            }
        }
    }
}
  1. Параметризуйте рецепт с помощью корневого пакета
  2. Атрибуты для целей документации
  3. Если мы не можем вычислить имя пакета, верните
  4. Если блок компиляции уже находится в корневом пакете, верните
  5. Вычислить относительный путь из корневого пакета и имени пакета устройства
  6. Вычислить новый путь
  7. Верните компиляционный блок с новым путем

Тестирование рецепта

API Openrewrite поддается тестированию. Он предлагает анализаторы на разных языках, которые он поддерживает, и в контексте выполнения в памяти. Вот отрыв из того, что я использовал для проверки вышеуказанного рецепта:

Diagram of classes involved in tests

Я решил использовать параметризованный тест JUNIT, но это не имеет значения:

class FlattenStructureTest {

    @ParameterizedTest
    @MethodSource("testData")
    fun `should flatten accordingly`(
        sourceCode: String,
        originalPath: String,
        configuredRootPackage: String,
        expectedPath: String
    ) {
        // Given
        val parser = KotlinParser.builder().build()                                     //1
        val cu = parser.parse(                                                          //2
            InMemoryExecutionContext(),                                                 //3
            sourceCode
        ).findFirst()                                                                   //4
            .orElseThrow { IllegalStateException("Failed to parse Kotlin file") }       //4
        val originalPath = Paths.get(originalPath)
        val modifiedCu = (cu as K.CompilationUnit).withSourcePath(originalPath)         //5

        // When
        val recipe = FlattenStructure(configuredRootPackage)
        val result = recipe.visitor.visit(modifiedCu, InMemoryExecutionContext())       //6

        // Then
        val expectedPath = Paths.get(expectedPath)
        assertEquals(expectedPath, (result as SourceFile).sourcePath)                   //7
    }

    // Provide the file content, the path, the root, and the expected path
}
  1. Парсеры предлагают API шаблона застройщика
  2. Сделайте анализ
  3. Поскольку мы не используем контекст для передачи сообщений по рецептам, мы можем создать экземпляр в памяти для каждого теста
  4. parseвозвращает потокSourceFileПолем Несмотря на то, что мы знаем, что он содержит один, нам нужно обрабатывать поток и возможное исключение.
  5. Из -за отсутствия дженериков нам нужно разыграть возвращенныхSourceFileкK.CompilationUnit
  6. Посетите исходный код вручную - в отличие от двигателя, который делает это для нас
  7. Ожидайте, что файл был перемещен (практически)

Теперь мы можем использовать рецепт:

---
type: specs.openrewrite.org/v1beta/recipe
name: ch.frankel.MyMigration
recipeList:
  - ch.frankel.openrewrite.kotlin.FlattenStructure:
      rootPackage: com.acme

Потенциальные будущие работы

Я намеренно исключил требование рецепта: рецептДОЛЖЕНБудьте json-serializable. Ни Градл, ни Мавен не используют эту функцию, но другие инструменты в экосистеме делают.

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

Заключение

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

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

Полный исходный код для этого поста можно найти на GitHub:

https://github.com/ajavageek/flatten-kotlin-recipe?embedable=true

Идти дальше:

  • Введение в Openrewrite
  • Написание рецепта рефакторинга Java

Первоначально опубликовано наJava Geek8 июня 2025 года


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE