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

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

/*
 * 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.common.base.Ascii;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;

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

  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}).");

  ReplaceMessages(
      AbstractCompiler compiler,
      MessageBundle bundle,
      boolean checkDuplicatedMessages,
      JsMessage.Style style,
      boolean strictReplacement) {
    super(compiler, checkDuplicatedMessages, style, bundle.idGenerator());

    this.bundle = bundle;
    this.strictReplacement = strictReplacement;
  }

  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;
  }

  @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.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 = constructAddOrStringNode(iterator, argListNode); Node newBlockNode = IR.block(IR.returnNode(valueNode)); if (!newBlockNode.isEquivalentTo( oldBlockNode, /* compareType= */ false, /* recurse= */ true, /* jsDoc= */ false, /* sideEffect= */ false)) { 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 { if (!partsIterator.hasNext()) { return IR.string(""); } 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 (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 = 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_key 'userName'
   *      |   +-- name 'someUserName'
   *      +-- string_key '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(); Map options = getOptions(objLitNode != null ? objLitNode.getNext() : null); // Build the replacement tree. Iterator iterator = message.parts().iterator(); return iterator.hasNext() ? constructStringExprNode(iterator, objLitNode, options, callNode) : IR.string(""); } /** * 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, @Nullable Node objLitNode, Map options, 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. String s = part.toString(); if (options.getOrDefault("html", false)) { // Note that "&" is not replaced because the translation can contain HTML entities. s = s.replace("<", "<"); } partNode = IR.string(s); } if (parts.hasNext()) { return IR.add(partNode, constructStringExprNode(parts, objLitNode, options, refNode)); } else { return partNode; } } private static Map getOptions(@Nullable Node optionsNode) throws MalformedException { Map options = new HashMap<>(); 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(); if (!value.isTrue() && !value.isFalse()) { throw new MalformedException("Literal true or false expected", value); } switch (optName) { case "html": options.put(optName, value.isTrue()); break; default: throw new MalformedException("Unexpected option", aNode); } } return options; } /** * 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: 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()); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy