com.google.javascript.jscomp.ReplaceMessages Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com.liferay.frontend.js.minifier
Show all versions of com.liferay.frontend.js.minifier
Liferay Frontend JS Minifier
/*
* Copyright 2004 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.GwtIncompatible;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.Iterator;
import javax.annotation.Nullable;
/**
* ReplaceMessages replaces user-visible messages with alternatives.
* It uses Google specific JsMessageVisitor implementation.
*
* @author [email protected] (Anatol Pomazau)
*/
@GwtIncompatible("JsMessage")
final class ReplaceMessages extends JsMessageVisitor {
private final MessageBundle bundle;
private final boolean strictReplacement;
static final DiagnosticType BUNDLE_DOES_NOT_HAVE_THE_MESSAGE =
DiagnosticType.error("JSC_BUNDLE_DOES_NOT_HAVE_THE_MESSAGE",
"Message with id = {0} could not be found in replacement bundle");
ReplaceMessages(AbstractCompiler compiler, MessageBundle bundle,
boolean checkDuplicatedMessages, JsMessage.Style style,
boolean strictReplacement) {
super(compiler, checkDuplicatedMessages, style, bundle.idGenerator());
this.bundle = bundle;
this.strictReplacement = strictReplacement;
}
@Override
void processMessageFallback(
Node callNode, JsMessage message1, JsMessage message2) {
boolean isFirstMessageTranslated =
(bundle.getMessage(message1.getId()) != null);
boolean isSecondMessageTranslated =
(bundle.getMessage(message2.getId()) != null);
Node replacementNode =
isSecondMessageTranslated && !isFirstMessageTranslated ?
callNode.getChildAtIndex(2) : callNode.getSecondChild();
callNode.replaceWith(replacementNode.detach());
Node changeScope = NodeUtil.getEnclosingChangeScopeRoot(replacementNode);
if (changeScope != null) {
compiler.reportChangeToChangeScope(changeScope);
}
}
@Override
protected void processJsMessage(JsMessage message,
JsMessageDefinition definition) {
// Get the replacement.
JsMessage replacement = bundle.getMessage(message.getId());
if (replacement == null) {
if (strictReplacement) {
compiler.report(JSError.make(
definition.getMessageNode(), BUNDLE_DOES_NOT_HAVE_THE_MESSAGE,
message.getId()));
// Fallback to the default message
return;
} else {
// In case if it is not a strict replacement we could leave original
// message.
replacement = message;
}
}
// Replace the message.
Node newValue;
Node msgNode = definition.getMessageNode();
try {
newValue = getNewValueNode(replacement, msgNode);
} catch (MalformedException e) {
compiler.report(JSError.make(
e.getNode(), MESSAGE_TREE_MALFORMED, e.getMessage()));
newValue = msgNode;
}
if (newValue != msgNode) {
newValue.useSourceInfoIfMissingFromForTree(msgNode);
msgNode.replaceWith(newValue);
compiler.reportChangeToEnclosingScope(newValue);
}
}
/**
* Constructs a node representing a message's value, or, if possible, just
* modifies {@code origValueNode} so that it accurately represents the
* message's value.
*
* @param message a message
* @param origValueNode the message's original value node
* @return a Node that can replace {@code origValueNode}
*
* @throws MalformedException if the passed node's subtree structure is
* not as expected
*/
private Node getNewValueNode(JsMessage message, Node origValueNode)
throws MalformedException {
switch (origValueNode.getToken()) {
case FUNCTION:
// The message is a function. Modify the function node.
updateFunctionNode(message, origValueNode);
return origValueNode;
case STRING:
// The message is a simple string. Modify the string node.
String newString = message.toString();
if (!origValueNode.getString().equals(newString)) {
origValueNode.setString(newString);
compiler.reportChangeToEnclosingScope(origValueNode);
}
return origValueNode;
case ADD:
// The message is a simple string. Create a string node.
return IR.string(message.toString());
case CALL:
// The message is a function call. Replace it with a string expression.
return replaceCallNode(message, origValueNode);
default:
throw new MalformedException(
"Expected FUNCTION, STRING, or ADD node; found: " + origValueNode.getToken(),
origValueNode);
}
}
/**
* Updates the descendants of a FUNCTION node to represent a message's value.
*
* The tree looks something like:
*
* function
* |-- name
* |-- lp
* | |-- name
* | -- name
* -- block
* |
* --return
* |
* --add
* |-- string foo
* -- name
*
*
* @param message a message
* @param functionNode the message's original FUNCTION value node
*
* @throws MalformedException if the passed node's subtree structure is
* not as expected
*/
private void updateFunctionNode(JsMessage message, Node functionNode)
throws MalformedException {
checkNode(functionNode, Token.FUNCTION);
Node nameNode = functionNode.getFirstChild();
checkNode(nameNode, Token.NAME);
Node argListNode = nameNode.getNext();
checkNode(argListNode, Token.PARAM_LIST);
Node oldBlockNode = argListNode.getNext();
checkNode(oldBlockNode, Token.BLOCK);
Iterator iterator = message.parts().iterator();
Node valueNode = iterator.hasNext()
? constructAddOrStringNode(iterator, argListNode)
: IR.string("");
Node newBlockNode = IR.block(IR.returnNode(valueNode));
// TODO(user): checkTreeEqual is overkill. I am in process of rewriting
// these functions.
if (newBlockNode.checkTreeEquals(oldBlockNode) != null) {
newBlockNode.useSourceInfoIfMissingFromForTree(oldBlockNode);
functionNode.replaceChild(oldBlockNode, newBlockNode);
compiler.reportChangeToEnclosingScope(newBlockNode);
}
}
/**
* Creates a parse tree corresponding to the remaining message parts in
* an iteration. The result will contain only STRING nodes, NAME nodes
* (corresponding to placeholder references), and/or ADD nodes used to
* combine the other two types.
*
* @param partsIterator an iterator over message parts
* @param argListNode a PARAM_LIST node whose children are valid placeholder names
* @return the root of the constructed parse tree
*
* @throws MalformedException if {@code partsIterator} contains a
* placeholder reference that does not correspond to a valid argument in
* the arg list
*/
private static Node constructAddOrStringNode(Iterator partsIterator,
Node argListNode)
throws MalformedException {
CharSequence part = partsIterator.next();
Node partNode = null;
if (part instanceof JsMessage.PlaceholderReference) {
JsMessage.PlaceholderReference phRef =
(JsMessage.PlaceholderReference) part;
for (Node node : argListNode.children()) {
if (node.isName()) {
String arg = node.getString();
// We ignore the case here because the transconsole only supports
// uppercase placeholder names, but function arguments in JavaScript
// code can have mixed case.
if (arg.equalsIgnoreCase(phRef.getName())) {
partNode = IR.name(arg);
}
}
}
if (partNode == null) {
throw new MalformedException(
"Unrecognized message placeholder referenced: " + phRef.getName(),
argListNode);
}
} else {
// The part is just a string literal.
partNode = IR.string(part.toString());
}
if (partsIterator.hasNext()) {
return IR.add(partNode,
constructAddOrStringNode(partsIterator, argListNode));
} else {
return partNode;
}
}
/**
* Replaces a CALL node with an inlined message value.
*
* The call tree looks something like:
*
* call
* |-- getprop
* | |-- name 'goog'
* | +-- string 'getMsg'
* |
* |-- string 'Hi {$userName}! Welcome to {$product}.'
* +-- objlit
* |-- string 'userName'
* |-- name 'someUserName'
* |-- string 'product'
* +-- call
* +-- name 'getProductName'
*
*
* For that example, we'd return:
*
* add
* |-- string 'Hi '
* +-- add
* |-- name someUserName
* +-- add
* |-- string '! Welcome to '
* +-- add
* |-- call
* | +-- name 'getProductName'
* +-- string '.'
*
* @param message a message
* @param callNode the message's original CALL value node
* @return a STRING node, or an ADD node that does string concatenation, if
* the message has one or more placeholders
*
* @throws MalformedException if the passed node's subtree structure is
* not as expected
*/
private Node replaceCallNode(JsMessage message, Node callNode)
throws MalformedException {
checkNode(callNode, Token.CALL);
Node getPropNode = callNode.getFirstChild();
checkNode(getPropNode, Token.GETPROP);
Node stringExprNode = getPropNode.getNext();
checkStringExprNode(stringExprNode);
Node objLitNode = stringExprNode.getNext();
// Build the replacement tree.
return constructStringExprNode(
message.parts().iterator(), objLitNode, callNode);
}
/**
* Creates a parse tree corresponding to the remaining message parts in an
* iteration. The result consists of one or more STRING nodes, placeholder
* replacement value nodes (which can be arbitrary expressions), and ADD
* nodes.
*
* @param parts an iterator over message parts
* @param objLitNode an OBJLIT node mapping placeholder names to values
* @return the root of the constructed parse tree
*
* @throws MalformedException if {@code parts} contains a placeholder
* reference that does not correspond to a valid placeholder name
*/
private static Node constructStringExprNode(
Iterator parts, Node objLitNode, Node refNode) throws MalformedException {
checkNotNull(refNode);
CharSequence part = parts.next();
Node partNode = null;
if (part instanceof JsMessage.PlaceholderReference) {
JsMessage.PlaceholderReference phRef =
(JsMessage.PlaceholderReference) part;
// The translated message is null
if (objLitNode == null) {
throw new MalformedException("Empty placeholder value map " +
"for a translated message with placeholders.", refNode);
}
for (Node key = objLitNode.getFirstChild(); key != null;
key = key.getNext()) {
if (key.getString().equals(phRef.getName())) {
Node valueNode = key.getFirstChild();
partNode = valueNode.cloneTree();
}
}
if (partNode == null) {
throw new MalformedException(
"Unrecognized message placeholder referenced: " + phRef.getName(),
objLitNode);
}
} else {
// The part is just a string literal.
partNode = IR.string(part.toString());
}
if (parts.hasNext()) {
return IR.add(partNode,
constructStringExprNode(parts, objLitNode, refNode));
} else {
return partNode;
}
}
/**
* Checks that a node is a valid string expression (either a string literal
* or a concatenation of string literals).
*
* @throws IllegalArgumentException if the node is null or the wrong type
*/
private static void checkStringExprNode(@Nullable Node node) {
if (node == null) {
throw new IllegalArgumentException("Expected a string; found: null");
}
switch (node.getToken()) {
case STRING:
break;
case ADD:
Node c = node.getFirstChild();
checkStringExprNode(c);
checkStringExprNode(c.getNext());
break;
default:
throw new IllegalArgumentException("Expected a string; found: " + node.getToken());
}
}
}