All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.ReplaceMessages Maven / Gradle / Ivy

There is a newer version: 9.0.8
Show newest version
/*
 * 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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.AstFactory.type;
import static com.google.javascript.jscomp.JsMessageVisitor.MESSAGE_TREE_MALFORMED;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.JsMessage.PlaceholderFormatException;
import com.google.javascript.jscomp.JsMessageVisitor.MalformedException;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Node.SideEffectFlags;
import com.google.javascript.rhino.Token;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * ReplaceMessages replaces user-visible messages with alternatives. It uses Google specific
 * JsMessageVisitor implementation.
 */
@GwtIncompatible("JsMessage")
public final class ReplaceMessages {
  public 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");

  public static final DiagnosticType INVALID_ALTERNATE_MESSAGE_PLACEHOLDERS =
      DiagnosticType.error(
          "JSC_INVALID_ALTERNATE_MESSAGE_PLACEHOLDERS",
          "Alternate message ID={0} placeholders ({1}) differs from {2} placeholders ({3}).");

  private final AbstractCompiler compiler;
  private final MessageBundle bundle;
  private final JsMessage.Style style;
  private final boolean strictReplacement;
  private final AstFactory astFactory;

  ReplaceMessages(
      AbstractCompiler compiler,
      MessageBundle bundle,
      JsMessage.Style style,
      boolean strictReplacement) {
    this.compiler = compiler;
    this.astFactory = compiler.createAstFactory();
    this.bundle = bundle;
    this.style = style;
    this.strictReplacement = strictReplacement;
  }

  /**
   * When the returned pass is executed, the original `goog.getMsg()` etc. calls will be replaced
   * with a form that will survive unchanged through optimizations unless eliminated as unused.
   *
   * 

After all optimizations are complete, the pass returned by `getReplacementCompletionPass()`. */ public CompilerPass getMsgProtectionPass() { return new MsgProtectionPass(); } class MsgProtectionPass extends JsMessageVisitor { public MsgProtectionPass() { super(ReplaceMessages.this.compiler, style, bundle.idGenerator()); } @Override public void process(Node externs, Node root) { // Add externs declarations for the function names we use in our replacements. NodeUtil.createSynthesizedExternsSymbol(compiler, ReplaceMessagesConstants.DEFINE_MSG_CALLEE); NodeUtil.createSynthesizedExternsSymbol( compiler, ReplaceMessagesConstants.FALLBACK_MSG_CALLEE); // JsMessageVisitor.process() does the traversal that calls the processX() methods below. super.process(externs, root); } @Override protected void processJsMessage(JsMessage message, JsMessageDefinition definition) { try { final Node origValueNode = definition.getMessageNode(); switch (origValueNode.getToken()) { case CALL: // This is the currently preferred form. // `MSG_A = goog.getMsg('hello, {$name}', {name: getName()}, {html: true})` protectGetMsgCall(origValueNode, message); break; case STRINGLIT: case ADD: // a legacy format // `MSG_A = 'abc' + 'def';` protectStringLiteralOrConcatMsg(origValueNode, message); break; case FUNCTION: protectLegacyFunctionMsg(origValueNode, message); break; default: throw new MalformedException( "Expected FUNCTION, STRING, ADD, or CALL node; found: " + origValueNode, origValueNode); } } catch (MalformedException e) { compiler.report(JSError.make(e.getNode(), MESSAGE_TREE_MALFORMED, e.getMessage())); } } private void protectGetMsgCall(Node callNode, JsMessage message) throws MalformedException { checkArgument(callNode.isCall(), callNode); // `goog.getMsg('message string', {}, {})` final Node googGetMsg = callNode.getFirstChild(); final Node originalMessageString = checkNotNull(googGetMsg.getNext()); final Node placeholdersNode = originalMessageString.getNext(); final Node optionsNode = placeholdersNode == null ? null : placeholdersNode.getNext(); final MsgOptions msgOptions = getOptions(message, optionsNode); // Construct // `__jscomp_define_msg__({}, {})` final String protectionFunctionName = ReplaceMessagesConstants.DEFINE_MSG_CALLEE; final Node newCallee = createProtectionFunctionCallee(protectionFunctionName).srcref(googGetMsg); final Node msgPropertiesNode = createMsgPropertiesNode(message, msgOptions).srcrefTree(originalMessageString); Node newCallNode = astFactory.createCall(newCallee, type(callNode), msgPropertiesNode).srcref(callNode); // If the result of this call (the message) is unused, there is no reason for optimizations // to preserve it. newCallNode.setSideEffectFlags(SideEffectFlags.NO_SIDE_EFFECTS); if (placeholdersNode != null) { checkState(placeholdersNode.isObjectLit(), placeholdersNode); // put quotes around the keys so they won't get renamed. for (Node strKey = placeholdersNode.getFirstChild(); strKey != null; strKey = strKey.getNext()) { checkState(strKey.isStringKey(), strKey); strKey.setQuotedString(); } newCallNode.addChildToBack(placeholdersNode.detach()); } callNode.replaceWith(newCallNode); compiler.reportChangeToEnclosingScope(newCallNode); } private Node createProtectionFunctionCallee(String protectionFunctionName) { final Node callee = astFactory.createNameWithUnknownType(protectionFunctionName); // The name is declared constant in the externs definition we created, so all references // to it must also be marked as constant for consistency's sake. callee.putBooleanProp(Node.IS_CONSTANT_NAME, true); return callee; } private void protectStringLiteralOrConcatMsg(Node valueNode, JsMessage message) { final Node msgProps = createMsgPropertiesNode(message, new MsgOptions()); final Node newCallNode = astFactory .createCall( createProtectionFunctionCallee(ReplaceMessagesConstants.DEFINE_MSG_CALLEE), type(valueNode), msgProps) .srcrefTreeIfMissing(valueNode); newCallNode.setSideEffectFlags(SideEffectFlags.NO_SIDE_EFFECTS); valueNode.replaceWith(newCallNode); compiler.reportChangeToEnclosingScope(newCallNode); } private void protectLegacyFunctionMsg(Node functionNode, JsMessage message) { // `MSG_X = function(name1, name2) { return expressionUsingName1AndName2; };` // NOTE: The JsMessageVisitor code will have examined the return value and constructed a // message string with placeholders matching the parameter names. checkArgument(functionNode.isFunction(), functionNode); final Node paramListNode = functionNode.getSecondChild(); final Node origBlock = paramListNode.getNext(); // NOTE: JsMessageVisitor would have thrown a MalformedException before reaching this point // if the function body were not a single return statement. final Node returnNode = origBlock.getOnlyChild(); final Node origValueNode = returnNode.getOnlyChild(); // Convert to // ```javascript // MSG_X = function(name1, name2) { // return __jscomp_define_msg__({}, {}); // }; // ``` final Node newCallee = createProtectionFunctionCallee(ReplaceMessagesConstants.DEFINE_MSG_CALLEE); final Node msgPropertiesNode = createMsgPropertiesNode(message, new MsgOptions()); // Convert the parameter list into a simple placeholders object // ```javascript // { // 'name1': name1, // 'name2': name2 // } // ``` QuotedKeyObjectLitBuilder placeholderObjLlitBuilder = new QuotedKeyObjectLitBuilder(); for (Node paramName = paramListNode.getFirstChild(); paramName != null; paramName = paramName.getNext()) { // Just assert here, because JsMessageVisitor should have already reported it if the // parameter list were malformed. checkState(paramName.isName(), paramName); placeholderObjLlitBuilder.addNode(paramName.getString(), paramName.cloneNode()); } final Node placeholderNode = placeholderObjLlitBuilder.build(); final Node newValueNode = astFactory .createCall(newCallee, type(origValueNode), msgPropertiesNode, placeholderNode) .srcrefTreeIfMissing(origValueNode); origValueNode.replaceWith(newValueNode); compiler.reportChangeToChangeScope(functionNode); } @Override void processMessageFallback(Node callNode, JsMessage message1, JsMessage message2) { final Node originalCallee = checkNotNull(callNode.getFirstChild(), callNode); final Node fallbackCallee = createProtectionFunctionCallee(ReplaceMessagesConstants.FALLBACK_MSG_CALLEE) .srcref(originalCallee); final Node originalFirstArg = checkNotNull(originalCallee.getNext(), callNode); final Node firstMsgKey = astFactory.createString(message1.getKey()).srcref(originalFirstArg); final Node originalSecondArg = checkNotNull(originalFirstArg.getNext(), callNode); final Node secondMsgKey = astFactory.createString(message2.getKey()).srcref(originalSecondArg); // `__jscomp_msg_fallback__('MSG_ONE', MSG_ONE, 'MSG_TWO', MSG_TWO)` final Node newCallNode = astFactory .createCall( fallbackCallee, type(callNode), firstMsgKey, originalFirstArg.detach(), secondMsgKey, originalSecondArg.detach()) .srcref(callNode); // If the result of this call (the message) is unused, there is no reason for optimizations // to preserve it. newCallNode.setSideEffectFlags(SideEffectFlags.NO_SIDE_EFFECTS); callNode.replaceWith(newCallNode); compiler.reportChangeToEnclosingScope(newCallNode); } } private Node createMsgPropertiesNode(JsMessage message, MsgOptions msgOptions) { QuotedKeyObjectLitBuilder msgPropsBuilder = new QuotedKeyObjectLitBuilder(); msgPropsBuilder.addString("key", message.getKey()); String altId = message.getAlternateId(); if (altId != null) { msgPropsBuilder.addString("alt_id", altId); } final String meaning = message.getMeaning(); if (meaning != null) { msgPropsBuilder.addString("meaning", meaning); } msgPropsBuilder.addString("msg_text", message.toString()); if (msgOptions.escapeLessThan) { // Just being present is what records this option as true msgPropsBuilder.addString("escapeLessThan", ""); } if (msgOptions.unescapeHtmlEntities) { // Just being present is what records this option as true msgPropsBuilder.addString("unescapeHtmlEntities", ""); } return msgPropsBuilder.build(); } private final class QuotedKeyObjectLitBuilder { // LinkedHashMap to keep the keys in the order we set them so our output is deterministic. private final LinkedHashMap keyToValueNodeMap = new LinkedHashMap<>(); private QuotedKeyObjectLitBuilder addString(String key, String value) { return addNode(key, astFactory.createString(value)); } private QuotedKeyObjectLitBuilder addNode(String key, Node node) { checkState(!keyToValueNodeMap.containsKey(key), "repeated key: %s", key); keyToValueNodeMap.put(key, node); return this; } private Node build() { final Node result = astFactory.createObjectLit(); for (Entry entry : keyToValueNodeMap.entrySet()) { result.addChildToBack(astFactory.createQuotedStringKey(entry.getKey(), entry.getValue())); } return result; } } /** * When the returned pass is executed, the protected messages created by `getMsgProtectionPass()` * will be replaced by the final message form read from the appropriate message bundle. */ public CompilerPass getReplacementCompletionPass() { return new ReplacementCompletionPass(); } class ReplacementCompletionPass implements CompilerPass { // Keep track of which messages actually got translated, so we know what do do when we // see a message fallback call. final Set translatedMsgKeys = new HashSet<>(); @Override public void process(Node externs, Node root) { // for each `__jscomp_define_msg__` call in post-order traversal // replace it with the appropriate expression NodeTraversal.traverse( compiler, root, new AbstractPostOrderCallback() { @Override public void visit(NodeTraversal t, Node n, Node parent) { ProtectedJsMessage protectedJsMessage = ProtectedJsMessage.fromAstNode(n, bundle.idGenerator()); if (protectedJsMessage != null) { visitMsgDefinition(protectedJsMessage); } else { ProtectedMsgFallback protectedMsgFallback = ProtectedMsgFallback.fromAstNode(n); if (protectedMsgFallback != null) { visitMsgFallback(protectedMsgFallback); } } } }); } void visitMsgDefinition(ProtectedJsMessage protectedJsMessage) { try { final JsMessage originalMsg = protectedJsMessage.jsMessage; final Node nodeToReplace = protectedJsMessage.definitionNode; final JsMessage translatedMsg = lookupMessage(protectedJsMessage.definitionNode, bundle, originalMsg); final JsMessage msgToUse; if (translatedMsg != null) { msgToUse = translatedMsg; // Remember that this one got translated in case it is used in a fallback. translatedMsgKeys.add(originalMsg.getKey()); } else { if (strictReplacement) { compiler.report( JSError.make(nodeToReplace, BUNDLE_DOES_NOT_HAVE_THE_MESSAGE, originalMsg.getId())); } msgToUse = originalMsg; } final MsgOptions msgOptions = new MsgOptions(); msgOptions.escapeLessThan = protectedJsMessage.escapeLessThan; msgOptions.unescapeHtmlEntities = protectedJsMessage.unescapeHtmlEntities; final Map placeholderMap = createPlaceholderNodeMap(protectedJsMessage.substitutionsNode); final ImmutableSet placeholderNames = msgToUse.placeholders(); if (placeholderMap.isEmpty() && !placeholderNames.isEmpty()) { throw new MalformedException( "Empty placeholder value map for a translated message with placeholders.", nodeToReplace); } else { for (String placeholderName : placeholderNames) { if (!placeholderMap.containsKey(placeholderName)) { throw new MalformedException( "Unrecognized message placeholder referenced: " + placeholderName, nodeToReplace); } } } final Node finalMsgConstructionExpression = constructStringExprNode( mergeStringParts(msgToUse.getParts()), placeholderMap, msgOptions); finalMsgConstructionExpression.srcrefTreeIfMissing(nodeToReplace); nodeToReplace.replaceWith(finalMsgConstructionExpression); compiler.reportChangeToEnclosingScope(finalMsgConstructionExpression); } catch (MalformedException e) { compiler.report(JSError.make(e.getNode(), MESSAGE_TREE_MALFORMED, e.getMessage())); } } private void visitMsgFallback(ProtectedMsgFallback protectedMsgFallback) { final Node valueNodeToUse; if (translatedMsgKeys.contains(protectedMsgFallback.firstMsgKey)) { // Obviously use the first message, if it is translated. valueNodeToUse = protectedMsgFallback.firstMsgValue; } else if (translatedMsgKeys.contains(protectedMsgFallback.secondMsgKey)) { // Fallback to the second message if it has a translation. valueNodeToUse = protectedMsgFallback.secondMsgValue; } else { // If neither is translated, then use the first message as it is defined in the source code. valueNodeToUse = protectedMsgFallback.firstMsgValue; } valueNodeToUse.detach(); protectedMsgFallback.callNode.replaceWith(valueNodeToUse); compiler.reportChangeToEnclosingScope(valueNodeToUse); } } private static class ProtectedMsgFallback { final Node callNode; final String firstMsgKey; final Node firstMsgValue; final String secondMsgKey; final Node secondMsgValue; ProtectedMsgFallback( Node callNode, String firstMsgKey, Node firstMsgValue, String secondMsgKey, Node secondMsgValue) { this.callNode = callNode; this.firstMsgKey = firstMsgKey; this.firstMsgValue = firstMsgValue; this.secondMsgKey = secondMsgKey; this.secondMsgValue = secondMsgValue; } @Nullable static ProtectedMsgFallback fromAstNode(Node n) { if (!n.isCall()) { return null; } final Node callee = n.getFirstChild(); if (!callee.matchesName(ReplaceMessagesConstants.FALLBACK_MSG_CALLEE)) { return null; } checkState(n.hasXChildren(5), "bad message fallback call: %s", n); final Node firstMsgKeyNode = callee.getNext(); final String firstMsgKey = firstMsgKeyNode.getString(); final Node firstMsgValue = firstMsgKeyNode.getNext(); final Node secondMsgKeyNode = firstMsgValue.getNext(); final String secondMsgKey = secondMsgKeyNode.getString(); final Node secondMsgValue = secondMsgKeyNode.getNext(); return new ProtectedMsgFallback(n, firstMsgKey, firstMsgValue, secondMsgKey, secondMsgValue); } } private JsMessage lookupMessage(Node callNode, MessageBundle bundle, JsMessage message) { JsMessage translatedMessage = bundle.getMessage(message.getId()); if (translatedMessage != null) { return translatedMessage; } String alternateId = message.getAlternateId(); if (alternateId == null) { return null; } JsMessage alternateMessage = bundle.getMessage(alternateId); if (alternateMessage != null) { // Validate that the alternate message is compatible with this message. Ideally we'd also // check meaning and description, but they're not populated by `MessageBundle.getMessage`. if (!Objects.equals(message.placeholders(), alternateMessage.placeholders())) { compiler.report( JSError.make( callNode, INVALID_ALTERNATE_MESSAGE_PLACEHOLDERS, alternateId, String.valueOf(alternateMessage.placeholders()), message.getKey(), String.valueOf(message.placeholders()))); return null; } } return alternateMessage; } /** * When the returned pass is executed, the original `goog.getMsg()` etc. calls will all be * replaced with the final message form read from the message bundle. * *

This is the original way of running this pass as a single operation. */ public CompilerPass getFullReplacementPass() { return new FullReplacementPass(); } class FullReplacementPass extends JsMessageVisitor { public FullReplacementPass() { super(ReplaceMessages.this.compiler, style, bundle.idGenerator()); } @Override void processMessageFallback(Node callNode, JsMessage message1, JsMessage message2) { boolean isFirstMessageTranslated = (lookupMessage(callNode, bundle, message1) != null); boolean isSecondMessageTranslated = (lookupMessage(callNode, bundle, message2) != 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. Node callNode = definition.getMessageNode(); JsMessage replacement = lookupMessage(callNode, bundle, message); if (replacement == null) { if (strictReplacement) { compiler.report( JSError.make(callNode, 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.srcrefTreeIfMissing(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 STRINGLIT: // 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 astFactory.createString(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); Node valueNode = constructAddOrStringNode(message.getParts(), argListNode); Node newBlockNode = IR.block(astFactory.createReturn(valueNode)); if (!newBlockNode.isEquivalentTo( oldBlockNode, /* compareType= */ false, /* recurse= */ true, /* jsDoc= */ false, /* sideEffect= */ false)) { newBlockNode.srcrefTreeIfMissing(oldBlockNode); oldBlockNode.replaceWith(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 parts the 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 Node constructAddOrStringNode(ImmutableList parts, Node argListNode) throws MalformedException { if (parts.isEmpty()) { return astFactory.createString(""); } Node resultNode = null; for (CharSequence part : parts) { final Node partNode = constructLegacyFunctionMsgPart(argListNode, part); resultNode = resultNode == null ? partNode : astFactory.createAdd(resultNode, partNode); } return resultNode; } private Node constructLegacyFunctionMsgPart(Node argListNode, CharSequence part) throws MalformedException { Node partNode = null; if (part instanceof JsMessage.PlaceholderReference) { JsMessage.PlaceholderReference phRef = (JsMessage.PlaceholderReference) part; for (Node node = argListNode.getFirstChild(); node != null; node = node.getNext()) { 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 (Ascii.equalsIgnoreCase(arg, 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 = astFactory.createString(part.toString()); } return partNode; } /** * Replaces a CALL node with an inlined message value. * *

For input that that looks like this *

     *   goog.getMsg(
     *       'Hi {$userName}! Welcome to {$product}.',
     *       { 'userName': 'someUserName', 'product': getProductName() })
     * 
     *
     * 

We'd return: *

     *   'Hi ' + someUserName + '! Welcome to ' + 'getProductName()' + '.'
     * 
* * @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); // `goog.getMsg` Node getPropNode = callNode.getFirstChild(); checkNode(getPropNode, Token.GETPROP); Node stringExprNode = getPropNode.getNext(); checkStringExprNode(stringExprNode); // optional `{ key1: value, key2: value2 }` replacements Node objLitNode = stringExprNode.getNext(); // optional replacement options, e.g. `{ html: true }` MsgOptions options = getOptions(message, objLitNode != null ? objLitNode.getNext() : null); Map placeholderMap = createPlaceholderNodeMap(objLitNode); final ImmutableSet placeholderNames = message.placeholders(); if (placeholderMap.isEmpty() && !placeholderNames.isEmpty()) { throw new MalformedException( "Empty placeholder value map for a translated message with placeholders.", callNode); } else { for (String placeholderName : placeholderNames) { if (!placeholderMap.containsKey(placeholderName)) { throw new MalformedException( "Unrecognized message placeholder referenced: " + placeholderName, callNode); } } } // Build the replacement tree. return constructStringExprNode(mergeStringParts(message.getParts()), placeholderMap, options); } } private Map createPlaceholderNodeMap(Node objLitNode) throws MalformedException { Map placeholderMap = new HashMap<>(); if (objLitNode != null) { for (Node key = objLitNode.getFirstChild(); key != null; key = key.getNext()) { checkState(key.isStringKey(), key); String name = key.getString(); boolean isKeyAlreadySeen = placeholderMap.put(name, key.getOnlyChild()) != null; if (isKeyAlreadySeen) { throw new MalformedException("Duplicate placeholder name", key); } } } return placeholderMap; } /** * 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 msgParts an iterator over message parts * @param placeholderMap map from placeholder names to value Nodes * @return the root of the constructed parse tree */ private Node constructStringExprNode( List msgParts, Map placeholderMap, MsgOptions options) { if (msgParts.isEmpty()) { return astFactory.createString(""); } else { Node resultNode = null; for (CharSequence msgPart : msgParts) { final Node partNode = createNodeForMsgPart(msgPart, options, placeholderMap); resultNode = (resultNode == null) ? partNode : astFactory.createAdd(resultNode, partNode); } return resultNode; } } private Node createNodeForMsgPart( CharSequence part, MsgOptions options, Map placeholderMap) { final Node partNode; if (part instanceof JsMessage.PlaceholderReference) { JsMessage.PlaceholderReference phRef = (JsMessage.PlaceholderReference) part; final String placeholderName = phRef.getName(); partNode = checkNotNull(placeholderMap.get(placeholderName)).cloneTree(); } else { // The part is just a string literal. String s = part.toString(); if (options.escapeLessThan) { // Note that "&" is not replaced because the translation can contain HTML entities. s = s.replace("<", "<"); } if (options.unescapeHtmlEntities) { // Unescape entities that need to be escaped when embedding HTML or XML in data/attributes // of an HTML/XML document. See https://www.w3.org/TR/xml/#sec-predefined-ent. // Note that "&" must be the last to avoid "creating" new entities. // To print an html entity in the resulting message, double-escape: `&amp;`. s = s.replace("<", "<") .replace(">", ">") .replace("'", "'") .replace(""", "\"") .replace("&", "&"); } partNode = astFactory.createString(s); } return partNode; } private static MsgOptions getOptions(JsMessage message, @Nullable Node optionsNode) throws MalformedException { MsgOptions options = new MsgOptions(); if (optionsNode == null) { return options; } if (!optionsNode.isObjectLit()) { throw new MalformedException("OBJLIT node expected", optionsNode); } for (Node aNode = optionsNode.getFirstChild(); aNode != null; aNode = aNode.getNext()) { if (!aNode.isStringKey()) { throw new MalformedException("STRING_KEY node expected as OBJLIT key", aNode); } String optName = aNode.getString(); Node value = aNode.getFirstChild(); switch (optName) { case "html": options.escapeLessThan = value.isTrue(); if (!value.isTrue() && !value.isFalse()) { throw new MalformedException("html: Literal true or false expected", value); } break; case "unescapeHtmlEntities": options.unescapeHtmlEntities = value.isTrue(); if (!value.isTrue() && !value.isFalse()) { throw new MalformedException( "unescapeHtmlEntities: Literal true or false expected", value); } break; case "example": case "original_code": { // Verify this format to inform users ASAP if they're supplying something unexpected. // These options are only used when generating the XMB file, but normal compilations // happen much more frequently than that, so we want to report these errors now. // ``` // { // example: { // 'name': 'George' // }, // original_code: { // 'name': 'getName()' // } // } // ``` if (!value.isObjectLit()) { throw new MalformedException(optName + ": object literal required", value); } final ImmutableSet placeholders = message.placeholders(); Node stringKeyNode; for (stringKeyNode = value.getFirstChild(); stringKeyNode != null; stringKeyNode = stringKeyNode.getNext()) { if (!stringKeyNode.isStringKey()) { throw new MalformedException("placeholder name required", stringKeyNode); } String placeholderName = stringKeyNode.getString(); if (!placeholders.contains(placeholderName)) { throw new MalformedException("unknown placeholder name", stringKeyNode); } Node placeholderValue = stringKeyNode.getOnlyChild(); if (!placeholderValue.isStringLit()) { throw new MalformedException("string literal required", placeholderValue); } } break; } default: throw new MalformedException("Unexpected option", aNode); } } return options; } /** Options for escaping characters in translated messages. */ private static class MsgOptions { // Replace `'<'` with `'<'` in the message. private boolean escapeLessThan = false; // Replace these escaped entities with their literal characters in the message // (Overrides escapeLessThan) // '<' -> '<' // '>' -> '>' // ''' -> "'" // '"' -> '"' // '&' -> '&' private boolean unescapeHtmlEntities = false; } /** Merges consecutive string parts in the list of message parts. */ private static List mergeStringParts(List parts) { List result = new ArrayList<>(); for (CharSequence part : parts) { if (part instanceof JsMessage.PlaceholderReference) { result.add(part); } else { CharSequence lastPart = result.isEmpty() ? null : Iterables.getLast(result); if (lastPart == null || lastPart instanceof JsMessage.PlaceholderReference) { result.add(part); } else { result.set(result.size() - 1, lastPart.toString() + part); } } } return result; } /** * 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 STRINGLIT: case TEMPLATELIT: break; case ADD: Node c = node.getFirstChild(); checkStringExprNode(c); checkStringExprNode(c.getNext()); break; default: throw new IllegalArgumentException("Expected a string; found: " + node.getToken()); } } /** * Holds information about the protected form of a translatable message that appears in the AST. * *

The original translatable messages are replaced with this protected form by the logic in * `ReplaceMessages` to protect the message information through the optimization passes. * *


   *   // original
   *   var MSG_GREETING = goog.getMsg('Hello, {$name}!', {name: person.getName()}, {html: false});
   *   // protected form
   *   var MSG_GREETING = __jscomp_define_msg__(
   *       {
   *         'key': 'MSG_GREETING',
   *         'msg_text': 'Hello, {$name}!',
   *         'escapeLessThan': ''
   *       },
   *       {'name': person.getName()});
   * 
*/ public static class ProtectedJsMessage { private final JsMessage jsMessage; // The expression Node that defines the message and should be replaced with the localized // message. private final Node definitionNode; // e.g. `{ name: x.getName(), age: x.getAgeString() }` @Nullable private final Node substitutionsNode; // Replace `'<'` with `'<'` in the message. private final boolean escapeLessThan; // Replace these escaped entities with their literal characters in the message // (Overrides escapeLessThan) // '<' -> '<' // '>' -> '>' // ''' -> "'" // '"' -> '"' // '&' -> '&' private final boolean unescapeHtmlEntities; private ProtectedJsMessage( JsMessage jsMessage, Node definitionNode, @Nullable Node substitutionsNode, boolean escapeLessThan, boolean unescapeHtmlEntities) { this.jsMessage = jsMessage; this.definitionNode = definitionNode; this.substitutionsNode = substitutionsNode; this.escapeLessThan = escapeLessThan; this.unescapeHtmlEntities = unescapeHtmlEntities; } @Nullable public static ProtectedJsMessage fromAstNode(Node node, JsMessage.IdGenerator idGenerator) { if (!node.isCall()) { return null; } final Node calleeNode = checkNotNull(node.getFirstChild(), node); if (!calleeNode.matchesName(ReplaceMessagesConstants.DEFINE_MSG_CALLEE)) { return null; } final Node propertiesNode = checkNotNull(calleeNode.getNext(), calleeNode); final Node substitutionsNode = propertiesNode.getNext(); boolean escapeLessThanOption = false; boolean unescapeHtmlEntitiesOption = false; JsMessage.Builder jsMessageBuilder = new JsMessage.Builder(); checkState(propertiesNode.isObjectLit(), propertiesNode); for (Node strKey = propertiesNode.getFirstChild(); strKey != null; strKey = strKey.getNext()) { checkState(strKey.isStringKey(), strKey); String key = strKey.getString(); Node valueNode = strKey.getOnlyChild(); checkState(valueNode.isStringLit(), valueNode); String value = valueNode.getString(); switch (key) { case "key": jsMessageBuilder.setKey(value); break; case "meaning": jsMessageBuilder.setMeaning(value); break; case "alt_id": jsMessageBuilder.setAlternateId(value); break; case "msg_text": try { jsMessageBuilder.setMsgText(value); } catch (PlaceholderFormatException unused) { // Somehow we stored the protected message text incorrectly, which should never // happen. throw new IllegalStateException( valueNode.getLocation() + ": Placeholder incorrectly formatted: >" + value + "<"); } break; case "escapeLessThan": // Just being present enables this option escapeLessThanOption = true; break; case "unescapeHtmlEntities": // just being present enables this option unescapeHtmlEntitiesOption = true; break; default: throw new IllegalStateException("unknown protected message key: " + strKey); } } return new ProtectedJsMessage( jsMessageBuilder.build(idGenerator), node, substitutionsNode, escapeLessThanOption, unescapeHtmlEntitiesOption); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy