Статьи

Использование Gradle для построения и применения преобразований AST

Недавно я хотел как построить, так и применить локальные аст-преобразования в проекте Gradle. Хотя я мог найти несколько примеров написания преобразований, я не смог найти полный пример, показывающий весь процесс сборки. Преобразование должно быть скомпилировано отдельно, а затем помещено в путь к классам, поэтому его источник не может просто находиться в остальной части исходного кода Groovy. Это та деталь, которая на некоторое время сбила меня с толку.

Сначала я настроил отдельную задачу GroovyCompile для обработки аннотации перед остальным источником (вытекает из полезного предложения Питера Нидервизера на форумах Gradle). Хотя это сработало, гораздо более простым решением для применения преобразований является настройка многопроектной сборки. Основной проект зависит от подпроекта с исходными файлами преобразования ast. Вот минимальная примерная структура каталогов:

ast/build.gradle
Ast build file
ast/src/main/groovy/com/cholick/ast/Marker.groovy
интерфейс маркера
ast/src/main/groovy/com/cholick/ast/Transform.groovy
Аст трансформация
build.gradle
основной файл сборки
settings.gradle
конфигурация иерархии проекта
src/main/groovy/com/cholick/main/Main.groovy
источник для преобразования

Для полного рабочего источника (с простыми тестами и без * импорта), клон https://github.com/cholick/gradle_ast_example

Корневой файл build.gradle содержит зависимость от проекта ast:

1
2
3
4
dependencies {
    ...
    compile(project(':ast'))
}

Корень settings.gradle определяет подпроект ast:

1
include 'ast'

Базовый проект также имеет src / main / groovy / com / cholick / main / Main.groovy, с исходным файлом для преобразования. В этом примере преобразование ast, которое я написал, помещает метод с именем ‘добавлен’ в класс.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
package com.cholick.main
 
import com.cholick.ast.Marker
 
@Marker
class Main {
    static void main(String[] args) {
        new Main().run()
    }
    def run() {
        println 'Running main'
        assert this.class.declaredMethods.find { it.name == 'added' }
        added()
    }
}

В подпроекте ast ast / src / main / groovy / com / cholick / ast / Marker.groovy определяет интерфейс для маркировки классов для преобразования ast:

01
02
03
04
05
06
07
08
09
10
package com.cholick.ast
 
import org.codehaus.groovy.transform.GroovyASTTransformationClass
 
import java.lang.annotation.*
 
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
@GroovyASTTransformationClass(['com.cholick.ast.Transform'])
public @interface Marker {}

Наконец, класс преобразования ast обрабатывает исходные классы и добавляет метод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.cholick.ast
 
import org.codehaus.groovy.ast.*
import org.codehaus.groovy.ast.builder.AstBuilder
import org.codehaus.groovy.control.*
import org.codehaus.groovy.transform.*
 
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
class Transform implements ASTTransformation {
    void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
        if (!astNodes) return
        if (!astNodes[0]) return
        if (!astNodes[1]) return
        if (!(astNodes[0] instanceof AnnotationNode)) return
        if (astNodes[0].classNode?.name != Marker.class.name) return
 
        ClassNode annotatedClass = (ClassNode) astNodes[1]
        MethodNode newMethod = makeMethod(annotatedClass)
        annotatedClass.addMethod(newMethod)
    }
    MethodNode makeMethod(ClassNode source) {
        def ast = new AstBuilder().buildFromString(CompilePhase.INSTRUCTION_SELECTION, false,
                "def added() { println 'Added' }"
        )
        return (MethodNode) ast[1].methods.find { it.name == 'added' }
    }
}

Спасибо Гамлету Д’Арси за отличный пример трансформации, а также Питеру Нидервизеру за ответ на мой вопрос на форумах.