...

пятница, 9 февраля 2018 г.

JavaParser. Корёжим код легко и непринуждённо

В мире существует множество клёвых маленьких библиотек, которые как бы и не знаменитые, но очень полезные. Идея в том, чтобы потихоньку знакомить Хабр с такими вещами. Сегодня расскажу о JavaParser.

JavaParser — это набор инструментов для парсинга, анализа, трансформации и генерации Java-кода. Иначе говоря, если нужно взять кусок джавакода и как-то его покорёжить подручными методами и без необходимости в особых знаниях, эта либа — самое то.

Где-то посреди статьи вы ВНЕЗАПНО можете осознать, какой кошмар и ужас можно сотворить этой либой, и никак не дождётесь дочитать текст и полить меня гневными комментариями. Не сдерживайтесь, не стоит — сразу скрольте до самого низу и изливайте душу :)


Код распространяется на гитхабе под лицензиями Apache, LGPL и GPL. Авторы сделали для проекта относительно приличный сайт, и даже запилили небольшую книжку, распространяемую совершенно бесплатно — что как бы подтверждает серьёзность их намерений.

Я перекинулся парой вопросов с авторами либы на FOSDEM, авторы оставляют впечатление умных и адекватных людей. Эта статья основана на их докладе.

Что делает эта либа? Вначале она превращает Java-код в AST (абстрактное синтаксическое дерево) — parsing. Во-вторых, она может взять уже готовый AST и превратить его в Java-код — unparsing.

В чем тут засада, и зачем нам вообще нужны библиотеки. Глядите:

String habraPostText = "Пыщ пыщ пыщ, ололо, я водитель НЛО";

public void writehHabraPost(String habraPostText) {
    habraPostText = "Привет, Хабр. Я сегодня пьян.";
}

public void writehHabraPost() {
    habraPostText = "Привет, Хабр. Я сегодня пьян.";
}

Для обоих этих методов AST в JavaParser сгенерится одинаковый. Конкретная нода в AST не знает, откуда именно пришел habraPostText.

Или например, у нас будут методы aMethod(int foo) и aMethod(String foo), внутри которых переменная печатается с помощью System.out.println. AST тоже выйдет одинаковый.

Поэтому в JavaParser есть так называемый symbol solver, который каждому куску AST вычисляет конкретные соответствующие куски исходника. Он у них лежит в виде отдельного проекта на GitHub под названием JSS.

Существует код, который JavaParser может просчитать и без JSS. Например, если мы дёргаем у поля геттер, то ссылка на вызываемое поле закодирована прямо в код. С другой стороны, если есть метод, в котором как-то хитро вычисляется возвращаемый тип, то тут уже нужно подключать тяжелую артиллерию, коей является JSS. Вручную написать такую штуку было бы весьма сложно.

Теперь, зачем всё это может быть нужно. Например, вы хотите автоматизировать генерацию мусорного кода (привет, Lombok!). Или написать транспилер, который код на каком-то выдуманном вами скриптовом языке будет превращать в Java.

Код, который такое делает, очень простой. Давайте сделаем класс хабрапоста:

CompilationUnit cu = new CompilationUnit();
cu.setPackageDeclaration("ru.habrahabr.hub.java.examples.javaparser");

ClassOrInterfaceDeclaration habraPost = cu.addClass("HabraPost");
habraPost.addField("String", "title");
habraPost.addField("String", "text");

И теперь добавим конструктор, заставляющий заполнить эти поля:

habraPost.addConstructor(Modifier.PUBLIC)
         .addParameter("String", "title")
         .addParameter("String", "text")
         .setBody(new BlockStmt()
            .addStatement(new ExpressionStmt(new AssignExpr(
                new FieldAccessExpr(new ThisExpr(), "title"),
                new NameExpr("title"),
                AssignExpr.Operator.ASSIGN)))
            .addStatement(new ExpressionStmt(new AssignExpr(
                new FieldAccessExpr(new ThisExpr(), "text"),
                new NameExpr("text"),
                AssignExpr.Operator.ASSIGN))));

