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

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

/*
 * Copyright 2008 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 static com.google.common.base.Preconditions.checkState;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.CaseFormat;
import com.google.debugging.sourcemap.proto.Mapping.OriginalMapping;
import com.google.javascript.jscomp.JsMessage.Builder;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.parsing.parser.util.format.SimpleFormat;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

/**
 * Traverses across parsed tree and finds I18N messages. Then it passes it to {@link
 * JsMessageVisitor#processJsMessage(JsMessage, JsMessageDefinition)}.
 */
@GwtIncompatible("JsMessage, java.util.regex")
public abstract class JsMessageVisitor extends AbstractPostOrderCallback implements CompilerPass {

  private static final String MSG_FUNCTION_NAME = "goog.getMsg";
  private static final String MSG_FALLBACK_FUNCTION_NAME = "goog.getMsgWithFallback";

  static final DiagnosticType MESSAGE_HAS_NO_DESCRIPTION =
      DiagnosticType.warning(
          "JSC_MSG_HAS_NO_DESCRIPTION", "Message {0} has no description. Add @desc JsDoc tag.");

  static final DiagnosticType MESSAGE_HAS_NO_TEXT =
      DiagnosticType.warning(
          "JSC_MSG_HAS_NO_TEXT",
          "Message value of {0} is just an empty string. " + "Empty messages are forbidden.");

  static final DiagnosticType MESSAGE_TREE_MALFORMED =
      DiagnosticType.error("JSC_MSG_TREE_MALFORMED", "Message parse tree malformed. {0}");

  static final DiagnosticType MESSAGE_HAS_NO_VALUE =
      DiagnosticType.error("JSC_MSG_HAS_NO_VALUE", "message node {0} has no value");

  static final DiagnosticType MESSAGE_DUPLICATE_KEY =
      DiagnosticType.error(
          "JSC_MSG_KEY_DUPLICATED",
          "duplicate message variable name found for {0}, " + "initial definition {1}:{2}");

  static final DiagnosticType MESSAGE_NODE_IS_ORPHANED =
      DiagnosticType.warning(
          "JSC_MSG_ORPHANED_NODE",
          MSG_FUNCTION_NAME + "() function could be used only with MSG_* property or variable");

  static final DiagnosticType MESSAGE_NOT_INITIALIZED_USING_NEW_SYNTAX =
      DiagnosticType.warning(
          "JSC_MSG_NOT_INITIALIZED_USING_NEW_SYNTAX",
          "message not initialized using " + MSG_FUNCTION_NAME);

  static final DiagnosticType BAD_FALLBACK_SYNTAX =
      DiagnosticType.error(
          "JSC_MSG_BAD_FALLBACK_SYNTAX",
          SimpleFormat.format(
              "Bad syntax. " + "Expected syntax: %s(MSG_1, MSG_2)", MSG_FALLBACK_FUNCTION_NAME));

  static final DiagnosticType FALLBACK_ARG_ERROR =
      DiagnosticType.error(
          "JSC_MSG_FALLBACK_ARG_ERROR", "Could not find message entry for fallback argument {0}");

  private static final String PH_JS_PREFIX = "{$";
  private static final String PH_JS_SUFFIX = "}";

  static final String MSG_PREFIX = "MSG_";

