Статьи

Наблюдатели для узлов AST в JavaParser

Мы приближаемся к первому релиз-кандидату для JavaParser 3.0. Одной из последних добавленных нами функций была поддержка наблюдения за изменениями во всех узлах абстрактного синтаксического дерева. Пока я писал код для этой функции, я получил ценные отзывы от Дэнни ван Брюггена (он же Матозоид) и Круса Максимилиена. Поэтому я использую «мы» для обозначения команды JavaParser.

Для чего могут использоваться наблюдатели на узлах AST?

Я думаю, что это очень важная функция для экосистемы JavaParser, поскольку она облегчает интеграцию с JavaParser, реагируя на изменения, внесенные в AST. Возможные изменения, которые можно наблюдать, — это установка нового имени для класса или добавление нового поля. Различные инструменты могут реагировать на эти изменения по-разному. Например:

  • редактор может обновить свой список символов, который может использоваться для таких вещей, как автозаполнение
  • некоторые фреймворки могут перегенерировать исходный код для отражения изменений
  • проверка может быть выполнена, чтобы проверить, привело ли новое изменение к неправильному AST
  • такие библиотеки, как JavaSymbolSolver, могут пересчитывать типы выражений

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

AstObserver

JavaParser 3.0 AST основан на узлах и списках узлов. Узел, как, например, TypeDeclaration , может иметь разные группы потомков. Когда эти группы могут содержать более одного узла, мы используем NodeLists. Например, TypeDeclarations может иметь несколько членов (поля, методы, внутренние классы). Таким образом, у каждого TypeDeclaration есть NodeList для хранения полей, одно для хранения методов и т. Д. Другие дочерние элементы, такие как имя TypeDeclaration, вместо этого непосредственно содержатся в узле.

Мы представили новый интерфейс под названием AstObserver. AstObserver получает изменения в Nodes и NodeLists.

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
 * An Observer for an AST element (either a Node or a NodeList).
 */
public interface AstObserver {
  
    /**
     * Type of change occurring on a List
     */
    public enum ListChangeType {
        ADDITION,
        REMOVAL
    }
  
    /**
     * The value of a property is changed
     *
     * @param observedNode owner of the property
     * @param property property changed
     * @param oldValue value of the property before the change
     * @param newValue value of the property after the change
     */
    void propertyChange(Node observedNode, ObservableProperty property, Object oldValue, Object newValue);
  
    /**
     * The parent of a node is changed
     *
     * @param observedNode node of which the parent is changed
     * @param previousParent previous parent
     * @param newParent new parent
     */
    void parentChange(Node observedNode, Node previousParent, Node newParent);
  
    /**
     * A list is changed
     *
     * @param observedNode list changed
     * @param type type of change
     * @param index position at which the changed occurred
     * @param nodeAddedOrRemoved element added or removed
     */
    void listChange(NodeList observedNode, ListChangeType type, int index, Node nodeAddedOrRemoved);
}

Что наблюдать

Теперь у нас есть AstObserver, и нам нужно решить, какие изменения он должен получить. Мы подумали о трех возможных сценариях:

  1. Наблюдение только одного узла, например ClassDeclaration. Наблюдатель получит уведомления об изменениях в этом узле (например, если имя класса изменится), но не о каком-либо из его потомков. Например, если поле с именем изменения класса, наблюдатель не будет уведомлен
  2. Для узла и всех его потомков в момент регистрации наблюдателя. В этом случае, если я зарегистрирую наблюдателя для ClassDeclaration, я буду уведомлен об изменениях в классе и всех его полях и методах. Если новое поле будет добавлено и позже изменено, я не получу уведомления об этих изменениях
  3. Для узла и всех его потомков, как существующих на момент регистрации наблюдателя, так и добавленных позже.

Итак, узел теперь имеет этот метод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
    * Register a new observer for the given node. Depending on the mode specified also descendants, existing
    * and new, could be observed. For more details see <i>ObserverRegistrationMode</i>.
    */
   public void register(AstObserver observer, ObserverRegistrationMode mode) {
       if (mode == null) {
           throw new IllegalArgumentException("Mode should be not null");
       }
       switch (mode) {
           case JUST_THIS_NODE:
               register(observer);
               break;
           case THIS_NODE_AND_EXISTING_DESCENDANTS:
               registerForSubtree(observer);
               break;
           case SELF_PROPAGATING:
               registerForSubtree(PropagatingAstObserver.transformInPropagatingObserver(observer));
               break;
           default:
               throw new UnsupportedOperationException("This mode is not supported: " + mode);
       }
   }

Чтобы различать эти три случая, мы просто используем перечисление ( ObserverRegistrationMode ). Позже вы можете увидеть, как мы реализовали PropagatingAstObserver .

Реализация поддержки для наблюдателей

Если бы JavaParser был основан на некоторой мета-моделирующей среде, такой как EMF, это было бы чрезвычайно просто сделать. Учитывая, что это не тот случай, мне нужно было добавить вызов уведомления во все установщики классов AST (их около 90).

Поэтому, когда сеттер вызывается на определенном узле, он уведомляет всех наблюдателей. Просто. Возьмем для примера setName в TypeDeclaration <T> :

1
2
3
4
5
6
7
@Override
public T setName(SimpleName name) {
    notifyPropertyChange(ObservableProperty.NAME, this.name, name);
    this.name = assertNotNull(name);
    setAsParentNodeOf(name);
    return (T) this;
}

Поскольку у нас нет правильной метамодели, у нас нет определений для свойств. Поэтому мы добавили список свойств в перечисление с именем ObservableProperty . Таким образом, наблюдатель может проверить, какое свойство было изменено, и решить, как реагировать.

Внутренняя иерархия наблюдателей

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

Однако этого недостаточно, потому что в некоторых случаях вы можете захотеть также наблюдать за всеми узлами, которые добавляются к поддереву после размещения ваших наблюдателей. Мы делаем это с помощью PropagatingAstObserver . Это AstObserver, который, когда видит, что новый узел был присоединен к узлу, он наблюдает за началом наблюдения нового узла. Просто, а?

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
 * This AstObserver attach itself to all new nodes added to the nodes already observed.
 */
public abstract class PropagatingAstObserver implements AstObserver {
  
    /**
     * Wrap a given observer to make it self-propagating. If the given observer is an instance of PropagatingAstObserver
     * the observer is returned without changes.
     */
    public static PropagatingAstObserver transformInPropagatingObserver(final AstObserver observer) {
        if (observer instanceof PropagatingAstObserver) {
            return (PropagatingAstObserver)observer;
        }
        return new PropagatingAstObserver() {
            @Override
            public void concretePropertyChange(Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
                observer.propertyChange(observedNode, property, oldValue, newValue);
            }
  
            @Override
            public void concreteListChange(NodeList observedNode, ListChangeType type, int index, Node nodeAddedOrRemoved) {
                observer.listChange(observedNode, type, index, nodeAddedOrRemoved);
            }
  
            @Override
            public void parentChange(Node observedNode, Node previousParent, Node newParent) {
                observer.parentChange(observedNode, previousParent, newParent);
            }
        };
    }
  
    @Override
    public final void propertyChange(Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
        considerRemoving(oldValue);
        considerAdding(newValue);
        concretePropertyChange(observedNode, property, oldValue, newValue);
    }
  
    @Override
    public final void listChange(NodeList observedNode, ListChangeType type, int index, Node nodeAddedOrRemoved) {
        if (type == ListChangeType.REMOVAL) {
            considerRemoving(nodeAddedOrRemoved);
        } else if (type == ListChangeType.ADDITION) {
            considerAdding(nodeAddedOrRemoved);
        }
        concreteListChange(observedNode, type, index, nodeAddedOrRemoved);
    }
  
    public void concretePropertyChange(Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
        // do nothing
    }
  
    public void concreteListChange(NodeList observedNode, ListChangeType type, int index, Node nodeAddedOrRemoved) {
        // do nothing
    }
  
    @Override
    public void parentChange(Node observedNode, Node previousParent, Node newParent) {
        // do nothing
    }
  
    private void considerRemoving(Object element) {
        if (element instanceof Observable) {
            if (((Observable) element).isRegistered(this)) {
                ((Observable) element).unregister(this);
            }
        }
    }
  
    private void considerAdding(Object element) {
        if (element instanceof Node) {
            ((Node) element).registerForSubtree(this);
        } else if (element instanceof Observable) {
            ((Observable) element).register(this);
        }
    }
  
}

Наблюдатели в действии

Давайте посмотрим, как это работает на практике:

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
28
29
30
31
32
33
34
35
36
37
// write some code and parse it
String code = "class A { int f; void foo(int p) { return 'z'; }}";
CompilationUnit cu = JavaParser.parse(code);
  
// set up our observer
List changes = new ArrayList<>();
AstObserver observer = new AstObserverAdapter() {
    @Override
    public void propertyChange(Node observedNode, ObservableProperty property, Object oldValue, Object newValue) {
        changes.add(String.format("%s.%s changed from %s to %s", observedNode.getClass().getSimpleName(), property.name().toLowerCase(), oldValue, newValue));
    }
};
cu.getClassByName("A").register(observer, /* Here we could use different modes */);
  
// Doing some changes
cu.getClassByName("A").setName("MyCoolClass");
cu.getClassByName("MyCoolClass").getFieldByName("f").setElementType(new PrimitiveType(PrimitiveType.Primitive.Boolean));
cu.getClassByName("MyCoolClass").getMethodsByName("foo").get(0).getParamByName("p").setName("myParam");
// Here we are adding a new field and immediately changing it
cu.getClassByName("MyCoolClass").addField("int", "bar").getVariables().get(0).setInit("0");
  
// If we registered our observer with mode JUST_THIS_NODE
assertEquals(Arrays.asList("ClassOrInterfaceDeclaration.name changed from A to MyCoolClass"), changes);
  
// If we registered our observer with mode THIS_NODE_AND_EXISTING_DESCENDANTS
assertEquals(Arrays.asList("ClassOrInterfaceDeclaration.name changed from A to MyCoolClass",
        "FieldDeclaration.element_type changed from int to boolean",
        "VariableDeclaratorId.name changed from p to myParam"), changes);
  
// If we registered our observer with mode SELF_PROPAGATING
assertEquals(Arrays.asList("ClassOrInterfaceDeclaration.name changed from A to MyCoolClass",
        "FieldDeclaration.element_type changed from int to boolean",
        "VariableDeclaratorId.name changed from p to myParam",
        "FieldDeclaration.modifiers changed from [] to []",
        "FieldDeclaration.element_type changed from empty to int",
        "VariableDeclaratorId.array_bracket_pairs_after_id changed from com.github.javaparser.ast.NodeList@1 to com.github.javaparser.ast.NodeList@1",
        "VariableDeclarator.init changed from null to 0"), changes);

Выводы

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

Мне действительно любопытно посмотреть, что люди будут строить. Кстати, знаете ли вы какой-нибудь проект, использующий JavaParser, о котором вы хотите сообщить нам? Оставьте комментарий или откройте вопрос на GitHub, мы с нетерпением ждем от вас!