Javascript.RU

Создание собственных аннотаций

Java-фреймворк, на котором построен Google Closure Compiler, может быть расширен. Например, можно добавить новые аннотации, которые делают что-то с кодом.

К примеру, можно добавить @strip - аннотацию, которая уберет из итогового кода все вызовы объекта.

Работать будет так:

/** @strip */
console = {
  debug: function() { /* ... */ }
}

function doSomething() {
	console.log()
	alert(1);
}

Превратится в:

function doSomething(){alert(1)};

Создавать аннотации можно практически любые. В частности, аннотация может иметь параметры: @param {string}. Действия, которые компилятор будет совершать с аннотированным объектом (объектом, функцией и т.п.) описываются на языке Java и могут быть практически любыми.

Единственно, следует иметь в виду, что аннотация - это все же для компилятора, а не для браузера. Отлаживать удобнее всего несжатый код, а в нем аннотации будут простыми комментариями.
Хотя, даже сжатый код можно поотлаживать, используя Closure Inspector в Firefox.

Таким образом, в общем случае заведомо полезны аннотации, помогающие лучше сжать/обработать код.

Добавление аннотаций состоит из нескольких частей. Нужно указать ее в парсере javascript, добавить в список возможных аннотаций JSDoc, создать функцию для проверки аннотации, описать действия компилятора и т.п. Разберем шаги поподробнее на примере @strip.

За структуру данных JSDoc отвечает класс com.google.javascript.rhino.JSDocInfo,

Для новой аннотации следует создать флаг в общем списке:

...
private static final int MASK_FILEOVERVIEW  = 0x00001000; // @fileoverview
private static final int MASK_IMPLICITCAST  = 0x00002000; // @implicitCast
private static final int MASK_NOSIDEEFFECTS = 0x00004000; // @nosideeffects
// допишем нашу аннотацию в конец, создадим ей новую маску
private static final int MASK_STRIP = 0x00008000; // @strip

И добавим методы установки и проверки, аналогично setExport:

void setStrip(boolean value) {
  setFlag(value, MASK_STRIP);
}
public boolean isStrip() {
  return getFlag(MASK_STRIP);
}

Наша аннотация - бинарная (есть/нет), поэтому последующие действия по сути состоят в создании рядом с export аналогов с именем strip.

Класс com.google.javascript.rhino.JSDocInfoBuilder отвечает за построение объекта JSDoc из аннотаций. Добавим новую аннотацию и туда:

// аналогично recordExport..
public boolean recordStrip() {
    if (!currentInfo.isStrip()) {
      currentInfo.setStrip(true);
      populated = true;
      return true;
    } else {
      return false;
    }
}

Класс com.google.javascript.jscomp.parsing.JsDocInfoParser - последний класс парсера, требующий исправления. Он отвечает за разбор текста скрипта. Допишем новую аннотацию в список enum Annotation:

...
EXTENDS,
EXPORT,
// наша аннотация
STRIP,

.. И добавим ее в общий список recognizedAnnotations:

...
put("enum", Annotation.ENUM).
put("export", Annotation.EXPORT).
// наша новая строка
put("strip", Annotation.STRIP).

Кроме того, добавим фрагмент в метод parse, который будет записывать найденную аннотацию в JSDoc.

Можно это сделать аналогично case EXPORT:

case EXPORT:
    ...
case STRIP:
  if (!jsdocBuilder.recordStrip()) {
    parser.addWarning("msg.jsdoc.strip",
    stream.getLineno(), stream.getCharno());
  }
  token = eatTokensUntilEOL();
  continue retry;

Итак, парсер готов. Аннотация будет распознана и превращена в JSDoc. При обходе синтаксического дерева javascript, компилятор сможет вызовом node.getJSDocInfo() получить JSDoc, и затем проверить isStrip() - нет ли у данного объекта аннотации @strip.

Для того, чтобы аннотация заработала, необходимо действие. Почти все действия google closure compiler реализуются путем прохода компилятора по синтаксическому дереву.

Проход может осуществлять любой класс с интерфейсом CompilerPass. Он осуществляется в обратном порядке (Post-Order).

Мы не будем патчить существующие проходы, а создадим свой. Он будет проходить по дереву, и при нахождении узла с isStrip() == true - добавлять имя объекта в список stripTypes.

В последующих проходах этот список будет обработан компилятором, и все обращения к указанному объекту будут удалены из кода.

Вот код для такого прохода.

package com.google.javascript.jscomp;

import com.google.common.collect.Sets;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.FunctionNode;

class ApplyAnnotationStrip extends NodeTraversal.AbstractPostOrderCallback implements CompilerPass {

    private final Compiler compiler;

    ApplyAnnotationStrip(Compiler compiler) {
        this.compiler = compiler;
    }

    public void visit(NodeTraversal t, Node n, Node parent) {
        JSDocInfo jsDocInfo = n.getJSDocInfo();

        if (jsDocInfo == null || !jsDocInfo.isStrip()) return;

        /* получить имя объекта/функции */ 
        String name = null;
        if (n.getType() == Token.FUNCTION) {
            name = ((FunctionNode)n).getFunctionName();
        } else if (n.getType() == Token.ASSIGN) {
            name = n.getFirstChild().getQualifiedName();
        }

        /* добавить в список */
        CompilerOptions options = compiler.getOptions();
        if (options.stripTypes.isEmpty()) {
            options.stripTypes = Sets.newHashSet();
        }
        options.stripTypes.add(name);
    }

    public void process(Node externs, Node root) {
        NodeTraversal.traverse(compiler, root, this);
    }

}

Метод process требуется для интерфейса CompilerPass и осуществляет, собственно, проход компилятора - путем обхода дерева скрипта.

Третий аргумент NodeTraversal.traverse - объект, производящий обработку узлов во время обхода. Для этого у него должен быть метод visit. Мы реализуем его здесь же.

Метод visit, как видно из исходного кода, отрабатывает только для узлов с JSDoc и isStrip == true.

Он получает имя объекта и добавляет соответствующую строку в опцию компилятора stripTypes, которая более подробно описана в статье Автоудаление отладочных свойств и объектов.

Если коротко - последующие проходы компилятора используют эту опцию для удаления всех обращений к указанным в ней символам.

Для того, чтобы получать имя объекта, и вообще делать какие-то операции, компилятор использует Синтаксическое дерево.

Для того, чтобы в общих чертах представлять, как устроено дерево разбора javascript, можно почитать стандарт языка, документацию к Rhino, а впрочем - вполне можно что-то понять, запустив компилятор с опциями --use_only_custom_externs --print_tree --js ваш_скрипт. Такой вызов распечатает синтаксическое дерево вашего скрипта.

Итак, структура JSDoc готова, проход компилятора тоже описан, остается добавить этот проход к исполнению компилятором.

За это отвечает класс com.google.javascript.jscomp.DefaultPassConfig. Добавление прохода осуществляется посредством фабричного метода PassFactory.

Добавим новый метод аналогично уже существующим:

private final PassFactory applyAnnotationStrip =
  new PassFactory("applyAnnotationStrip", true) {
    @Override
    protected CompilerPass createInternal(AbstractCompiler compiler) {
        return new ApplyAnnotationStrip((Compiler)compiler);
    }
};

Наш проход должен осуществится до оптимизационных проходов компилятора (В частности, до прохода, удаляющего strip'нутые символы), чтобы список stripTypes был использован.
Поэтому имеет смысл добавить его в конец метода getChecks, перед assertAllOneTimePasses(checks);

protected List<PassFactory> getChecks() {
    // ...
    checks.add(processDefines);

    checks.add(applyAnnotationStrip);

    assertAllOneTimePasses(checks);
    return checks;
  }

Google Closure Compiler с нашими дополнениями должен корректно обрабатывать исходный пример.

В частности:

/** @strip */
console = {
  debug: function() { /* ... */ }
}

function doSomething() {
	console.log()
	alert(1);
}

Должно становиться (обычная оптимизация, не продвинутая):

function doSomething(){alert(1)};

У меня не составило особого труда пропатчить исходники по этому документу с нуля, но на всякий случай - вот патч к ревизии 9: stripann.patch.txt.

У кода в этой статье два основных недостатка.

Во-первых, я не использовал его в production достаточно долго. Конечно, допилить и оттестировать - вопрос (не очень большого) времени, но честно говоря, я предпочитаю опцию stripTypes командной строки компилятора.

Во-вторых, добавление аннотации требует патча нескольких файлов компилятора.

Впрочем, это не должно быть проблемой, т.к. патчи не нарушают структуру кода и будут успешно мержится с SVN.

Надеюсь, пример из статьи послужит хорошей иллюстрацией, как можно добавлять собственные аннотации к Google Closure Compiler.

В зависимости от проекта, они могут оказаться не менее полезными, чем стандартные аннотации компилятора.


Автор: LioLick (не зарегистрирован), дата: 19 октября, 2011 - 11:44
#permalink

if (n.getType() == Token.FUNCTION) {
name = ((FunctionNode)n).getFunctionName();
}

Не работает приведение:
"com.google.javascript.rhino.Node cannot be cast to com.google.javascript.rhino.FunctionNode".

Удалось ли кому-то заставить работать?


Автор: Гость (не зарегистрирован), дата: 27 июня, 2021 - 14:43
#permalink

Да


Автор: 2048 cupcakes (не зарегистрирован), дата: 9 июля, 2021 - 06:34
#permalink

Этот метод процесса требуется для интерфейса CompilerPass и фактически выполняет переключение компилятора - путем обхода дерева сценария. Третий аргумент NodeTraversal.traverse - это объект, который обрабатывает узлы во время обхода. Для этого его необходимо посетить. Этот процесс обычно показывает код ошибки #


Автор: 2048 cupcakes (не зарегистрирован), дата: 9 июля, 2021 - 06:36
#permalink

Этот метод процесса требуется для интерфейса CompilerPass и фактически выполняет переключение компилятора - путем обхода дерева сценария. Третий аргумент NodeTraversal.traverse - это объект, который обрабатывает узлы во время обхода. Для этого его необходимо посетить. Этот процесс обычно показывает код ошибки #2048 cupcakes


Автор: Гость (не зарегистрирован), дата: 6 января, 2022 - 15:00
#permalink

Ну все так приемрно и работает - но с разными массивами данных и приразных вводных. Такие функции как правило нужны почти всем. Если заниматься кодировкой на джаве - в первую очередь будет надо еще знать алгоритмы перебора


Автор: Гость (не зарегистрирован), дата: 6 января, 2022 - 15:00
#permalink

Ну все так приемрно и работает - но с разными массивами данных и приразных вводных. Такие функции как правило нужны почти всем. Если заниматься кодировкой на джаве - в первую очередь будет надо еще знать алгоритмы перебора


 
Текущий раздел
Поиск по сайту
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Последние темы на форуме
Forum