  /**
   * Pattern for unnamed messages.
   *
   * 

All JS messages in JS code should have unique name but messages in generated code (i.e. from * soy template) could have duplicated message names. Later we replace the message names with ids * constructed as a hash of the message content. * *

Soy generates messages with names * MSG_UNNAMED.* . This pattern recognizes such messages. */ private static final Pattern MSG_UNNAMED_PATTERN = Pattern.compile("MSG_UNNAMED.*"); private static final Pattern CAMELCASE_PATTERN = Pattern.compile("[a-z][a-zA-Z\\d]*[_\\d]*"); static final String HIDDEN_DESC_PREFIX = "@hidden"; // For old-style JS messages private static final String DESC_SUFFIX = "_HELP"; private final boolean needToCheckDuplications; private final JsMessage.Style style; private final JsMessage.IdGenerator idGenerator; final AbstractCompiler compiler; /** * The names encountered associated with their defining node and source. We use it for tracking * duplicated message ids in the source code. */ private final Map messageNames = new HashMap<>(); private final Map unnamedMessages = new HashMap<>(); /** * List of found goog.getMsg call nodes. * *

When we visit goog.getMsg() node we add it, and later when we visit its parent we remove it. * All nodes that are left at the end of traversing are orphaned nodes. It means have no * corresponding var or property node. */ private final Set googMsgNodes = new HashSet<>(); private final CheckLevel checkLevel; /** * Creates JS message visitor. * * @param compiler the compiler instance * @param needToCheckDuplications whether to check duplicated messages in traversed * @param style style that should be used during parsing * @param idGenerator generator that used for creating unique ID for the message */ protected JsMessageVisitor( AbstractCompiler compiler, boolean needToCheckDuplications, JsMessage.Style style, JsMessage.IdGenerator idGenerator) { this.compiler = compiler; this.needToCheckDuplications = needToCheckDuplications; this.style = style; this.idGenerator = idGenerator; checkLevel = (style == JsMessage.Style.CLOSURE) ? CheckLevel.ERROR : CheckLevel.WARNING; // TODO(anatol): add flag that decides whether to process UNNAMED messages. // Some projects would not want such functionality (unnamed) as they don't // use SOY templates. } @Override public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, root, this); for (Node msgNode : googMsgNodes) { compiler.report(JSError.make(msgNode, checkLevel, MESSAGE_NODE_IS_ORPHANED)); } } @Override public void visit(NodeTraversal traversal, Node node, Node unused) { collectGetMsgCall(traversal, node); checkMessageInitialization(traversal, node); } /** This method is called for every Node in the sources AST. */ private void checkMessageInitialization(NodeTraversal traversal, Node node) { final Node parent = node.getParent(); String messageKey; final String originalMessageKey; final Node msgNode; final boolean isVar; switch (node.getToken()) { case NAME: // Case: `var MSG_HELLO = 'Message';` if (parent == null || !NodeUtil.isNameDeclaration(parent)) { return; } messageKey = node.getString(); originalMessageKey = node.getOriginalName(); msgNode = node.getFirstChild(); isVar = true; break; case ASSIGN: // Case: `somenamespace.someclass.MSG_HELLO = 'Message';` Node getProp = node.getFirstChild(); if (!getProp.isGetProp()) { return; } messageKey = getProp.getLastChild().getString(); originalMessageKey = getProp.getOriginalName(); msgNode = node.getLastChild(); isVar = false; break; case STRING_KEY: // Case: `var t = {MSG_HELLO: 'Message'}`; if (node.isQuotedString() || !node.hasChildren() || parent.isObjectPattern()) { // Don't require goog.getMsg() for quoted keys // Case: `var msgs = { 'MSG_QUOTED': anything };` // // Don't try to require goog.getMsg() for destructuring assignment targets. // goog.getMsg() needs to be used in a direct assignment to a variable or property // only. // Case: `var {MSG_HELLO} = anything; // Case: `var {something: MSG_HELLO} = anything; return; } checkState(parent.isObjectLit(), parent); messageKey = node.getString(); originalMessageKey = node.getOriginalName(); msgNode = node.getFirstChild(); isVar = false; break; default: return; } if (originalMessageKey != null) { messageKey = originalMessageKey; } // If we've reached this point, then messageKey is the name of a variable or a property that is // being assigned a value and msgNode is the Node representing the value being assigned. // However, we haven't actually determined yet that name looks like it should be a translatable // message or that the value is a call to goog.getMsg(). // Is this a message name? boolean isNewStyleMessage = msgNode != null && msgNode.isCall(); if (!isMessageName(messageKey, isNewStyleMessage)) { return; } if (msgNode == null) { compiler.report(JSError.make(node, MESSAGE_HAS_NO_VALUE, messageKey)); return; } if (isLegalMessageVarAlias(msgNode)) { return; } // Report a warning if a qualified messageKey that looks like a message (e.g. "a.b.MSG_X") // doesn't use goog.getMsg(). if (isNewStyleMessage) { googMsgNodes.remove(msgNode); } else if (style != JsMessage.Style.LEGACY) { // TODO(johnlenz): promote this to an error once existing conflicts have been // cleaned up. compiler.report(JSError.make(node, MESSAGE_NOT_INITIALIZED_USING_NEW_SYNTAX)); if (style == JsMessage.Style.CLOSURE) { // Don't extract the message if we aren't accepting LEGACY messages return; } } boolean isUnnamedMsg = isUnnamedMessageName(messageKey); JsMessage.Builder builder = new JsMessage.Builder(isUnnamedMsg ? null : messageKey); OriginalMapping mapping = compiler.getSourceMapping( traversal.getSourceName(), traversal.getLineNumber(), traversal.getCharno()); if (mapping != null) { builder.setSourceName(mapping.getOriginalFile()); } else { builder.setSourceName(traversal.getSourceName()); } try { if (isVar) { extractMessageFromVariable(builder, node, parent, parent.getParent()); } else { extractMessageFrom(builder, msgNode, node); } } catch (MalformedException ex) { compiler.report(JSError.make(ex.getNode(), MESSAGE_TREE_MALFORMED, ex.getMessage())); return; } JsMessage extractedMessage = builder.build(idGenerator); // If asked to check named internal messages. if (needToCheckDuplications && !isUnnamedMsg && !extractedMessage.isExternal()) { checkIfMessageDuplicated(messageKey, msgNode); } trackMessage(traversal, extractedMessage, messageKey, msgNode, isUnnamedMsg); if (extractedMessage.isEmpty()) { // value of the message is an empty string. Translators do not like it. compiler.report(JSError.make(node, MESSAGE_HAS_NO_TEXT, messageKey)); } // New-style messages must have descriptions. We don't emit a warning // for legacy-style messages, because there are thousands of // them in legacy code that are not worth the effort to fix, since they've // already been translated anyway. String desc = extractedMessage.getDesc(); if (isNewStyleMessage && (desc == null || desc.trim().isEmpty()) && !extractedMessage.isExternal()) { compiler.report(JSError.make(node, MESSAGE_HAS_NO_DESCRIPTION, messageKey)); } JsMessageDefinition msgDefinition = new JsMessageDefinition(msgNode); processJsMessage(extractedMessage, msgDefinition); } private void collectGetMsgCall(NodeTraversal traversal, Node call) { if (!call.isCall()) { return; } // goog.getMsg() if (call.getFirstChild().matchesQualifiedName(MSG_FUNCTION_NAME)) { googMsgNodes.add(call); } else if (call.getFirstChild().matchesQualifiedName(MSG_FALLBACK_FUNCTION_NAME)) { visitFallbackFunctionCall(traversal, call); } } /** * Track a message for later retrieval. * *

This is used for tracking duplicates, and for figuring out message fallback. Not all message * types are trackable, because that would require a more sophisticated analysis. e.g., function * f(s) { s.MSG_UNNAMED_X = 'Some untrackable message'; } */ private void trackMessage( NodeTraversal t, JsMessage message, String msgName, Node msgNode, boolean isUnnamedMessage) { if (!isUnnamedMessage) { MessageLocation location = new MessageLocation(message, msgNode); messageNames.put(msgName, location); } else { Var var = t.getScope().getVar(msgName); if (var != null) { unnamedMessages.put(var, message); } } } /** * Defines any special cases that are exceptions to what would otherwise be illegal message * assignments. * *

These exceptions are generally due to the pass being designed before new syntax was * introduced. * * @param msgNode Node representing the value assigned to the message variable or property */ private static boolean isLegalMessageVarAlias(Node msgNode) { if (msgNode.isGetProp() && msgNode.isQualifiedName() && msgNode.getLastChild().getString().startsWith(MSG_PREFIX)) { // Case: `foo.Thing.MSG_EXAMPLE_ALIAS = bar.OtherThing.MSG_EXAMPLE;` // // This kind of construct is created by TypeScript code generation and // Es6ToEs3ClassSideInheritance. Just ignore it; the message will have already been extracted // from the base class. return true; } if (!msgNode.isName()) { return false; } String originalName = (msgNode.getOriginalName() != null) ? msgNode.getOriginalName() : msgNode.getString(); if (originalName.startsWith(MSG_PREFIX)) { // Creating an alias for a message is also allowed, and sometimes happens in generated code, // including some of the code generated by this compiler's transpilations. // e.g. // `var MSG_EXAMPLE_ALIAS = MSG_EXAMPLE;` // `var {MSG_HELLO_ALIAS} = MSG_HELLO; // `var {MSG_HELLO_ALIAS: goog$module$my$module_MSG_HELLO} = x;`). // `exports = {MSG_FOO}` // or `exports = {MSG_FOO: MSG_FOO}` when used with declareLegacyNamespace. return true; } return false; } /** Get a previously tracked message. */ private JsMessage getTrackedMessage(NodeTraversal t, String msgName) { boolean isUnnamedMessage = isUnnamedMessageName(msgName); if (!isUnnamedMessage) { MessageLocation location = messageNames.get(msgName); return location == null ? null : location.message; } else { Var var = t.getScope().getVar(msgName); if (var != null) { return unnamedMessages.get(var); } } return null; } /** * Checks if message already processed. If so - it generates 'message duplicated' compiler error. * * @param msgName the name of the message * @param msgNode the node that represents JS message */ private void checkIfMessageDuplicated(String msgName, Node msgNode) { if (messageNames.containsKey(msgName)) { MessageLocation location = messageNames.get(msgName); compiler.report( JSError.make( msgNode, MESSAGE_DUPLICATE_KEY, msgName, location.messageNode.getSourceFileName(), Integer.toString(location.messageNode.getLineno()))); } } /** * Creates a {@link JsMessage} for a JS message defined using a JS variable declaration (e.g * var MSG_X = ...;). * * @param builder the message builder * @param nameNode a NAME node for a JS message variable * @param parentNode a VAR node, parent of {@code nameNode} * @param grandParentNode the grandparent of {@code nameNode}. This node is only used to get meta * data about the message that might be surrounding it (e.g. a message description). This * argument may be null if the meta data is not needed. * @throws MalformedException if {@code varNode} does not correspond to a valid JS message VAR * node */ private void extractMessageFromVariable( Builder builder, Node nameNode, Node parentNode, @Nullable Node grandParentNode) throws MalformedException { // Determine the message's value Node valueNode = nameNode.getFirstChild(); switch (valueNode.getToken()) { case STRING: case ADD: maybeInitMetaDataFromJsDocOrHelpVar(builder, parentNode, grandParentNode); builder.appendStringPart(extractStringFromStringExprNode(valueNode)); break; case FUNCTION: maybeInitMetaDataFromJsDocOrHelpVar(builder, parentNode, grandParentNode); extractFromFunctionNode(builder, valueNode); break; case CALL: maybeInitMetaDataFromJsDoc(builder, parentNode); extractFromCallNode(builder, valueNode); break; default: throw new MalformedException( "Cannot parse value of message " + builder.getKey(), valueNode); } } /** * Creates a {@link JsMessage} for a JS message defined using an assignment to a qualified name * (e.g a.b.MSG_X = goog.getMsg(...);). * * @param builder the message builder * @param valueNode a node in a JS message value * @param docNode the node containing the jsdoc. * @throws MalformedException if {@code getPropNode} does not correspond to a valid JS message * node */ private void extractMessageFrom(Builder builder, Node valueNode, Node docNode) throws MalformedException { maybeInitMetaDataFromJsDoc(builder, docNode); extractFromCallNode(builder, valueNode); } /** * Initializes the meta data in a JsMessage by examining the nodes just before and after a message * VAR node. * * @param builder the message builder whose meta data will be initialized * @param varNode the message VAR node * @param parentOfVarNode {@code varNode}'s parent node */ private void maybeInitMetaDataFromJsDocOrHelpVar( Builder builder, Node varNode, @Nullable Node parentOfVarNode) throws MalformedException { // First check description in @desc if (maybeInitMetaDataFromJsDoc(builder, varNode)) { return; } // Check the preceding node for meta data if ((parentOfVarNode != null) && maybeInitMetaDataFromHelpVar(builder, varNode.getPrevious())) { return; } // Check the subsequent node for meta data maybeInitMetaDataFromHelpVar(builder, varNode.getNext()); } /** * Initializes the meta data in a JsMessage by examining a node just before or after a message VAR * node. * * @param builder the message builder whose meta data will be initialized * @param sibling a node adjacent to the message VAR node * @return true iff message has corresponding description variable */ private static boolean maybeInitMetaDataFromHelpVar(Builder builder, @Nullable Node sibling) throws MalformedException { if ((sibling != null) && (sibling.isVar())) { Node nameNode = sibling.getFirstChild(); String name = nameNode.getString(); if (name.equals(builder.getKey() + DESC_SUFFIX)) { Node valueNode = nameNode.getFirstChild(); String desc = extractStringFromStringExprNode(valueNode); if (desc.startsWith(HIDDEN_DESC_PREFIX)) { builder.setDesc(desc.substring(HIDDEN_DESC_PREFIX.length()).trim()); builder.setIsHidden(true); } else { builder.setDesc(desc); } return true; } } return false; } /** * Initializes the meta data in a message builder given a node that may contain JsDoc properties. * * @param builder the message builder whose meta data will be initialized * @param node the node with the message's JSDoc properties * @return true if message has JsDoc with valid description in @desc annotation */ private static boolean maybeInitMetaDataFromJsDoc(Builder builder, Node node) { boolean messageHasDesc = false; JSDocInfo info = node.getJSDocInfo(); if (info != null) { String desc = info.getDescription(); if (desc != null) { builder.setDesc(desc); messageHasDesc = true; } if (info.isHidden()) { builder.setIsHidden(true); } if (info.getMeaning() != null) { builder.setMeaning(info.getMeaning()); } if (info.getAlternateMessageId() != null) { builder.setAlternateId(info.getAlternateMessageId()); } } return messageHasDesc; } /** * Returns the string value associated with a node representing a JS string or several JS strings * added together (e.g. {@code 'str'} or {@code 's' + 't' + 'r'}). * * @param node the node from where we extract the string * @return String representation of the node * @throws MalformedException if the parsed message is invalid */ private static String extractStringFromStringExprNode(Node node) throws MalformedException { switch (node.getToken()) { case STRING: return node.getString(); case TEMPLATELIT: if (node.hasOneChild()) { // Cooked string can be null only for tagged template literals. // A tagged template literal would hit the default case below. return checkNotNull(node.getFirstChild().getCookedString()); } else { throw new MalformedException( "Template literals with substitutions are not allowed.", node); } case ADD: StringBuilder sb = new StringBuilder(); for (Node child : node.children()) { sb.append(extractStringFromStringExprNode(child)); } return sb.toString(); default: throw new MalformedException( "STRING or ADD node expected; found: " + node.getToken(), node); } } /** * Initializes a message builder from a FUNCTION node. * *

* *

   * The tree should look something like:
   *
   * function
   *  |-- name
   *  |-- lp
   *  |   |-- name 
   *  |    -- name 
   *   -- block
   *      |
   *       --return
   *           |
   *            --add
   *               |-- string foo
   *                -- name 
   * 
* * @param builder the message builder * @param node the function node that contains a message * @throws MalformedException if the parsed message is invalid */ private void extractFromFunctionNode(Builder builder, Node node) throws MalformedException { Set phNames = new HashSet<>(); for (Node fnChild : node.children()) { switch (fnChild.getToken()) { case NAME: // This is okay. The function has a name, but it is empty. break; case PARAM_LIST: // Parse the placeholder names from the function argument list. for (Node argumentNode : fnChild.children()) { if (argumentNode.isName()) { String phName = argumentNode.getString(); if (phNames.contains(phName)) { throw new MalformedException("Duplicate placeholder name: " + phName, argumentNode); } else { phNames.add(phName); } } } break; case BLOCK: // Build the message's value by examining the return statement Node returnNode = fnChild.getFirstChild(); if (!returnNode.isReturn()) { throw new MalformedException( "RETURN node expected; found: " + returnNode.getToken(), returnNode); } for (Node child : returnNode.children()) { extractFromReturnDescendant(builder, child); } // Check that all placeholders from the message text have appropriate // object literal keys for (String phName : builder.getPlaceholders()) { if (!phNames.contains(phName)) { throw new MalformedException( "Unrecognized message placeholder referenced: " + phName, returnNode); } } break; default: throw new MalformedException( "NAME, PARAM_LIST, or BLOCK node expected; found: " + node, fnChild); } } } /** * Appends value parts to the message builder by traversing the descendants of the given RETURN * node. * * @param builder the message builder * @param node the node from where we extract a message * @throws MalformedException if the parsed message is invalid */ private static void extractFromReturnDescendant(Builder builder, Node node) throws MalformedException { switch (node.getToken()) { case STRING: builder.appendStringPart(node.getString()); break; case NAME: builder.appendPlaceholderReference(node.getString()); break; case ADD: for (Node child : node.children()) { extractFromReturnDescendant(builder, child); } break; default: throw new MalformedException( "STRING, NAME, or ADD node expected; found: " + node.getToken(), node); } } /** * Initializes a message builder from a CALL node. * *

The tree should look 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'
   * 
* * @param builder the message builder * @param node the call node from where we extract the message * @throws MalformedException if the parsed message is invalid */ private void extractFromCallNode(Builder builder, Node node) throws MalformedException { // Check the function being called if (!node.isCall()) { throw new MalformedException( "Message must be initialized using " + MSG_FUNCTION_NAME + " function.", node); } Node fnNameNode = node.getFirstChild(); if (!fnNameNode.matchesQualifiedName(MSG_FUNCTION_NAME)) { throw new MalformedException( "Message initialized using unrecognized function. " + "Please use " + MSG_FUNCTION_NAME + "() instead.", fnNameNode); } // Get the message string Node stringLiteralNode = fnNameNode.getNext(); if (stringLiteralNode == null) { throw new MalformedException("Message string literal expected", stringLiteralNode); } // Parse the message string and append parts to the builder parseMessageTextNode(builder, stringLiteralNode); Node objLitNode = stringLiteralNode.getNext(); Set phNames = new HashSet<>(); if (objLitNode != null) { // Register the placeholder names if (!objLitNode.isObjectLit()) { throw new MalformedException("OBJLIT node expected", objLitNode); } for (Node aNode = objLitNode.getFirstChild(); aNode != null; aNode = aNode.getNext()) { if (!aNode.isStringKey()) { throw new MalformedException("STRING_KEY node expected as OBJLIT key", aNode); } String phName = aNode.getString(); if (!isLowerCamelCaseWithNumericSuffixes(phName)) { throw new MalformedException("Placeholder name not in lowerCamelCase: " + phName, aNode); } if (phNames.contains(phName)) { throw new MalformedException("Duplicate placeholder name: " + phName, aNode); } phNames.add(phName); } } // Check that all placeholders from the message text have appropriate objlit // values Set usedPlaceholders = builder.getPlaceholders(); for (String phName : usedPlaceholders) { if (!phNames.contains(phName)) { throw new MalformedException( "Unrecognized message placeholder referenced: " + phName, node); } } // Check that objLiteral have only names that are present in the // message text for (String phName : phNames) { if (!usedPlaceholders.contains(phName)) { throw new MalformedException("Unused message placeholder: " + phName, node); } } } /** * Appends the message parts in a JS message value extracted from the given text node. * * @param builder the JS message builder to append parts to * @param node the node with string literal that contains the message text * @throws MalformedException if {@code value} contains a reference to an unregistered placeholder */ private static void parseMessageTextNode(Builder builder, Node node) throws MalformedException { String value = extractStringFromStringExprNode(node); while (true) { int phBegin = value.indexOf(PH_JS_PREFIX); if (phBegin < 0) { // Just a string literal builder.appendStringPart(value); return; } else { if (phBegin > 0) { // A string literal followed by a placeholder builder.appendStringPart(value.substring(0, phBegin)); } // A placeholder. Find where it ends int phEnd = value.indexOf(PH_JS_SUFFIX, phBegin); if (phEnd < 0) { throw new MalformedException( "Placeholder incorrectly formatted in: " + builder.getKey(), node); } String phName = value.substring(phBegin + PH_JS_PREFIX.length(), phEnd); builder.appendPlaceholderReference(phName); int nextPos = phEnd + PH_JS_SUFFIX.length(); if (nextPos < value.length()) { // Iterate on the rest of the message value value = value.substring(nextPos); } else { // The message is parsed return; } } } } /** Visit a call to goog.getMsgWithFallback. */ private void visitFallbackFunctionCall(NodeTraversal t, Node call) { // Check to make sure the function call looks like: // goog.getMsgWithFallback(MSG_1, MSG_2); // or: // goog.getMsgWithFallback(some.import.MSG_1, some.import.MSG_2); if (!call.hasXChildren(3) || !isMessageIdentifier(call.getSecondChild()) || !isMessageIdentifier(call.getLastChild())) { compiler.report(JSError.make(call, BAD_FALLBACK_SYNTAX)); return; } Node firstArg = call.getSecondChild(); String name = getMessageNameFromNode(firstArg); JsMessage firstMessage = getTrackedMessage(t, name); if (firstMessage == null) { compiler.report(JSError.make(firstArg, FALLBACK_ARG_ERROR, name)); return; } Node secondArg = firstArg.getNext(); name = getMessageNameFromNode(secondArg); if (name == null) { name = secondArg.getString(); } JsMessage secondMessage = getTrackedMessage(t, name); if (secondMessage == null) { compiler.report(JSError.make(secondArg, FALLBACK_ARG_ERROR, name)); return; } processMessageFallback(call, firstMessage, secondMessage); } /** * Processes found JS message. Several examples of "standard" processing routines are: * *
    *
  1. extract all JS messages *
  2. replace JS messages with localized versions for some specific language *
  3. check that messages have correct syntax and present in localization bundle *
* * @param message the found message * @param definition the definition of the object and usually contains all additional message * information like message node/parent's node */ protected abstract void processJsMessage(JsMessage message, JsMessageDefinition definition); /** * Processes the goog.getMsgWithFallback primitive. goog.getMsgWithFallback(MSG_1, MSG_2); * *

By default, does nothing. */ void processMessageFallback(Node callNode, JsMessage message1, JsMessage message2) {} /** Returns whether the given JS identifier is a valid JS message name. */ boolean isMessageName(String identifier, boolean isNewStyleMessage) { return identifier.startsWith(MSG_PREFIX) && (style == JsMessage.Style.CLOSURE || isNewStyleMessage || !identifier.endsWith(DESC_SUFFIX)); } private static boolean isMessageIdentifier(Node node) { return getMessageNameFromNode(node) != null; } /** * Extracts a message name (e.g. MSG_FOO) from either a NAME node or a GETPROP node. This should * cover all of the following cases: * *

    *
  1. a NAME node (e.g. MSG_FOO) *
  2. a NAME node which is the product of renaming (e.g. $module$contents$MSG_FOO) *
  3. a GETPROP node (e.g. some.import.MSG_FOO) *
*/ private static String getMessageNameFromNode(Node node) { String messageName = node.getQualifiedName(); if (messageName == null) { return null; } if (messageName.contains(MSG_PREFIX)) { return messageName.substring(messageName.indexOf(MSG_PREFIX)); } return null; } /** Returns whether the given message name is in the unnamed namespace. */ private static boolean isUnnamedMessageName(String identifier) { return MSG_UNNAMED_PATTERN.matcher(identifier).matches(); } /** * Returns whether a string is nonempty, begins with a lowercase letter, and contains only digits * and underscores after the first underscore. */ static boolean isLowerCamelCaseWithNumericSuffixes(String input) { return CAMELCASE_PATTERN.matcher(input).matches(); } /** * Converts the given string from upper-underscore case to lower-camel case, preserving numeric * suffixes. For example: "NAME" -> "name" "A4_LETTER" -> "a4Letter" "START_SPAN_1_23" -> * "startSpan_1_23". */ static String toLowerCamelCaseWithNumericSuffixes(String input) { // Determine where the numeric suffixes begin int suffixStart = input.length(); while (suffixStart > 0) { char ch = '\0'; int numberStart = suffixStart; while (numberStart > 0) { ch = input.charAt(numberStart - 1); if (Character.isDigit(ch)) { numberStart--; } else { break; } } if ((numberStart > 0) && (numberStart < suffixStart) && (ch == '_')) { suffixStart = numberStart - 1; } else { break; } } if (suffixStart == input.length()) { return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, input); } else { return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, input.substring(0, suffixStart)) + input.substring(suffixStart); } } /** * Checks a node's type. * * @throws MalformedException if the node is null or the wrong type */ protected void checkNode(@Nullable Node node, Token type) throws MalformedException { if (node == null) { throw new MalformedException("Expected node type " + type + "; found: null", node); } if (node.getToken() != type) { throw new MalformedException( "Expected node type " + type + "; found: " + node.getToken(), node); } } static class MalformedException extends Exception { private static final long serialVersionUID = 1L; private final Node node; MalformedException(String message, Node node) { super(message); this.node = node; } Node getNode() { return node; } } private static class MessageLocation { private final JsMessage message; private final Node messageNode; private MessageLocation(JsMessage message, Node messageNode) { this.message = message; this.messageNode = messageNode; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy