Создание собственных аннотаций
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.
В зависимости от проекта, они могут оказаться не менее полезными, чем стандартные аннотации компилятора.
|
if (n.getType() == Token.FUNCTION) {
name = ((FunctionNode)n).getFunctionName();
}
Не работает приведение:
"com.google.javascript.rhino.Node cannot be cast to com.google.javascript.rhino.FunctionNode".
Удалось ли кому-то заставить работать?
Да
Этот метод процесса требуется для интерфейса CompilerPass и фактически выполняет переключение компилятора - путем обхода дерева сценария. Третий аргумент NodeTraversal.traverse - это объект, который обрабатывает узлы во время обхода. Для этого его необходимо посетить. Этот процесс обычно показывает код ошибки #
Этот метод процесса требуется для интерфейса CompilerPass и фактически выполняет переключение компилятора - путем обхода дерева сценария. Третий аргумент NodeTraversal.traverse - это объект, который обрабатывает узлы во время обхода. Для этого его необходимо посетить. Этот процесс обычно показывает код ошибки #2048 cupcakes
Ну все так приемрно и работает - но с разными массивами данных и приразных вводных. Такие функции как правило нужны почти всем. Если заниматься кодировкой на джаве - в первую очередь будет надо еще знать алгоритмы перебора
Ну все так приемрно и работает - но с разными массивами данных и приразных вводных. Такие функции как правило нужны почти всем. Если заниматься кодировкой на джаве - в первую очередь будет надо еще знать алгоритмы перебора