Теперь сгенерим бойлерплейт для геттеров-сеттеров:

habraPost.addMethod("getTitle", Modifier.PUBLIC).setBody(
        new BlockStmt().addStatement(
                new ReturnStmt(new NameExpr("title"))));

habraPost.addMethod("getText", Modifier.PUBLIC).setBody(
        new BlockStmt().addStatement(
                new ReturnStmt(new NameExpr("text"))));

И теперь распечатаем наш класс в консоль:

System.out.println(cu.toString());

На выходе получится что-то вроде:

package ru.habrahabr.hub.java.examples.javaparser;

public class HabraPost {
    String title;
    String text;

    public HabraPost(String title, String text) {
        this.title = title;
        this.text = text;
    }

    public void getTitle() {
        return title;
    }

    public void getText() {
        return text;
    }
}

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

long wtfs = getNodes(myAPI, MethodDeclaration.class).stream()
         .filter(m -> m.getParameters().size > 10)
         .count();

System.out.println(String.format("Количество методов, за которые тебя стоит уволить: %d", wtfs));

Я не буду ударяться в сложные примеры, так как там всё видно по API. Единственная реально необычная фича, это то, что можно позвать JSS и покопаться в типах. Например, давайте найдём такой класс, который унаследован от максимального количества других классов (рекурсивно):

ResolvedReferenceTypeDeclaration c = 
    getNodes(myAPI, ClassorInterfaceDeclaration).stream()
    .filter(c -> !c.isInterface())
    .map(c -> c.resolve()) // JSS!
    .sorted(Comparator.comparingInt(o -> -1 * o.getAllAncestors().size)))
    .findFirst().get();

В результате, эту либу можно использовать для автоматического рефакторинга. Для этого можно не иметь продвинутых рефакторилок внутри IDE, а ограничиться исключительно возможностями JavaParser. Давайте сделаем рефакторинг: замену вызова более старого метода на более новый.

Помните вот эту историю?


Допустим у нас есть метод checkMegamozg(Boolean moderatorInAGoodMood), который Boomburum запускает у себя в мозгу 666 раз в день. Нужно превратить его в checkHabrahabr(Boolean moderatorInAGoodMood).

Вначале мы ищем нужный метод:

getNodes(myAPI, MethodCallExpr.class).stream()
    .filter(m -> m.resolveInvokedMethod()).
                 getQialifiedSignature()
                 .equals("ru.habrahabr.Habr.checkMegamozg(java.lang.Boolean)"))
    .forEach(m -> m.replace(replaceCallsToMegamozg(m)));

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

public MethodCallExpr replaceCallsToMegamozg() {
    MethodCallExpr newMethodCall = new MethodCallExpr(
            methodCall.getScope.get(), "checkHabrahabr");
    newMethodCall.addArgument(methodCall.getArgument(0));
    return newMethodCall;
}

Причём если где-то там внутри методов затесались комментарии (привет, lany!), JavaParser всячески пытается их не потерять. Это жутко неприятная задача и авторы очень парятся об этой теме.

Как видим, эта либа — как маленький швейцарский ножик, простая и относительно надёжная. В будущем будут добавляться небольшие фичи типа встроенного языка шаблонов, чтобы можно было генерить Java-классы не вручную, а подгрузив их из файла и заменив ${плейсхолдеры}.

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


Минутка рекламы. Как вы, наверное, знаете, мы делаем конференции. Ближайшие — JBreak 2018 и JPoint 2018. Можно туда прийти и вживую пообщаться с разработчиками разных моднейших технологий, например там будет Simon Ritter — Deputy CTO из Azul Systems. Там же можно встретиться c Виктором Гамовым из "Разбора Полётов" и кучей других интересных людей (и с бездельниками типа меня тоже можно пересечься). Короче, заходите, мы вас ждём.

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий